feat: Integrate static argobox pages and update theme

This commit is contained in:
Daniel LaForce 2025-04-27 16:02:11 -06:00
parent f1f5ac4b33
commit 7681c34bc1
30 changed files with 13974 additions and 1148 deletions

10
package-lock.json generated
View File

@ -14,6 +14,7 @@
"@astrojs/sitemap": "latest",
"@astrojs/tailwind": "latest",
"astro": "latest",
"nodemailer": "^6.10.1",
"tailwindcss": "^3.0.24"
},
"devDependencies": {
@ -5167,6 +5168,15 @@
"integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==",
"license": "MIT"
},
"node_modules/nodemailer": {
"version": "6.10.1",
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.10.1.tgz",
"integrity": "sha512-Z+iLaBGVaSjbIzQ4pX6XV41HrooLsQ10ZWPUehGmuantvzWoDVBnmsdUcOIDM1t+yPor5pDhVlDESgOMEGxhHA==",
"license": "MIT-0",
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/normalize-path": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",

View File

@ -15,6 +15,7 @@
"@astrojs/sitemap": "latest",
"@astrojs/tailwind": "latest",
"astro": "latest",
"nodemailer": "^6.10.1",
"tailwindcss": "^3.0.24"
},
"devDependencies": {

View File

@ -7,6 +7,7 @@ const navItems = [
{ name: 'Home', url: '/' },
{ name: 'Blog', url: '/blog' },
{ name: 'Projects', url: '/projects' },
{ name: 'Tech Stack', url: '/tech-stack' },
{ name: 'Home Lab', url: 'https://argobox.com' },
{ name: 'Resources', url: '/resources' },
{ name: 'About', url: 'https://laforceit.com' },

View File

@ -16,115 +16,128 @@ export interface GraphEdge {
}
interface Props {
currentPost: any;
currentPost?: any; // Made optional
relatedPosts?: any[];
graphData?: { nodes: GraphNode[], edges: GraphEdge[] }; // New optional prop for pre-defined graph data
height?: string; // Optional height prop for customizing the graph height
}
const { currentPost, relatedPosts = [] } = Astro.props;
const { currentPost, relatedPosts = [], graphData: initialGraphData, height = "200px" } = Astro.props;
// Generate unique ID for the graph container
const graphId = `mini-cy-${Math.random().toString(36).substring(2, 9)}`;
// Ensure currentPost has necessary properties
const safeCurrentPost = {
id: currentPost.slug || 'current-post',
title: currentPost.data?.title || 'Current Post',
tags: currentPost.data?.tags || [],
category: currentPost.data?.category || 'Uncategorized',
};
// Prepare graph data
const nodes: GraphNode[] = [];
const edges: GraphEdge[] = [];
let nodes: GraphNode[] = [];
let edges: GraphEdge[] = [];
const addedTagIds = new Set<string>();
const addedPostIds = new Set<string>();
// Add current post node
nodes.push({
id: safeCurrentPost.id,
label: safeCurrentPost.title,
type: 'post',
url: `/posts/${safeCurrentPost.id}/`
});
addedPostIds.add(safeCurrentPost.id);
// Check if we have pre-defined graph data
if (initialGraphData && initialGraphData.nodes && initialGraphData.edges) {
// Use the provided graph data
nodes = initialGraphData.nodes;
edges = initialGraphData.edges;
} else if (currentPost) {
// Generate graph data from currentPost and relatedPosts
// Ensure currentPost has necessary properties
const safeCurrentPost = {
id: currentPost.slug || 'current-post',
title: currentPost.data?.title || 'Current Post',
tags: currentPost.data?.tags || [],
category: currentPost.data?.category || 'Uncategorized',
};
// Add tags from current post
safeCurrentPost.tags.forEach((tag: string) => {
const tagId = `tag-${tag}`;
// Only add if not already added
if (!addedTagIds.has(tagId)) {
nodes.push({
id: tagId,
label: tag,
type: 'tag',
url: `/tag/${tag}/`
});
addedTagIds.add(tagId);
}
// Add edge from current post to tag
edges.push({
source: safeCurrentPost.id,
target: tagId,
type: 'post-tag'
// Add current post node
nodes.push({
id: safeCurrentPost.id,
label: safeCurrentPost.title,
type: 'post',
url: `/posts/${safeCurrentPost.id}/`
});
});
addedPostIds.add(safeCurrentPost.id);
// Add related posts and their connections
if (relatedPosts && relatedPosts.length > 0) {
relatedPosts.forEach(post => {
if (!post) return;
// Add tags from current post
safeCurrentPost.tags.forEach((tag: string) => {
const tagId = `tag-${tag}`;
const postId = post.slug || `post-${Math.random().toString(36).substring(2, 9)}`;
// Skip if already added or is the current post
if (addedPostIds.has(postId) || postId === safeCurrentPost.id) {
return;
// Only add if not already added
if (!addedTagIds.has(tagId)) {
nodes.push({
id: tagId,
label: tag,
type: 'tag',
url: `/tag/${tag}/`
});
addedTagIds.add(tagId);
}
// Add related post node
nodes.push({
id: postId,
label: post.data?.title || 'Related Post',
type: 'post',
url: `/posts/${postId}/`
});
addedPostIds.add(postId);
// Add edge from current post to related post
// Add edge from current post to tag
edges.push({
source: safeCurrentPost.id,
target: postId,
type: 'post-post'
});
// Add shared tags and their connections
const postTags = post.data?.tags || [];
postTags.forEach((tag: string) => {
// Only add connections for tags that the current post also has
if (safeCurrentPost.tags.includes(tag)) {
const tagId = `tag-${tag}`;
// Add edge from related post to shared tag
edges.push({
source: postId,
target: tagId,
type: 'post-tag'
});
}
target: tagId,
type: 'post-tag'
});
});
// Add related posts and their connections
if (relatedPosts && relatedPosts.length > 0) {
relatedPosts.forEach(post => {
if (!post) return;
const postId = post.slug || `post-${Math.random().toString(36).substring(2, 9)}`;
// Skip if already added or is the current post
if (addedPostIds.has(postId) || postId === safeCurrentPost.id) {
return;
}
// Add related post node
nodes.push({
id: postId,
label: post.data?.title || 'Related Post',
type: 'post',
url: `/posts/${postId}/`
});
addedPostIds.add(postId);
// Add edge from current post to related post
edges.push({
source: safeCurrentPost.id,
target: postId,
type: 'post-post'
});
// Add shared tags and their connections
const postTags = post.data?.tags || [];
postTags.forEach((tag: string) => {
// Only add connections for tags that the current post also has
if (safeCurrentPost.tags.includes(tag)) {
const tagId = `tag-${tag}`;
// Add edge from related post to shared tag
edges.push({
source: postId,
target: tagId,
type: 'post-tag'
});
}
});
});
}
}
// Generate graph data
// Generate final graph data
const graphData = { nodes, edges };
// Determine if we have a valid central node (first node)
const hasCentralNode = nodes.length > 0;
---
<div class="sidebar-card knowledge-graph-card">
<h3 class="sidebar-title">Post Connections</h3>
<div class="mini-knowledge-graph">
<div id={graphId} class="mini-cy"></div>
<div id={graphId} class="mini-cy" style={`height: ${height};`}></div>
</div>
</div>
@ -142,7 +155,7 @@ const graphData = { nodes, edges };
.mini-knowledge-graph {
position: relative;
width: 100%;
height: 200px;
height: auto; /* Height will be set by the inline style */
}
.mini-cy {
@ -155,7 +168,7 @@ const graphData = { nodes, edges };
}
</style>
<script define:vars={{ graphId, graphData }}>
<script define:vars={{ graphId, graphData, hasCentralNode }}>
// Initialize the miniature knowledge graph
function initializeMiniGraph() {
// Ensure Cytoscape is available
@ -179,6 +192,102 @@ const graphData = { nodes, edges };
return;
}
// Create the style array with base styles
const styleArray = [
// Node styling
{
selector: 'node',
style: {
'background-color': '#3B82F6', // Default blue for posts
'label': 'data(label)',
'width': 15,
'height': 15,
'font-size': '8px',
'color': '#E2E8F0',
'text-valign': 'bottom',
'text-halign': 'center',
'text-margin-y': 4,
'text-wrap': 'ellipsis',
'text-max-width': '60px',
'border-width': 1,
'border-color': '#0F1219',
'border-opacity': 0.8
}
},
// Post node specific styles
{
selector: 'node[type="post"]',
style: {
'background-color': '#3B82F6', // Blue for posts
'shape': 'ellipse',
'width': 18,
'height': 18
}
},
// Tag node specific styles
{
selector: 'node[type="tag"]',
style: {
'background-color': '#10B981', // Green for tags
'shape': 'diamond',
'width': 15,
'height': 15
}
},
// Edge styles
{
selector: 'edge',
style: {
'width': 1,
'line-color': 'rgba(226, 232, 240, 0.4)',
'curve-style': 'bezier',
'opacity': 0.6
}
},
// Post-tag edge specific styles
{
selector: 'edge[type="post-tag"]',
style: {
'line-color': 'rgba(16, 185, 129, 0.6)', // Green
'line-style': 'dashed'
}
},
// Post-post edge specific styles
{
selector: 'edge[type="post-post"]',
style: {
'line-color': 'rgba(59, 130, 246, 0.6)', // Blue
'line-style': 'solid',
'width': 1.5
}
},
// Hover styles
{
selector: 'node:hover',
style: {
'background-color': '#F59E0B', // Amber on hover
'border-color': '#FFFFFF',
'border-width': 2,
'cursor': 'pointer'
}
}
];
// Only add the central node styling if we have a valid central node
// (This addresses the optional refinement in the requirements)
if (hasCentralNode) {
styleArray.push({
selector: `#${graphData.nodes[0]?.id}`,
style: {
'background-color': '#06B6D4', // Cyan for current post
'width': 25,
'height': 25,
'border-width': 2,
'border-color': '#E2E8F0'
}
});
}
// Initialize Cytoscape with improved layout parameters for small space
const cy = cytoscape({
container,
@ -200,96 +309,7 @@ const graphData = { nodes, edges };
}
}))
],
style: [
// Node styling
{
selector: 'node',
style: {
'background-color': '#3B82F6', // Default blue for posts
'label': 'data(label)',
'width': 15,
'height': 15,
'font-size': '8px',
'color': '#E2E8F0',
'text-valign': 'bottom',
'text-halign': 'center',
'text-margin-y': 4,
'text-wrap': 'ellipsis',
'text-max-width': '60px',
'border-width': 1,
'border-color': '#0F1219',
'border-opacity': 0.8
}
},
// Post node specific styles
{
selector: 'node[type="post"]',
style: {
'background-color': '#3B82F6', // Blue for posts
'shape': 'ellipse',
'width': 18,
'height': 18
}
},
// Current post node (first in the nodes array)
{
selector: `#${graphData.nodes[0]?.id}`,
style: {
'background-color': '#06B6D4', // Cyan for current post
'width': 25,
'height': 25,
'border-width': 2,
'border-color': '#E2E8F0'
}
},
// Tag node specific styles
{
selector: 'node[type="tag"]',
style: {
'background-color': '#10B981', // Green for tags
'shape': 'diamond',
'width': 15,
'height': 15
}
},
// Edge styles
{
selector: 'edge',
style: {
'width': 1,
'line-color': 'rgba(226, 232, 240, 0.4)',
'curve-style': 'bezier',
'opacity': 0.6
}
},
// Post-tag edge specific styles
{
selector: 'edge[type="post-tag"]',
style: {
'line-color': 'rgba(16, 185, 129, 0.6)', // Green
'line-style': 'dashed'
}
},
// Post-post edge specific styles
{
selector: 'edge[type="post-post"]',
style: {
'line-color': 'rgba(59, 130, 246, 0.6)', // Blue
'line-style': 'solid',
'width': 1.5
}
},
// Hover styles
{
selector: 'node:hover',
style: {
'background-color': '#F59E0B', // Amber on hover
'border-color': '#FFFFFF',
'border-width': 2,
'cursor': 'pointer'
}
}
],
style: styleArray,
// Use a compact layout for sidebar
layout: {
name: 'cose',

1075
src/pages/ansible-docs.html Normal file

File diff suppressed because it is too large Load Diff

615
src/pages/ansible-help.html Normal file
View File

@ -0,0 +1,615 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Ansible Sandbox Help | Argobox</title>
<!-- SEO Meta Tags -->
<meta name="description" content="Help guide for using the Ansible Sandbox environment. Learn how to deploy infrastructure as code demonstrations.">
<!-- Favicon -->
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 width=%22256%22 height=%22256%22 viewBox=%220 0 100 100%22><rect width=%22100%22 height=%22100%22 rx=%2220%22 fill=%22%230f172a%22></rect><path fill=%22%233b82f6%22 d=%22M30 40L50 20L70 40L50 60L30 40Z%22></path><path fill=%22%233b82f6%22 d=%22M50 60L70 40L70 70L50 90L30 70L30 40L50 60Z%22 fill-opacity=%220.6%22></path></svg>">
<!-- Google Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
<!-- FontAwesome -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/css/all.min.css">
<!-- Main CSS -->
<link rel="stylesheet" href="styles.css">
<style>
:root {
--primary-bg: #0f172a;
--secondary-bg: #1e293b;
--accent: #3b82f6;
--accent-darker: #2563eb;
--accent-gradient: linear-gradient(135deg, #2563eb, #3b82f6);
--accent-glow: 0 0 15px rgba(59, 130, 246, 0.5);
--text-primary: #e2e8f0;
--text-secondary: #94a3b8;
--text-accent: #3b82f6;
--success: #10b981;
--warning: #f59e0b;
--error: #ef4444;
--info: #0ea5e9;
--border: rgba(71, 85, 105, 0.5);
--card-bg: rgba(30, 41, 59, 0.8);
--card-hover-bg: rgba(30, 41, 59, 0.95);
--card-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.3);
--transition-normal: 0.3s ease;
--glass-effect: blur(10px);
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Inter', system-ui, -apple-system, sans-serif;
background-color: var(--primary-bg);
color: var(--text-primary);
line-height: 1.6;
background-image:
radial-gradient(circle at 20% 35%, rgba(29, 78, 216, 0.15) 0%, transparent 50%),
radial-gradient(circle at 75% 60%, rgba(14, 165, 233, 0.1) 0%, transparent 50%);
display: flex;
flex-direction: column;
min-height: 100vh;
}
.content-wrapper {
flex: 1;
display: flex;
flex-direction: column;
}
.container {
max-width: 900px;
margin: 0 auto;
padding: 1.5rem;
}
.main-content {
margin-top: 140px; /* Space for navbar */
padding-bottom: 60px; /* Space for footer */
}
.help-header {
text-align: center;
margin-bottom: 3rem;
position: relative;
}
.help-title {
font-size: 2.5rem;
font-weight: 700;
margin-bottom: 1rem;
}
.help-subtitle {
color: var(--text-secondary);
font-size: 1.2rem;
max-width: 700px;
margin: 0 auto;
}
.back-link {
position: absolute;
top: 0;
left: 0;
display: flex;
align-items: center;
gap: 0.5rem;
color: var(--accent);
font-weight: 500;
transition: all var(--transition-normal);
}
.back-link:hover {
color: var(--accent-darker);
transform: translateX(-3px);
}
.help-section {
background-color: var(--card-bg);
border: 1px solid var(--border);
border-radius: 1rem;
padding: 2rem;
margin-bottom: 2rem;
}
.help-section-title {
font-size: 1.5rem;
font-weight: 600;
margin-bottom: 1.5rem;
color: var(--accent);
position: relative;
padding-bottom: 0.75rem;
}
.help-section-title::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
width: 50px;
height: 3px;
background: var(--accent-gradient);
border-radius: 3px;
}
.step-grid {
display: grid;
grid-template-columns: auto 1fr;
gap: 1.5rem 1rem;
margin-bottom: 1.5rem;
}
.step-number {
background: var(--accent-gradient);
color: white;
width: 32px;
height: 32px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-weight: 600;
font-size: 0.9rem;
}
.step-title {
font-weight: 600;
font-size: 1.1rem;
align-self: center;
}
.step-content {
grid-column: span 2;
color: var(--text-secondary);
}
.step-content p {
margin-bottom: 1rem;
}
.step-content p:last-child {
margin-bottom: 0;
}
.step-image {
grid-column: span 2;
border-radius: 0.5rem;
border: 1px solid var(--border);
overflow: hidden;
margin-top: 0.5rem;
}
.step-image img {
width: 100%;
display: block;
}
.tip {
background-color: rgba(59, 130, 246, 0.1);
border-left: 4px solid var(--accent);
padding: 1rem;
border-radius: 0 0.5rem 0.5rem 0;
margin: 1.5rem 0;
}
.tip-title {
font-weight: 600;
color: var(--accent);
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.5rem;
}
.faq-item {
margin-bottom: 1.5rem;
}
.faq-question {
font-weight: 600;
margin-bottom: 0.5rem;
color: var(--text-primary);
}
.faq-answer {
color: var(--text-secondary);
}
.key-term {
font-weight: 500;
color: var(--accent);
}
.help-footer {
text-align: center;
margin-top: 3rem;
color: var(--text-secondary);
font-size: 0.9rem;
}
.help-footer a {
color: var(--accent);
text-decoration: none;
}
.help-footer a:hover {
text-decoration: underline;
}
@media (max-width: 768px) {
.step-grid {
grid-template-columns: 1fr;
gap: 0.75rem;
}
.step-number {
margin-bottom: 0.25rem;
}
.step-title, .step-content, .step-image {
grid-column: 1;
}
}
.live-indicator {
width: 8px;
height: 8px;
background-color: var(--success);
border-radius: 50%;
position: relative;
}
.live-indicator.offline {
background-color: var(--error);
}
.live-indicator::after {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
border-radius: 50%;
background-color: var(--success);
animation: pulse 2s infinite;
z-index: -1;
}
.live-indicator.offline::after {
background-color: var(--error);
}
.logo-text-glow, .logo-dot-glow {
transition: all var(--transition-normal);
}
.logo-text-glow {
text-shadow: 0 0 10px rgba(59, 130, 246, 0.5);
color: var(--accent);
}
.logo-dot-glow {
color: var(--text-primary);
}
</style>
</head>
<body>
<!-- Navigation Bar -->
<nav class="navbar">
<div class="container">
<div class="logo">
<a href="index.html">
<span class="logo-text-glow">ArgoBox</span><span class="logo-dot-glow">.com</span>
</a>
</div>
<div class="nav-menu">
<a href="index.html#home" class="nav-link">Home</a>
<a href="index.html#technologies" class="nav-link">Technologies</a>
<a href="index.html#services" class="nav-link">Services</a>
<a href="index.html#projects" class="nav-link">Projects</a>
<a href="index.html#dashboards" class="nav-link">Dashboards</a>
<a href="index.html#contact" class="nav-link">Contact</a>
</div>
<div class="nav-buttons">
<a href="dashboard.html" class="dashboard-link" target="_blank">
<span class="live-indicator offline"></span>
<span>Live Dashboard</span>
</a>
<button class="menu-toggle" aria-label="Toggle menu">
<i class="fas fa-bars"></i>
</button>
</div>
</div>
</nav>
<!-- Top Links -->
<div class="top-links">
<a href="https://laforceit.com" class="laforceit-link">
<span class="logo-text-glow">LaForceIT</span><span class="logo-dot-glow">.com</span>
</a>
<a href="construction.html" class="signin-button" target="_blank">
<i class="fas fa-sign-in-alt"></i>
<span>Sign In</span>
</a>
</div>
<!-- Content Wrapper -->
<div class="content-wrapper">
<div class="main-content">
<div class="container">
<header class="help-header">
<a href="ansible-sandbox.html" class="back-link">
<i class="fas fa-arrow-left"></i>
Back to Ansible Sandbox
</a>
<h1 class="help-title">Ansible Sandbox Help Guide</h1>
<p class="help-subtitle">
Learn how to use and get the most out of the Ansible Sandbox environment
</p>
</header>
<div class="help-section">
<h2 class="help-section-title">Getting Started</h2>
<div class="step-grid">
<div class="step-number">1</div>
<div class="step-title">Select a Playbook</div>
<div class="step-content">
<p>The sandbox offers various Ansible playbooks showcasing different infrastructure automation scenarios. Choose one from the left panel based on your interests.</p>
<p>Each playbook comes with a description and information about its complexity level and typical run time.</p>
</div>
</div>
<div class="step-grid">
<div class="step-number">2</div>
<div class="step-title">Explore the Playbook Code</div>
<div class="step-content">
<p>View the Ansible YAML code to understand how the automation works. The code is syntax-highlighted for readability.</p>
<p>Hover over different sections to see what each part of the playbook does.</p>
</div>
</div>
<div class="step-grid">
<div class="step-number">3</div>
<div class="step-title">Configure Parameters</div>
<div class="step-content">
<p>Switch to the "Configuration" tab to customize various parameters for your deployment. These might include:</p>
<ul style="list-style-type: disc; margin-left: 1.5rem;">
<li>Domain names</li>
<li>Directory paths</li>
<li>Feature toggles</li>
<li>VM template selection</li>
<li>Resource allocation</li>
</ul>
</div>
</div>
<div class="step-grid">
<div class="step-number">4</div>
<div class="step-title">Deploy the Environment</div>
<div class="step-content">
<p>Click the "Deploy" button to launch the automation process. The system will:</p>
<ol style="list-style-type: decimal; margin-left: 1.5rem;">
<li>Create necessary virtual machines</li>
<li>Run the selected Ansible playbook</li>
<li>Configure all services</li>
<li>Provide access to the deployed environment</li>
</ol>
</div>
</div>
<div class="step-grid">
<div class="step-number">5</div>
<div class="step-title">Monitor Progress</div>
<div class="step-content">
<p>Watch the deployment process in real-time on the "Output" tab, which shows the Ansible execution log.</p>
<p>Once deployment completes, you'll see a success message with details on how to access the deployed environment.</p>
</div>
</div>
<div class="step-grid">
<div class="step-number">6</div>
<div class="step-title">Explore the Environment</div>
<div class="step-content">
<p>After deployment, you can:</p>
<ul style="list-style-type: disc; margin-left: 1.5rem;">
<li>View the VM status and details in the "VM Status" tab</li>
<li>Access deployed applications via provided URLs</li>
<li>See resource utilization metrics</li>
<li>Monitor the environment's remaining active time</li>
</ul>
</div>
</div>
<div class="tip">
<div class="tip-title">
<i class="fas fa-lightbulb"></i>
<span>Pro Tip</span>
</div>
<p>
Sandbox environments automatically shut down after 30 minutes to conserve resources. You'll see a countdown timer showing the remaining time. Make sure to complete your exploration before time runs out!
</p>
</div>
</div>
<div class="help-section">
<h2 class="help-section-title">Frequently Asked Questions</h2>
<div class="faq-item">
<div class="faq-question">What is an Ansible playbook?</div>
<div class="faq-answer">
An Ansible playbook is a YAML file that describes a set of tasks to be executed on remote servers. Playbooks define the desired state of systems and can configure applications, deploy software, and orchestrate advanced IT workflows.
</div>
</div>
<div class="faq-item">
<div class="faq-question">Is the sandbox environment isolated?</div>
<div class="faq-answer">
Yes, each sandbox environment is completely isolated. Your deployments and configurations won't affect other users or any production systems. This provides a safe space to experiment with infrastructure automation.
</div>
</div>
<div class="faq-item">
<div class="faq-question">Can I save or download the playbooks?</div>
<div class="faq-answer">
Yes, you can copy the playbook code to use in your own environments. Each code segment can be selected and copied to your clipboard. For a more organized approach, visit the Documentation page for downloadable versions of all playbooks.
</div>
</div>
<div class="faq-item">
<div class="faq-question">What happens if I need more time with an environment?</div>
<div class="faq-answer">
Currently, all sandbox environments are limited to 30 minutes. If you need more time, you can always redeploy the environment after it expires, which will give you a fresh 30-minute window to continue your exploration.
</div>
</div>
<div class="faq-item">
<div class="faq-question">Can I modify the playbooks?</div>
<div class="faq-answer">
The playbook code displayed is read-only to ensure consistent and reliable deployments. However, you can customize many aspects of the deployment through the Configuration tab, which allows you to adjust key parameters without modifying the underlying code.
</div>
</div>
<div class="faq-item">
<div class="faq-question">What if I encounter an error during deployment?</div>
<div class="faq-answer">
If an error occurs during deployment, the Output tab will display the specific error message from Ansible. You can use the "Reset" button to clear the environment and try again, potentially with different configuration options.
</div>
</div>
</div>
<div class="help-section">
<h2 class="help-section-title">Key Terms</h2>
<p style="margin-bottom: 1.5rem;">
<span class="key-term">Playbook</span> - A YAML file defining a series of Ansible tasks and configurations.
</p>
<p style="margin-bottom: 1.5rem;">
<span class="key-term">Task</span> - An individual action Ansible executes on a managed host, such as installing a package or creating a file.
</p>
<p style="margin-bottom: 1.5rem;">
<span class="key-term">Role</span> - A reusable, self-contained unit of tasks, variables, files, templates, and modules that can be shared across playbooks.
</p>
<p style="margin-bottom: 1.5rem;">
<span class="key-term">Inventory</span> - A list of hosts that Ansible will manage, grouped logically for targeted execution.
</p>
<p style="margin-bottom: 1.5rem;">
<span class="key-term">Handler</span> - A special type of task that only runs when notified by another task that made a change.
</p>
<p style="margin-bottom: 1.5rem;">
<span class="key-term">Variable</span> - A value that can be set and referenced in playbooks, making them more flexible and reusable.
</p>
<p>
<span class="key-term">Template</span> - A text file that uses variables to create dynamic configuration files.
</p>
</div>
<div class="help-footer">
<p>Need more assistance? <a href="mailto:daniel@argobox.com">Contact Support</a></p>
</div>
</div>
</div>
</div>
<!-- Footer -->
<footer class="footer">
<div class="container">
<div class="footer-content">
<div class="footer-info">
<div class="footer-logo">
<span class="logo-text-glow">ArgoBox</span>
<span class="logo-dot-glow">.com</span>
</div>
<p class="footer-description">
Enterprise-grade home lab environment for DevOps experimentation, infrastructure automation, and containerized application deployment.
</p>
<div class="footer-evolution">
<i class="fas fa-code-branch evolution-icon"></i>
<span>Continuously evolving since 2011</span>
</div>
</div>
<div class="footer-links">
<div class="footer-links-column">
<h3 class="footer-heading">Platforms</h3>
<a href="dashboard.html" class="footer-link">Dashboard</a>
<a href="ansible-sandbox.html" class="footer-link">Ansible Sandbox</a>
<a href="construction.html" class="footer-link">Gitea</a>
<a href="construction.html" class="footer-link">OpenWebUI</a>
</div>
<div class="footer-links-column">
<h3 class="footer-heading">Documentation</h3>
<a href="construction.html" class="footer-link">Architecture</a>
<a href="construction.html" class="footer-link">Kubernetes</a>
<a href="construction.html" class="footer-link">Ansible</a>
<a href="construction.html" class="footer-link">Network</a>
</div>
<div class="footer-links-column">
<h3 class="footer-heading">Resources</h3>
<a href="construction.html" class="footer-link">Ansible Playbooks</a>
<a href="construction.html" class="footer-link">K8s Manifests</a>
<a href="construction.html" class="footer-link">Shell Scripts</a>
<a href="construction.html" class="footer-link">Configuration Files</a>
</div>
</div>
</div>
<div class="footer-bottom">
<p>&copy; <span id="current-year">2025</span> All rights reserved. Inovin LLC</p>
</div>
</div>
</footer>
<!-- JavaScript -->
<script src="script.js"></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
// Set current year in footer
if (document.getElementById('current-year')) {
document.getElementById('current-year').textContent = new Date().getFullYear();
}
// Mobile menu toggle
const menuToggle = document.querySelector('.menu-toggle');
const navMenu = document.querySelector('.nav-menu');
if (menuToggle && navMenu) {
menuToggle.addEventListener('click', function() {
navMenu.classList.toggle('show');
const icon = menuToggle.querySelector('i');
if (navMenu.classList.contains('show')) {
icon.classList.remove('fa-bars');
icon.classList.add('fa-times');
} else {
icon.classList.remove('fa-times');
icon.classList.add('fa-bars');
}
});
}
});
</script>
</body>
</html>

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,627 @@
---
// src/pages/ansible/docs.astro - Converted from static ansible-docs.html
import BaseLayout from '../../layouts/BaseLayout.astro';
import Header from '../../components/Header.astro';
import Footer from '../../components/Footer.astro';
const title = "Ansible Sandbox Documentation | Argobox";
const description = "Comprehensive documentation for the Ansible Sandbox playbooks. Learn about infrastructure automation, deployment patterns, and best practices.";
// Define sidebar structure (can be generated dynamically if needed)
const sidebarNav = [
{
title: "Getting Started",
links: [
{ href: "#introduction", text: "Introduction" },
{ href: "#sandbox-overview", text: "Sandbox Overview" },
{ href: "#infrastructure", text: "Infrastructure Design" },
]
},
{
title: "Playbooks",
links: [
{ href: "#web-server", text: "Web Server Deployment" },
{ href: "#docker-compose", text: "Docker Compose Stack" },
{ href: "#k3s-cluster", text: "K3s Kubernetes Cluster" },
{ href: "#lamp-stack", text: "LAMP Stack" },
{ href: "#security-hardening", text: "Security Hardening" },
]
},
{
title: "Advanced Topics",
links: [
{ href: "#custom-roles", text: "Custom Roles" },
{ href: "#variables-inventory", text: "Variables & Inventory" },
{ href: "#best-practices", text: "Best Practices" },
]
},
{
title: "Reference",
links: [
{ href: "#cli-commands", text: "CLI Commands" },
{ href: "#troubleshooting", text: "Troubleshooting" },
]
}
];
---
<BaseLayout {title} {description}>
{/* Add Font Awesome if not loaded globally */}
{/* <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/css/all.min.css" integrity="sha512-z3gLpd7yknf1YoNbCzqRKc4qyor8gaKU1qmn+CShxbuBusANI9QpRohGBreCFkKxLhei6S9CQXFEbbKuqLg0DA==" crossorigin="anonymous" referrerpolicy="no-referrer" /> */}
<Header slot="header" />
<div class="docs-container">
<aside class="sidebar" id="docs-sidebar">
<div class="sidebar-header">
<h1 class="sidebar-title">Ansible Docs</h1>
<a href="/ansible/sandbox" class="back-link"> {/* Updated Link */}
<i class="fas fa-arrow-left"></i>
<span>Back to Sandbox</span>
</a>
</div>
{sidebarNav.map(section => (
<div class="sidebar-section">
<h2 class="sidebar-section-title">{section.title}</h2>
<ul class="sidebar-nav">
{section.links.map(link => (
<li class="sidebar-nav-item">
<a href={link.href} class="sidebar-nav-link">{link.text}</a>
</li>
))}
</ul>
</div>
))}
<div class="sidebar-section contact-info">
<h2 class="sidebar-section-title">Need Help?</h2>
<p>If you encounter issues or have questions, feel free to reach out:</p>
<a href="mailto:daniel.laforce@argobox.com" class="contact-email">daniel.laforce@argobox.com</a>
</div>
</aside>
<main class="main-content" id="main-docs-content">
<header class="docs-header">
<h1 class="docs-title">Ansible Sandbox Documentation</h1>
<p class="docs-subtitle">Explore the playbooks, understand the infrastructure, and learn best practices for using the interactive Ansible Sandbox.</p>
</header>
<!-- Introduction Section -->
<section id="introduction" class="docs-section">
<h2 class="docs-section-title">Introduction</h2>
<p class="docs-text">
Welcome to the documentation for the ArgoBox Ansible Sandbox. This interactive environment allows you to safely experiment with Ansible playbooks designed to manage various aspects of a modern infrastructure setup, including web servers, containerized applications, and Kubernetes clusters.
</p>
<p class="docs-text">
This documentation provides details on the available playbooks, the underlying infrastructure design of the sandbox environment, and best practices for writing and testing your own automation scripts.
</p>
<div class="docs-note">
<h3 class="docs-note-title"><i class="fas fa-info-circle"></i> Purpose</h3>
<p>The primary goal of the sandbox is to provide a hands-on learning experience for Ansible in a pre-configured, safe environment that mirrors real-world scenarios.</p>
</div>
</section>
<!-- Sandbox Overview Section -->
<section id="sandbox-overview" class="docs-section">
<h2 class="docs-section-title">Sandbox Overview</h2>
<p class="docs-text">
The Ansible Sandbox provides you with temporary access to a set of virtual machines (VMs) where you can execute pre-defined Ansible playbooks or even upload and run your own. The environment is reset periodically to ensure a clean state for each session.
</p>
<h3 class="docs-subsection-title">Key Features</h3>
<ul class="docs-list">
<li>Isolated environment with multiple target VMs.</li>
<li>Pre-loaded collection of common Ansible roles and playbooks.</li>
<li>Ability to select and run specific playbooks via a web interface.</li>
<li>Real-time output streaming of playbook execution.</li>
<li>Network access between sandbox VMs for testing multi-tier applications.</li>
<li>Environment reset functionality.</li>
</ul>
<a href="/ansible/sandbox" class="docs-button"> {/* Updated Link */}
<i class="fab fa-ansible"></i> Launch Ansible Sandbox
</a>
</section>
<!-- Infrastructure Design Section -->
<section id="infrastructure" class="docs-section">
<h2 class="docs-section-title">Infrastructure Design</h2>
<p class="docs-text">
The sandbox environment consists of several virtual machines managed by the main ArgoBox infrastructure, typically running on Proxmox VE. These VMs are provisioned specifically for sandbox use and are isolated from the core production services.
</p>
<h3 class="docs-subsection-title">Components</h3>
<table class="docs-table">
<thead>
<tr>
<th>Component</th>
<th>Description</th>
<th>OS</th>
<th>Purpose</th>
</tr>
</thead>
<tbody>
<tr>
<td>Control Node</td>
<td>The VM where Ansible commands are executed from.</td>
<td>Debian 12</td>
<td>Runs Ansible engine, hosts playbooks.</td>
</tr>
<tr>
<td>Web Server Node</td>
<td>Target VM for web server playbooks (Nginx/Apache).</td>
<td>Ubuntu 22.04</td>
<td>Simulates a typical web server.</td>
</tr>
<tr>
<td>Database Node</td>
<td>Target VM for database playbooks (PostgreSQL/MySQL).</td>
<td>Debian 12</td>
<td>Simulates a database server.</td>
</tr>
<tr>
<td>Docker Node</td>
<td>Target VM with Docker installed for container playbooks.</td>
<td>Ubuntu 22.04</td>
<td>Runs Docker containers via Compose.</td>
</tr>
</tbody>
</table>
<div class="docs-note">
<h3 class="docs-note-title"><i class="fas fa-network-wired"></i> Networking</h3>
<p>All sandbox VMs are placed on a dedicated, isolated VLAN with controlled access to simulate a realistic network environment.</p>
</div>
</section>
<!-- Playbooks Section -->
<section id="playbooks" class="docs-section">
<h2 class="docs-section-title">Available Playbooks</h2>
<p class="docs-text">The following playbooks are available for execution within the sandbox environment. Each playbook demonstrates different Ansible concepts and common infrastructure tasks.</p>
{/* Web Server Playbook */}
<div id="web-server" class="docs-subsection">
<h3 class="docs-subsection-title">Web Server Deployment (Nginx)</h3>
<p class="docs-text">Installs and configures the Nginx web server on the 'Web Server Node'. Includes setting up a basic virtual host and ensuring the service is running.</p>
<div class="docs-code">
<pre><code>---
- name: Deploy Nginx Web Server
hosts: web_server_node # Corresponds to inventory group
become: yes # Execute tasks with sudo
tasks:
- name: Update apt cache
ansible.builtin.apt:
update_cache: yes
tags: [install]
- name: Install Nginx
ansible.builtin.apt:
name: nginx
state: present
tags: [install]
- name: Ensure Nginx service is started and enabled
ansible.builtin.service:
name: nginx
state: started
enabled: yes
tags: [configure]
- name: Deploy basic index page (template example)
ansible.builtin.template:
src: templates/index.html.j2 # Example template path
dest: /var/www/html/index.nginx-debian.html # Default Nginx path
owner: root
group: root
mode: '0644'
tags: [configure]</code></pre>
</div>
{/* Docker Compose Playbook */}
<div id="docker-compose" class="docs-subsection">
<h3 class="docs-subsection-title">Docker Compose Stack (Example: Portainer)</h3>
<p class="docs-text">Deploys a simple Docker Compose application (e.g., Portainer) on the 'Docker Node'. Requires Docker and Docker Compose to be pre-installed on the target.</p>
<div class="docs-code">
<pre><code>---
- name: Deploy Docker Compose Application
hosts: docker_node
become: yes
vars:
app_name: portainer
app_dir: "/opt/{{ app_name }}"
compose_file_url: "https://downloads.portainer.io/ce2-19/portainer-agent-stack.yml" # Example URL
tasks:
- name: Ensure application directory exists
ansible.builtin.file:
path: "{{ app_dir }}"
state: directory
mode: '0755'
- name: Download Docker Compose file
ansible.builtin.get_url:
url: "{{ compose_file_url }}"
dest: "{{ app_dir }}/docker-compose.yml"
mode: '0644'
register: compose_download
- name: Deploy Docker Compose stack
community.docker.docker_compose:
project_src: "{{ app_dir }}"
state: present # Ensures stack is up
when: compose_download.changed # Only run if file was downloaded/updated
</code></pre>
</div>
</div>
{/* K3s Playbook */}
<div id="k3s-cluster" class="docs-subsection">
<h3 class="docs-subsection-title">K3s Kubernetes Cluster (Basic Setup)</h3>
<p class="docs-text">Installs a single-node K3s cluster on the 'Control Node'. Note: This is a simplified setup for demonstration.</p>
<div class="docs-code">
<pre><code>---
- name: Install K3s Single Node Cluster
hosts: control_node # Install on the control node itself for demo
become: yes
tasks:
- name: Download K3s installation script
ansible.builtin.get_url:
url: https://get.k3s.io
dest: /tmp/k3s-install.sh
mode: '0755'
- name: Execute K3s installation script
ansible.builtin.command:
cmd: /tmp/k3s-install.sh
creates: /usr/local/bin/k3s # Avoid re-running if k3s exists
register: k3s_install_result
changed_when: k3s_install_result.rc == 0
- name: Ensure K3s service is started
ansible.builtin.service:
name: k3s
state: started
enabled: yes
- name: Wait for Kubeconfig to be available
ansible.builtin.wait_for:
path: /etc/rancher/k3s/k3s.yaml
timeout: 60
- name: Read Kubeconfig file
ansible.builtin.slurp:
src: /etc/rancher/k3s/k3s.yaml
register: k3s_kubeconfig
- name: Display Kubeconfig hint (for manual use)
ansible.builtin.debug:
msg: "K3s installed. Use 'sudo k3s kubectl get nodes' or copy Kubeconfig."
</code></pre>
</div>
<div class="docs-warning">
<h3 class="docs-warning-title"><i class="fas fa-exclamation-triangle"></i> Simplified Setup</h3>
<p>This playbook installs a basic single-node K3s cluster. Production setups require more complex configuration, multiple nodes, and security considerations.</p>
</div>
</div>
{/* LAMP Stack Playbook */}
<div id="lamp-stack" class="docs-subsection">
<h3 class="docs-subsection-title">LAMP Stack Installation</h3>
<p class="docs-text">Installs Apache, MySQL (MariaDB), and PHP on the 'Web Server Node'.</p>
{/* Add LAMP playbook code example here */}
<p class="docs-text"><i>Code example for LAMP stack coming soon...</i></p>
</div>
{/* Security Hardening Playbook */}
<div id="security-hardening" class="docs-subsection">
<h3 class="docs-subsection-title">Basic Security Hardening</h3>
<p class="docs-text">Applies basic security measures like installing fail2ban and configuring UFW firewall rules on a target node.</p>
{/* Add Security playbook code example here */}
<p class="docs-text"><i>Code example for security hardening coming soon...</i></p>
</div>
</section>
{/* Advanced Topics Section */}
<section id="advanced-topics" class="docs-section">
<h2 class="docs-section-title">Advanced Topics</h2>
<div id="custom-roles" class="docs-subsection">
<h3 class="docs-subsection-title">Using Custom Roles</h3>
<p class="docs-text">Learn how to structure your automation using Ansible Roles for better reusability and organization. Examples of common role structures will be provided.</p>
<p class="docs-text"><i>Details on custom roles coming soon...</i></p>
</div>
<div id="variables-inventory" class="docs-subsection">
<h3 class="docs-subsection-title">Variables and Inventory Management</h3>
<p class="docs-text">Understand how to manage variables for different environments (e.g., development, production) and how to define your infrastructure using Ansible inventory files (static and dynamic).</p>
<p class="docs-text"><i>Details on variables and inventory coming soon...</i></p>
</div>
<div id="best-practices" class="docs-subsection">
<h3 class="docs-subsection-title">Ansible Best Practices</h3>
<p class="docs-text">Explore recommended practices for writing effective, maintainable, and idempotent Ansible playbooks, including task naming, using handlers, and managing secrets.</p>
<p class="docs-text"><i>Best practices details coming soon...</i></p>
</div>
</section>
<!-- Reference Section -->
<section id="reference" class="docs-section">
<h2 class="docs-section-title">Reference</h2>
<div id="cli-commands" class="docs-subsection">
<h3 class="docs-subsection-title">Common CLI Commands</h3>
<p class="docs-text">A quick reference for frequently used Ansible CLI commands.</p>
<table class="docs-table">
<thead>
<tr><th>Command</th><th>Description</th></tr>
</thead>
<tbody>
<tr><td><code>ansible --version</code></td><td>Check Ansible version.</td></tr>
<tr><td><code>ansible-inventory --list -i inventory.yml</code></td><td>List inventory hosts and groups.</td></tr>
<tr><td><code>ansible all -m ping -i inventory.yml</code></td><td>Ping all hosts in inventory.</td></tr>
<tr><td><code>ansible-playbook playbook.yml -i inventory.yml</code></td><td>Run a playbook.</td></tr>
<tr><td><code>ansible-playbook playbook.yml --check</code></td><td>Perform a dry run of a playbook.</td></tr>
<tr><td><code>ansible-playbook playbook.yml --limit web_servers</code></td><td>Run playbook only on specific hosts/groups.</td></tr>
<tr><td><code>ansible-vault create secrets.yml</code></td><td>Create an encrypted vault file.</td></tr>
<tr><td><code>ansible-playbook playbook.yml --ask-vault-pass</code></td><td>Run playbook asking for vault password.</td></tr>
</tbody>
</table>
</div>
<div id="troubleshooting" class="docs-subsection">
<h3 class="docs-subsection-title">Troubleshooting Tips</h3>
<p class="docs-text">Common issues and how to resolve them when working with the sandbox or Ansible in general.</p>
<ul class="docs-list">
<li><strong>Connection Errors:</strong> Ensure SSH keys are correctly configured and target VMs are reachable. Check firewall rules.</li>
<li><strong>Permission Denied:</strong> Use `become: yes` for tasks requiring root privileges. Verify sudo configuration on target nodes.</li>
<li><strong>Module Not Found:</strong> Ensure required Ansible collections or Python libraries are installed on the control node.</li>
<li><strong>Idempotency Issues:</strong> Review tasks to ensure they only make changes when necessary (e.g., use `creates` argument for command/shell modules).</li>
<li><strong>Variable Undefined:</strong> Check variable definitions, scope (host_vars, group_vars), and precedence. Use `-v` or `-vvv` for verbose output.</li>
</ul>
</div>
</section>
<footer class="docs-footer">
Last updated: April 27, 2025 | Need more help? Visit the <a href="https://docs.ansible.com/" target="_blank" rel="noopener noreferrer">Official Ansible Documentation</a>.
</footer>
</main>
</div>
<button class="mobile-menu-toggle" id="mobile-menu-btn" aria-label="Toggle Documentation Menu">
<i class="fas fa-bars"></i>
</button>
<Footer /> {/* Assuming Footer is global */}
</BaseLayout>
<style is:global>
/* Import base styles if needed, or rely on BaseLayout */
/* @import url('../styles/theme.css'); */
/* Styles specific to the docs layout */
.docs-container {
display: flex;
/* Adjust top/bottom margin if header/footer height changes */
/* margin-top: 4.5rem; */ /* Assuming header height */
/* min-height: calc(100vh - 4.5rem - 3rem); */ /* Full height minus header/footer */
}
.sidebar {
width: 280px;
background-color: var(--sidebar-bg, var(--bg-secondary)); /* Use theme var with fallback */
border-right: 1px solid var(--border);
position: sticky; /* Make sidebar sticky */
top: 4.5rem; /* Stick below header */
height: calc(100vh - 4.5rem); /* Full height minus header */
overflow-y: auto;
scrollbar-width: thin;
scrollbar-color: var(--border) transparent;
padding: 2rem 0;
flex-shrink: 0; /* Prevent shrinking */
}
.sidebar::-webkit-scrollbar { width: 6px; }
.sidebar::-webkit-scrollbar-track { background: transparent; }
.sidebar::-webkit-scrollbar-thumb { background-color: var(--border); border-radius: 3px; }
.sidebar-header { padding: 0 1.5rem; margin-bottom: 2rem; }
.sidebar-title { font-size: 1.25rem; font-weight: 600; margin-bottom: 0.5rem; color: var(--text-primary); }
.back-link { display: flex; align-items: center; gap: 0.5rem; color: var(--accent); font-weight: 500; font-size: 0.9rem; padding: 0.5rem 0; transition: all var(--transition-normal); text-decoration: none; }
.back-link:hover { color: var(--accent-darker); transform: translateX(-3px); }
.sidebar-section { margin-bottom: 1.5rem; }
.sidebar-section-title { font-weight: 600; font-size: 0.85rem; text-transform: uppercase; color: var(--text-secondary); letter-spacing: 0.05em; padding: 0 1.5rem; margin-bottom: 0.75rem; }
.sidebar-nav { list-style: none; padding: 0; margin: 0; }
.sidebar-nav-item { /* No extra styles needed */ }
.sidebar-nav-link { display: block; padding: 0.5rem 1.5rem; color: var(--text-secondary); text-decoration: none; transition: all var(--transition-normal); position: relative; font-size: 0.95rem; border-left: 3px solid transparent; /* Add space for active indicator */ }
.sidebar-nav-link:hover { color: var(--text-primary); background-color: rgba(255, 255, 255, 0.05); }
.sidebar-nav-link.active { color: var(--accent); background-color: rgba(59, 130, 246, 0.1); border-left-color: var(--accent); font-weight: 500; }
/* .sidebar-nav-link.active::before { content: ''; position: absolute; top: 0; left: 0; height: 100%; width: 3px; background: var(--accent-gradient); } */ /* Alternative active style */
.contact-info { padding: 0 1.5rem; margin-top: 2rem; }
.contact-info p { margin-bottom: 0.5rem; font-size: 0.9rem; color: var(--text-secondary); }
.contact-email { color: var(--accent); font-weight: 600; text-decoration: none; }
.contact-email:hover { text-decoration: underline; }
.main-content {
flex: 1;
padding: 3rem;
max-width: 1000px; /* Adjust max width as needed */
margin-left: 280px; /* Ensure space for fixed sidebar */
}
.docs-header { margin-bottom: 3rem; border-bottom: 1px solid var(--border); padding-bottom: 1.5rem; }
.docs-title { font-size: 2.5rem; font-weight: 700; margin-bottom: 0.5rem; color: var(--text-primary); }
.docs-subtitle { color: var(--text-secondary); font-size: 1.2rem; }
.docs-section { margin-bottom: 3rem; scroll-margin-top: 6rem; /* Offset for sticky header */ }
.docs-section-title { font-size: 1.75rem; font-weight: 600; margin-bottom: 1.5rem; color: var(--text-primary); position: relative; padding-bottom: 0.75rem; border-bottom: 1px solid var(--border); }
.docs-subsection { margin-bottom: 2rem; }
.docs-subsection-title { font-size: 1.25rem; font-weight: 600; margin-bottom: 1rem; color: var(--accent); }
.docs-text { color: var(--text-secondary); margin-bottom: 1.5rem; line-height: 1.7; }
.docs-text a { color: var(--accent); text-decoration: underline; }
.docs-text a:hover { color: var(--accent-darker); }
.docs-list { list-style-type: disc; margin-left: 1.5rem; margin-bottom: 1.5rem; color: var(--text-secondary); padding-left: 1rem; }
.docs-list li { margin-bottom: 0.5rem; }
.docs-code {
font-family: var(--font-mono); /* Use theme mono font */
background-color: var(--bg-code); /* Use theme code bg */
padding: 1.5rem;
border-radius: 0.5rem;
overflow-x: auto;
margin-bottom: 1.5rem;
border: 1px solid var(--border);
font-size: 0.9rem;
line-height: 1.6;
color: var(--text-code); /* Use theme code text color */
}
.docs-code pre { margin: 0; }
/* Add syntax highlighting styles if using Prism/Shiki integrated with Astro */
.docs-table { width: 100%; border-collapse: collapse; margin-bottom: 1.5rem; font-size: 0.9rem; }
.docs-table th { text-align: left; padding: 0.75rem 1rem; background-color: var(--bg-secondary); color: var(--text-primary); font-weight: 600; border-bottom: 2px solid var(--border); }
.docs-table td { padding: 0.75rem 1rem; border-bottom: 1px solid var(--border); color: var(--text-secondary); }
.docs-table tr:last-child td { border-bottom: none; }
.docs-table code { font-family: var(--font-mono); background-color: rgba(255, 255, 255, 0.05); padding: 0.1em 0.3em; border-radius: 3px; font-size: 0.85em; }
.docs-note, .docs-warning {
padding: 1rem 1.5rem;
border-radius: 0 0.5rem 0.5rem 0;
margin: 1.5rem 0;
font-size: 0.95rem;
}
.docs-note { background-color: rgba(59, 130, 246, 0.1); border-left: 4px solid var(--accent); }
.docs-warning { background-color: rgba(245, 158, 11, 0.1); border-left: 4px solid var(--warning); }
.docs-note-title, .docs-warning-title { font-weight: 600; display: flex; align-items: center; gap: 0.5rem; margin-bottom: 0.5rem; }
.docs-note-title { color: var(--accent); }
.docs-warning-title { color: var(--warning); }
.docs-note p, .docs-warning p { margin: 0; color: var(--text-secondary); }
.docs-button { display: inline-flex; align-items: center; gap: 0.5rem; background-color: var(--accent); color: white; padding: 0.75rem 1.25rem; border-radius: 0.5rem; font-weight: 500; text-decoration: none; transition: all var(--transition-normal); margin-bottom: 1.5rem; border: none; cursor: pointer; }
.docs-button:hover { background-color: var(--accent-darker); transform: translateY(-2px); box-shadow: var(--card-shadow); }
.docs-footer { border-top: 1px solid var(--border); margin-top: 3rem; padding-top: 2rem; color: var(--text-secondary); font-size: 0.9rem; }
.docs-footer a { color: var(--accent); }
.docs-footer a:hover { color: var(--accent-darker); }
.mobile-menu-toggle {
display: none; /* Hidden by default */
position: fixed;
bottom: 1.5rem;
right: 1.5rem;
background: var(--accent-gradient);
color: white;
width: 50px;
height: 50px;
border-radius: 50%;
border: none;
cursor: pointer;
font-size: 1.5rem;
align-items: center;
justify-content: center;
z-index: 1010; /* Above sidebar */
box-shadow: var(--card-shadow);
}
@media (max-width: 992px) {
.sidebar {
position: fixed; /* Change to fixed for mobile overlay */
top: 0; /* Align to top */
left: 0;
height: 100vh; /* Full viewport height */
z-index: 1000; /* Ensure it's above content */
transform: translateX(-100%);
transition: transform var(--transition-normal);
background-color: var(--sidebar-bg, var(--bg-secondary)); /* Ensure background */
padding-top: 5.5rem; /* Space for header */
}
.sidebar.active { transform: translateX(0); }
.main-content { margin-left: 0; padding: 2rem 1.5rem; margin-top: 4.5rem; /* Space for header */ }
.mobile-menu-toggle { display: flex; }
.docs-container { margin-top: 0; margin-bottom: 0; min-height: 100vh; } /* Adjust container */
}
</style>
<script>
// Docs Page Specific JS
// Mobile Sidebar Toggle
const mobileMenuBtn = document.getElementById('mobile-menu-btn');
const sidebar = document.getElementById('docs-sidebar');
const mainContent = document.getElementById('main-docs-content'); // Target main content
if (mobileMenuBtn && sidebar) {
mobileMenuBtn.addEventListener('click', () => {
sidebar.classList.toggle('active');
// Optional: Add overlay or dim main content when sidebar is open
if (sidebar.classList.contains('active')) {
mobileMenuBtn.innerHTML = '<i class="fas fa-times"></i>'; // Change icon to X
// Add overlay logic if desired
} else {
mobileMenuBtn.innerHTML = '<i class="fas fa-bars"></i>'; // Change icon back to bars
// Remove overlay logic if desired
}
});
// Close sidebar when clicking outside of it (on main content)
mainContent?.addEventListener('click', () => {
if (sidebar.classList.contains('active')) {
sidebar.classList.remove('active');
mobileMenuBtn.innerHTML = '<i class="fas fa-bars"></i>';
}
});
// Close sidebar when a link is clicked
sidebar.querySelectorAll('.sidebar-nav-link').forEach(link => {
link.addEventListener('click', () => {
if (sidebar.classList.contains('active')) {
sidebar.classList.remove('active');
mobileMenuBtn.innerHTML = '<i class="fas fa-bars"></i>';
}
});
});
}
// Active Link Highlighting (Scroll Spy)
const sections = document.querySelectorAll('.docs-section');
const navLinks = document.querySelectorAll('.sidebar-nav-link');
function activateLink() {
let currentSectionId = '';
const scrollPosition = window.scrollY;
sections.forEach(section => {
const sectionTop = section.offsetTop - 150; // Adjust offset as needed
const sectionHeight = section.offsetHeight;
const sectionId = section.getAttribute('id');
if (scrollPosition >= sectionTop && scrollPosition < sectionTop + sectionHeight) {
currentSectionId = sectionId;
}
});
// Handle reaching the bottom of the page
if ((window.innerHeight + window.scrollY) >= document.body.offsetHeight - 50) {
const lastSection = sections[sections.length - 1];
if (lastSection) {
currentSectionId = lastSection.getAttribute('id');
}
}
navLinks.forEach(link => {
link.classList.remove('active');
if (link.getAttribute('href') === `#${currentSectionId}`) {
link.classList.add('active');
}
});
// Ensure first link is active if scrolled to top
if (window.scrollY < 100 && navLinks.length > 0) {
navLinks.forEach(link => link.classList.remove('active'));
navLinks[0].classList.add('active');
}
}
window.addEventListener('scroll', activateLink);
// Initial check on load
document.addEventListener('DOMContentLoaded', activateLink);
</script>

View File

@ -0,0 +1,323 @@
---
// src/pages/ansible/help.astro - Converted from static ansible-help.html
import BaseLayout from '../../layouts/BaseLayout.astro';
import Header from '../../components/Header.astro';
import Footer from '../../components/Footer.astro';
const title = "Ansible Sandbox Help | Argobox";
const description = "Help guide for using the Ansible Sandbox environment. Learn how to deploy infrastructure as code demonstrations.";
---
<BaseLayout {title} {description}>
{/* Add Font Awesome if not loaded globally */}
{/* <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/css/all.min.css" integrity="sha512-z3gLpd7yknf1YoNbCzqRKc4qyor8gaKU1qmn+CShxbuBusANI9QpRohGBreCFkKxLhei6S9CQXFEbbKuqLg0DA==" crossorigin="anonymous" referrerpolicy="no-referrer" /> */}
<Header slot="header" />
<main class="main-content">
<div class="container">
<header class="help-header">
<a href="/ansible/sandbox" class="back-link"> {/* Updated Link */}
<i class="fas fa-arrow-left"></i>
Back to Ansible Sandbox
</a>
<h1 class="help-title">Ansible Sandbox Help Guide</h1>
<p class="help-subtitle">
Learn how to use and get the most out of the Ansible Sandbox environment.
</p>
</header>
<section class="help-section">
<h2 class="help-section-title">Getting Started</h2>
<div class="step-grid">
<div class="step-number">1</div>
<div class="step-title">Select a Playbook</div>
<div class="step-content">
<p>The sandbox offers various Ansible playbooks showcasing different infrastructure automation scenarios. Choose one from the left panel based on your interests.</p>
<p>Each playbook comes with a description and information about its complexity level and typical run time.</p>
</div>
{/* Optional: Add placeholder image if available <div class="step-image"><img src="/images/help/step1.png" alt="Select Playbook Step"></div> */}
</div>
<div class="step-grid">
<div class="step-number">2</div>
<div class="step-title">Explore the Playbook Code</div>
<div class="step-content">
<p>View the Ansible YAML code to understand how the automation works. The code is syntax-highlighted for readability.</p>
<p>Hover over different sections to see what each part of the playbook does.</p>
</div>
{/* Optional: Add placeholder image <div class="step-image"><img src="/images/help/step2.png" alt="Explore Code Step"></div> */}
</div>
<div class="step-grid">
<div class="step-number">3</div>
<div class="step-title">Configure Parameters</div>
<div class="step-content">
<p>Switch to the "Configuration" tab to customize various parameters for your deployment. These might include:</p>
<ul class="config-list">
<li>Domain names</li>
<li>Directory paths</li>
<li>Feature toggles</li>
<li>VM template selection</li>
<li>Resource allocation</li>
</ul>
</div>
</div>
<div class="step-grid">
<div class="step-number">4</div>
<div class="step-title">Deploy the Environment</div>
<div class="step-content">
<p>Click the "Deploy" button to launch the automation process. The system will:</p>
<ol class="deploy-steps-list">
<li>Create necessary virtual machines</li>
<li>Run the selected Ansible playbook</li>
<li>Configure all services</li>
<li>Provide access to the deployed environment</li>
</ol>
</div>
</div>
<div class="step-grid">
<div class="step-number">5</div>
<div class="step-title">Monitor Progress</div>
<div class="step-content">
<p>Watch the deployment process in real-time on the "Output" tab, which shows the Ansible execution log.</p>
<p>Once deployment completes, you'll see a success message with details on how to access the deployed environment.</p>
</div>
</div>
<div class="step-grid">
<div class="step-number">6</div>
<div class="step-title">Explore the Environment</div>
<div class="step-content">
<p>After deployment, you can:</p>
<ul class="explore-list">
<li>View the VM status and details in the "VM Status" tab</li>
<li>Access deployed applications via provided URLs</li>
<li>See resource utilization metrics</li>
<li>Monitor the environment's remaining active time</li>
</ul>
</div>
</div>
<div class="tip">
<div class="tip-title">
<i class="fas fa-lightbulb"></i>
<span>Pro Tip</span>
</div>
<p>
Sandbox environments automatically shut down after 30 minutes to conserve resources. You'll see a countdown timer showing the remaining time. Make sure to complete your exploration before time runs out!
</p>
</div>
</section>
<section class="help-section">
<h2 class="help-section-title">Frequently Asked Questions</h2>
<div class="faq-item">
<div class="faq-question">What is an Ansible playbook?</div>
<div class="faq-answer">
An Ansible playbook is a YAML file that describes a set of tasks to be executed on remote servers. Playbooks define the desired state of systems and can configure applications, deploy software, and orchestrate advanced IT workflows.
</div>
</div>
<div class="faq-item">
<div class="faq-question">Is the sandbox environment isolated?</div>
<div class="faq-answer">
Yes, each sandbox environment is completely isolated. Your deployments and configurations won't affect other users or any production systems. This provides a safe space to experiment with infrastructure automation.
</div>
</div>
<div class="faq-item">
<div class="faq-question">Can I save or download the playbooks?</div>
<div class="faq-answer">
Yes, you can copy the playbook code to use in your own environments. Each code segment can be selected and copied to your clipboard. For a more organized approach, visit the <a href="/ansible/docs">Documentation page</a> for downloadable versions of all playbooks. {/* Updated Link */}
</div>
</div>
<div class="faq-item">
<div class="faq-question">What happens if I need more time with an environment?</div>
<div class="faq-answer">
Currently, all sandbox environments are limited to 30 minutes. If you need more time, you can always redeploy the environment after it expires, which will give you a fresh 30-minute window to continue your exploration.
</div>
</div>
<div class="faq-item">
<div class="faq-question">Can I modify the playbooks?</div>
<div class="faq-answer">
The playbook code displayed is read-only to ensure consistent and reliable deployments. However, you can customize many aspects of the deployment through the Configuration tab, which allows you to adjust key parameters without modifying the underlying code.
</div>
</div>
<div class="faq-item">
<div class="faq-question">What if I encounter an error during deployment?</div>
<div class="faq-answer">
If an error occurs during deployment, the Output tab will display the specific error message from Ansible. You can use the "Reset" button to clear the environment and try again, potentially with different configuration options.
</div>
</div>
</section>
<section class="help-section">
<h2 class="help-section-title">Key Terms</h2>
<div class="key-terms-list">
<p><span class="key-term">Playbook</span> - A YAML file defining a series of Ansible tasks and configurations.</p>
<p><span class="key-term">Task</span> - An individual action Ansible executes on a managed host.</p>
<p><span class="key-term">Role</span> - A reusable unit of tasks, variables, files, etc.</p>
<p><span class="key-term">Inventory</span> - A list of hosts managed by Ansible.</p>
<p><span class="key-term">Handler</span> - A special task triggered by a change notification.</p>
<p><span class="key-term">Variable</span> - A value used to make playbooks flexible.</p>
<p><span class="key-term">Template</span> - A file using variables to create dynamic configuration files (e.g., Jinja2).</p>
</div>
</section>
<footer class="help-footer">
<p>Need more assistance? <a href="mailto:daniel.laforce@argobox.com">Contact Support</a> or visit the <a href="/ansible/docs">Full Documentation</a>.</p> {/* Updated Link */}
</footer>
</div>
</main>
<Footer slot="footer" />
</BaseLayout>
<style is:global>
/* Styles adapted from ansible-help.html and styles.css */
/* Rely on BaseLayout for body, container, etc. */
.main-content {
/* Adjust top margin if header height changes */
/* margin-top: 4.5rem; */
padding-top: 2rem; /* Add padding */
padding-bottom: 4rem;
}
.help-header {
text-align: center;
margin-bottom: 3rem;
position: relative;
padding-top: 1rem; /* Add space from header */
}
.help-title {
font-size: clamp(1.8rem, 5vw, 2.5rem); /* Responsive title */
font-weight: 700;
margin-bottom: 1rem;
color: var(--text-primary);
}
.help-subtitle {
color: var(--text-secondary);
font-size: clamp(1rem, 3vw, 1.2rem);
max-width: 700px;
margin: 0 auto;
}
.back-link {
position: absolute;
top: 0;
left: 0;
display: inline-flex; /* Use inline-flex */
align-items: center;
gap: 0.5rem;
color: var(--accent);
font-weight: 500;
transition: all var(--transition-normal);
text-decoration: none;
font-size: 0.9rem;
padding: 0.5rem 0;
}
.back-link:hover { color: var(--accent-darker); transform: translateX(-3px); }
.help-section {
background-color: var(--card-bg);
border: 1px solid var(--border);
border-radius: 1rem;
padding: clamp(1.5rem, 4vw, 2rem); /* Responsive padding */
margin-bottom: 2rem;
}
.help-section-title {
font-size: clamp(1.25rem, 4vw, 1.5rem);
font-weight: 600;
margin-bottom: 1.5rem;
color: var(--accent);
position: relative;
padding-bottom: 0.75rem;
border-bottom: 1px solid var(--border); /* Use border instead of ::after */
}
/* .help-section-title::after { content: ''; position: absolute; bottom: 0; left: 0; width: 50px; height: 3px; background: var(--accent-gradient); border-radius: 3px; } */
.step-grid {
display: grid;
grid-template-columns: auto 1fr;
gap: 1rem 1rem; /* Reduced gap */
margin-bottom: 1.5rem;
align-items: start; /* Align items to start */
}
.step-number {
background: var(--accent-gradient);
color: white;
width: 32px;
height: 32px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-weight: 600;
font-size: 0.9rem;
grid-row: 1 / span 2; /* Span number across title/content */
margin-top: 0.1rem; /* Align better with title */
}
.step-title {
font-weight: 600;
font-size: 1.1rem;
/* align-self: center; */ /* Removed */
color: var(--text-primary);
grid-column: 2;
grid-row: 1;
margin: 0; /* Remove default margins */
}
.step-content {
grid-column: 2;
grid-row: 2;
color: var(--text-secondary);
font-size: 0.95rem;
}
.step-content p { margin-bottom: 1rem; }
.step-content p:last-child { margin-bottom: 0; }
.step-content ul, .step-content ol {
list-style-position: outside;
margin-left: 1.5rem;
margin-top: 0.5rem;
margin-bottom: 1rem;
}
.step-content li { margin-bottom: 0.5rem; }
.tip {
background-color: rgba(59, 130, 246, 0.1);
border-left: 4px solid var(--accent);
padding: 1rem 1.5rem;
border-radius: 0 0.5rem 0.5rem 0;
margin: 1.5rem 0;
font-size: 0.95rem;
}
.tip-title { font-weight: 600; color: var(--accent); display: flex; align-items: center; gap: 0.5rem; margin-bottom: 0.5rem; }
.tip p { margin: 0; color: var(--text-secondary); }
.faq-item { margin-bottom: 1.5rem; }
.faq-question { font-weight: 600; margin-bottom: 0.5rem; color: var(--text-primary); }
.faq-answer { color: var(--text-secondary); font-size: 0.95rem; }
.key-terms-list p { margin-bottom: 0.75rem; }
.key-term { font-weight: 500; color: var(--accent); font-family: var(--font-mono); background-color: rgba(59, 130, 246, 0.1); padding: 0.1em 0.3em; border-radius: 3px; }
.help-footer { text-align: center; margin-top: 3rem; color: var(--text-secondary); font-size: 0.9rem; }
.help-footer a { color: var(--accent); text-decoration: none; }
.help-footer a:hover { text-decoration: underline; }
@media (max-width: 768px) {
.step-grid { grid-template-columns: 1fr; gap: 0.5rem; }
.step-number { grid-row: auto; margin-bottom: 0.5rem; justify-self: start; }
.step-title { grid-column: 1; grid-row: auto; }
.step-content { grid-column: 1; grid-row: auto; }
.back-link { position: static; margin-bottom: 1rem; }
.help-header { padding-top: 0; }
}
</style>

View File

@ -0,0 +1,625 @@
---
// src/pages/ansible/sandbox.astro - Converted from static ansible-sandbox.html
import BaseLayout from '../../layouts/BaseLayout.astro';
import Header from '../../components/Header.astro';
import Footer from '../../components/Footer.astro';
const title = "Ansible Sandbox | Argobox";
const description = "Deploy interactive visualizations with ArgoBox Ansible Sandbox - the easiest way to deploy web animations and showcase your creative work.";
// Template data (can be externalized later)
const templates = [
{ id: "fireworks", name: "Fireworks", description: "Interactive fireworks animation with click-to-launch effects", icon: "fas fa-fire", tags: ["Popular"], complexity: "Basic" },
{ id: "matrix", name: "Matrix Rain", description: "Digital rain effect inspired by The Matrix movie", icon: "fas fa-code", tags: [], complexity: "Intermediate" },
{ id: "starfield", name: "Starfield", description: "3D space journey through a field of stars", icon: "fas fa-star", tags: [], complexity: "Basic" },
{ id: "particles", name: "Particles", description: "Interactive particle system that responds to mouse movement", icon: "fas fa-atom", tags: [], complexity: "Intermediate" },
{ id: "3d-globe", name: "3D Globe", description: "Interactive 3D Earth globe with custom markers", icon: "fas fa-globe-americas", tags: ["New"], complexity: "Advanced" }
];
---
<BaseLayout {title} {description}>
{/* Add Font Awesome if not loaded globally */}
{/* <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/css/all.min.css" integrity="sha512-z3gLpd7yknf1YoNbCzqRKc4qyor8gaKU1qmn+CShxbuBusANI9QpRohGBreCFkKxLhei6S9CQXFEbbKuqLg0DA==" crossorigin="anonymous" referrerpolicy="no-referrer" /> */}
<Header slot="header" />
<main class="container sandbox-page-container"> {/* Use specific container class */}
<!-- Sandbox Container -->
<div class="sandbox-container card"> {/* Re-use card style for main container */}
<!-- Simulation Toggle -->
<div class="simulation-toggle">
<span class="toggle-label">Simulation Mode</span>
<label class="toggle-switch">
<input type="checkbox" id="simulation-toggle" checked>
<span class="toggle-slider"></span>
</label>
<span class="toggle-status">Active</span>
</div>
<!-- Offline Notice -->
<div class="offline-notice">
<div class="offline-notice-icon">
<i class="fas fa-exclamation-triangle"></i>
</div>
<div class="offline-notice-text">
<h3>Ansible Sandbox is Currently Offline</h3>
<p>The Ansible Sandbox environment is currently in simulation mode. You can explore the interface, but actual deployments are not available at this time. We're working to bring the full functionality online soon.</p>
</div>
</div>
<!-- Sandbox Header -->
<div class="sandbox-header">
<div class="sandbox-title">
<h1>Template Deployment</h1>
</div>
<div class="sandbox-actions">
<a href="/ansible/help" class="sandbox-btn"> {/* Updated Link */}
<i class="fas fa-question-circle"></i>
<span>Help</span>
</a>
<a href="/ansible/docs" class="sandbox-btn"> {/* Updated Link */}
<i class="fas fa-book"></i>
<span>Documentation</span>
</a>
</div>
</div>
<!-- Template Selection Section -->
<div class="templates-section">
<h2 class="section-title">Select a Template to Deploy</h2>
<p class="section-description">Choose from our collection of interactive visualizations to deploy with Ansible automation.</p>
<div class="templates-grid">
{templates.map(template => (
<div class="template-card" data-template={template.id}>
<div class="template-preview">
<i class={template.icon}></i>
</div>
<div class="template-content">
<h3 class="template-name">{template.name}</h3>
<p class="template-description">{template.description}</p>
<div class="template-meta">
{template.tags.map(tag => <span class="template-tag">{tag}</span>)}
<span class="template-complexity">{template.complexity}</span>
</div>
</div>
</div>
))}
</div>
<div class="deployment-actions">
<button id="preview-btn" class="sandbox-btn" disabled>
<i class="fas fa-eye"></i>
<span>Preview Template</span>
</button>
<button id="deploy-btn" class="sandbox-btn btn-primary" disabled>
<i class="fas fa-rocket"></i>
<span>Deploy Now</span>
</button>
</div>
</div>
<!-- Deployment Status Section (Initially Hidden) -->
<div id="deployment-section" class="deployment-section" style="display: none;">
<h2 class="section-title">Deployment Progress</h2>
<div class="status-container">
<div class="progress-bar-container">
<div class="progress-bar">
<div class="progress-value" id="deployment-progress"></div>
</div>
<div class="progress-label" id="progress-text">Initializing deployment...</div>
</div>
<div class="deployment-steps">
<div class="deployment-step pending" id="step-init">
<div class="step-icon">1</div>
<div class="step-content">
<div class="step-title">Initialization</div>
<div class="step-description">Setting up deployment environment</div>
</div>
</div>
<div class="deployment-step pending" id="step-template">
<div class="step-icon">2</div>
<div class="step-content">
<div class="step-title">Template Preparation</div>
<div class="step-description">Configuring selected template</div>
</div>
</div>
<div class="deployment-step pending" id="step-deploy">
<div class="step-icon">3</div>
<div class="step-content">
<div class="step-title">Deployment</div>
<div class="step-description">Executing Ansible deployment</div>
</div>
</div>
<div class="deployment-step pending" id="step-publish">
<div class="step-icon">4</div>
<div class="step-content">
<div class="step-title">Publishing</div>
<div class="step-description">Making your site live</div> {/* Simplified */}
</div>
</div>
</div>
<div class="deployment-details">
<div class="deployment-info" id="deployment-info">
<p>Deployment information will appear here once the process begins.</p>
</div>
</div>
</div>
</div>
<!-- Deployment Result Section (Initially Hidden) -->
<div id="result-section" class="result-section" style="display: none;">
<div class="success-card">
<div class="success-icon">
<i class="fas fa-check-circle"></i>
</div>
<h2>Deployment Successful!</h2>
<p>Your template has been deployed and is now live.</p>
<div class="deployment-url-container">
<p>You can access your deployment at:</p>
<a href="#" class="deployment-url" id="deployment-url" target="_blank" rel="noopener noreferrer">
<i class="fas fa-external-link-alt"></i>
<span id="deployment-url-text">https://u12345.argobox.com</span>
</a>
</div>
<div class="result-actions">
<button id="view-deployment-btn" class="sandbox-btn btn-primary">
<i class="fas fa-eye"></i>
<span>View Deployment</span>
</button>
<button id="new-deployment-btn" class="sandbox-btn">
<i class="fas fa-plus"></i>
<span>New Deployment</span>
</button>
</div>
</div>
</div>
<!-- Sandbox Footer -->
<div class="sandbox-footer">
<a href="/" class="back-to-site"> {/* Updated Link */}
<i class="fas fa-arrow-left"></i>
<span>Back to Main Site</span>
</a>
<div class="footer-info">Template deployments automatically expire after 30 minutes.</div> {/* Adjusted expiry */}
</div>
</div> {/* End sandbox-container */}
</main>
<Footer slot="footer" />
</BaseLayout>
<style is:global>
/* Styles adapted from ansible-sandbox.html and styles.css */
/* Use theme variables where possible */
.sandbox-page-container {
padding-top: 2rem; /* Add padding above main card */
padding-bottom: 4rem;
}
.card { /* Style for the main sandbox container */
background-color: var(--card-bg);
border-radius: 0.75rem; /* Slightly larger radius */
box-shadow: var(--card-shadow);
padding: clamp(1.5rem, 5vw, 2.5rem); /* Responsive padding */
margin-bottom: 2rem;
border: 1px solid var(--border);
}
/* Simulation Toggle Styles */
.simulation-toggle { display: flex; align-items: center; gap: 0.75rem; margin-bottom: 1.5rem; background-color: var(--bg-secondary); padding: 0.75rem 1rem; border-radius: 0.375rem; border: 1px solid var(--border); width: fit-content; }
.toggle-label { font-size: 0.875rem; font-weight: 500; color: var(--text-secondary); }
.toggle-switch { position: relative; display: inline-block; width: 40px; height: 20px; }
.toggle-switch input { opacity: 0; width: 0; height: 0; }
.toggle-slider { position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0; background-color: var(--text-secondary); transition: .4s; border-radius: 20px; }
.toggle-slider:before { position: absolute; content: ""; height: 16px; width: 16px; left: 2px; bottom: 2px; background-color: white; transition: .4s; border-radius: 50%; }
input:checked + .toggle-slider { background-color: var(--accent); }
input:checked + .toggle-slider:before { transform: translateX(20px); }
.toggle-status { font-size: 0.875rem; font-weight: 500; color: var(--text-primary); }
/* Offline Notice Styles */
.offline-notice { display: flex; align-items: center; background-color: rgba(245, 158, 11, 0.1); border: 1px solid rgba(245, 158, 11, 0.3); border-left: 4px solid var(--warning); padding: 1rem 1.5rem; border-radius: 0.5rem; margin-bottom: 2rem; }
.offline-notice-icon { font-size: 1.5rem; color: var(--warning); margin-right: 1rem; }
.offline-notice-text h3 { font-size: 1.1rem; font-weight: 600; color: var(--text-primary); margin-bottom: 0.25rem; }
.offline-notice-text p { font-size: 0.9rem; color: var(--text-secondary); margin: 0; }
/* Sandbox Header Styles */
.sandbox-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 2rem; padding-bottom: 1rem; border-bottom: 1px solid var(--border); flex-wrap: wrap; gap: 1rem; /* Allow wrapping */ }
.sandbox-title h1 { font-size: clamp(1.5rem, 4vw, 1.8rem); margin: 0; color: var(--text-primary); }
.sandbox-actions { display: flex; gap: 0.75rem; flex-wrap: wrap; }
.sandbox-btn { display: inline-flex; align-items: center; gap: 0.5rem; padding: 0.5rem 1rem; background-color: var(--bg-secondary); color: var(--text-secondary); border: 1px solid var(--border); border-radius: 0.375rem; text-decoration: none; font-size: 0.875rem; font-weight: 500; transition: all var(--transition-normal); cursor: pointer; }
.sandbox-btn:hover { background-color: var(--bg-tertiary); color: var(--text-primary); border-color: var(--text-secondary); }
.sandbox-btn.btn-primary { background-color: var(--accent); color: white; border-color: var(--accent); }
.sandbox-btn.btn-primary:hover { background-color: var(--accent-darker); border-color: var(--accent-darker); }
.sandbox-btn:disabled { opacity: 0.6; cursor: not-allowed; background-color: var(--bg-secondary); color: var(--text-secondary); border-color: var(--border); }
.sandbox-btn.btn-primary:disabled { background-color: var(--accent); opacity: 0.5; border-color: var(--accent); }
/* Template Deployment Styles */
.section-title { font-size: 1.5rem; font-weight: 600; margin-bottom: 0.5rem; color: var(--text-primary); }
.section-description { font-size: 0.95rem; color: var(--text-secondary); margin-bottom: 2rem; }
.templates-section { margin-bottom: 3rem; }
.templates-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); gap: 1.5rem; margin-bottom: 2rem; } /* Slightly smaller minmax */
.template-card { background-color: var(--card-bg); border: 1px solid var(--border); border-radius: 0.75rem; overflow: hidden; cursor: pointer; transition: all var(--transition-normal); display: flex; flex-direction: column; }
.template-card:hover { transform: translateY(-5px); box-shadow: var(--card-shadow); border-color: var(--accent); }
.template-card.selected { border: 2px solid var(--accent); box-shadow: 0 0 0 1px var(--accent), var(--card-shadow); background-color: rgba(59, 130, 246, 0.05); }
.template-preview { height: 140px; background-color: var(--bg-secondary); display: flex; align-items: center; justify-content: center; font-size: 3rem; color: var(--accent); overflow: hidden; position: relative; border-bottom: 1px solid var(--border); }
.template-preview img { width: 100%; height: 100%; object-fit: cover; }
.template-content { padding: 1.25rem; flex: 1; display: flex; flex-direction: column; }
.template-name { font-size: 1.2rem; font-weight: 600; margin-bottom: 0.5rem; color: var(--text-primary); }
.template-description { font-size: 0.9rem; color: var(--text-secondary); margin-bottom: 1rem; flex: 1; }
.template-meta { display: flex; align-items: center; gap: 0.75rem; font-size: 0.75rem; margin-top: auto; } /* Push meta to bottom */
.template-tag { background-color: rgba(59, 130, 246, 0.1); color: var(--accent); padding: 0.25rem 0.75rem; border-radius: 9999px; font-weight: 500; }
.template-complexity { color: var(--text-secondary); font-weight: 500; }
.deployment-actions { display: flex; justify-content: flex-end; gap: 1rem; margin-top: 1rem; flex-wrap: wrap; }
/* Deployment Status Styles */
.deployment-section { margin-bottom: 3rem; }
.status-container { background-color: var(--card-bg); border: 1px solid var(--border); border-radius: 0.75rem; padding: 1.5rem; }
.progress-bar-container { margin-bottom: 2rem; }
.progress-bar { height: 0.5rem; background-color: var(--bg-tertiary); border-radius: 9999px; overflow: hidden; margin-bottom: 1rem; }
.progress-value { height: 100%; background-color: var(--accent); border-radius: 9999px; width: 0%; transition: width 0.5s ease; }
.progress-label { font-size: 0.9rem; color: var(--text-secondary); text-align: center; }
.deployment-steps { display: flex; flex-direction: column; gap: 1rem; margin-bottom: 2rem; }
.deployment-step { display: flex; align-items: flex-start; padding: 1rem; background-color: var(--bg-secondary); border-radius: 0.5rem; border-left: 4px solid var(--border); transition: all var(--transition-normal); }
.deployment-step.pending { border-left-color: var(--border); }
.deployment-step.in-progress { border-left-color: var(--accent); background-color: rgba(59, 130, 246, 0.05); }
.deployment-step.completed { border-left-color: var(--success); background-color: rgba(16, 185, 129, 0.05); }
.deployment-step.failed { border-left-color: var(--error); background-color: rgba(239, 68, 68, 0.05); }
.step-icon { width: 2rem; height: 2rem; border-radius: 50%; background-color: var(--bg-tertiary); display: flex; align-items: center; justify-content: center; font-weight: 600; font-size: 0.9rem; margin-right: 1rem; flex-shrink: 0; transition: all var(--transition-normal); color: var(--text-secondary); }
.pending .step-icon { background-color: var(--bg-tertiary); }
.in-progress .step-icon { background-color: var(--accent); color: white; }
.completed .step-icon { background-color: var(--success); color: white; }
.failed .step-icon { background-color: var(--error); color: white; }
.step-content { flex: 1; }
.step-title { font-weight: 600; font-size: 1rem; margin-bottom: 0.25rem; color: var(--text-primary); }
.step-description { font-size: 0.85rem; color: var(--text-secondary); }
.deployment-details { background-color: var(--bg-secondary); border-radius: 0.5rem; padding: 1.25rem; border: 1px solid var(--border); }
.deployment-info { font-size: 0.9rem; color: var(--text-secondary); }
.deployment-info dl { display: grid; grid-template-columns: auto 1fr; gap: 0.5rem 1rem; margin: 0; }
.deployment-info dt { font-weight: 600; color: var(--text-primary); grid-column: 1; text-align: right; }
.deployment-info dd { margin: 0; color: var(--text-secondary); grid-column: 2; word-break: break-all; }
.deployment-info .status-badge { display: inline-block; padding: 0.25rem 0.5rem; border-radius: 0.25rem; font-size: 0.75rem; font-weight: 600; background-color: var(--bg-tertiary); }
.deployment-info .status-initializing, .deployment-info .status-unknown { color: var(--info); }
.deployment-info .status-deploying, .deployment-info .status-publishing { color: var(--warning); }
.deployment-info .status-completed { color: var(--success); }
.deployment-info .status-failed { color: var(--error); }
.deployment-info dd a { color: var(--accent); text-decoration: none; }
.deployment-info dd a:hover { text-decoration: underline; }
/* Deployment Result Styles */
.result-section { margin-bottom: 3rem; }
.success-card { background-color: var(--card-bg); border: 1px solid var(--border); border-radius: 0.75rem; padding: 2.5rem 1.5rem; text-align: center; }
.success-icon { font-size: 4rem; color: var(--success); margin-bottom: 1.5rem; }
.success-card h2 { font-size: 1.75rem; font-weight: 600; margin-bottom: 0.75rem; color: var(--text-primary); }
.success-card p { font-size: 1rem; color: var(--text-secondary); margin-bottom: 2rem; }
.deployment-url-container { margin-bottom: 2rem; }
.deployment-url-container p { margin-bottom: 0.5rem; font-size: 0.9rem; color: var(--text-secondary); }
.deployment-url { display: inline-flex; align-items: center; gap: 0.75rem; padding: 0.75rem 1.5rem; background-color: var(--bg-secondary); border: 1px solid var(--border); border-radius: 0.5rem; color: var(--accent); font-size: 1.1rem; font-weight: 500; text-decoration: none; transition: all var(--transition-normal); }
.deployment-url:hover { background-color: var(--bg-tertiary); transform: translateY(-2px); border-color: var(--accent); }
.result-actions { display: flex; justify-content: center; gap: 1rem; flex-wrap: wrap; }
/* Sandbox Footer Styles */
.sandbox-footer { display: flex; justify-content: space-between; align-items: center; margin-top: 3rem; padding-top: 1.5rem; border-top: 1px solid var(--border); font-size: 0.85rem; color: var(--text-secondary); flex-wrap: wrap; gap: 1rem; }
.back-to-site { display: inline-flex; align-items: center; gap: 0.5rem; color: var(--text-secondary); text-decoration: none; transition: color var(--transition-normal); }
.back-to-site:hover { color: var(--accent); }
/* Responsive */
@media (max-width: 768px) {
.sandbox-header { flex-direction: column; align-items: flex-start; }
.sandbox-actions { width: 100%; }
.templates-grid { grid-template-columns: 1fr; }
.deployment-actions { flex-direction: column; }
.deployment-actions button { width: 100%; }
.result-actions { flex-direction: column; }
.result-actions button { width: 100%; }
.sandbox-footer { flex-direction: column; text-align: center; }
.deployment-info dl { grid-template-columns: 1fr; }
.deployment-info dt { text-align: left; margin-bottom: 0.1rem; }
.deployment-info dd { margin-bottom: 0.5rem; }
}
</style>
<script is:inline>
// Adapted Sandbox JS from ansible-sandbox.html
// Needs to run inline because it manipulates the DOM of this specific page
document.addEventListener('DOMContentLoaded', function() {
// Configuration
// Use Astro environment variables (import.meta.env)
// Make sure these are prefixed with PUBLIC_ if they need to be client-accessible
const API_BASE_URL = import.meta.env.PUBLIC_ANSIBLE_SANDBOX_API_URL || 'https://ansible-sandbox.fly.dev';
let SIMULATION_MODE = true; // Default to true, controlled by toggle
const BASE_DOMAIN = import.meta.env.PUBLIC_ANSIBLE_SANDBOX_DOMAIN || 'argobox.com';
// Elements
const templateCards = document.querySelectorAll('.template-card');
const deployBtn = document.getElementById('deploy-btn');
const previewBtn = document.getElementById('preview-btn');
const deploymentSection = document.getElementById('deployment-section');
const resultSection = document.getElementById('result-section');
const deploymentProgress = document.getElementById('deployment-progress');
const progressText = document.getElementById('progress-text');
const deploymentInfo = document.getElementById('deployment-info');
const deploymentUrl = document.getElementById('deployment-url');
const deploymentUrlText = document.getElementById('deployment-url-text');
const viewDeploymentBtn = document.getElementById('view-deployment-btn');
const newDeploymentBtn = document.getElementById('new-deployment-btn');
const simulationToggle = document.getElementById('simulation-toggle');
const toggleStatus = document.querySelector('.toggle-status');
const templatesSection = document.querySelector('.templates-section');
// State
let selectedTemplate = null;
let deploymentId = null;
let deploymentStatus = 'pending';
let pollingIntervalId = null;
// Initialize UI
function initUI() {
templateCards.forEach(card => card.addEventListener('click', () => selectTemplate(card)));
if (deployBtn) deployBtn.addEventListener('click', startDeployment);
if (previewBtn) previewBtn.addEventListener('click', previewTemplate);
if (viewDeploymentBtn) viewDeploymentBtn.addEventListener('click', () => {
if (deploymentUrl && deploymentUrl.href && deploymentUrl.href !== '#') {
window.open(deploymentUrl.href, '_blank');
}
});
if (newDeploymentBtn) newDeploymentBtn.addEventListener('click', resetDeployment);
if (simulationToggle) {
SIMULATION_MODE = simulationToggle.checked;
updateToggleStatus();
simulationToggle.addEventListener('change', () => {
SIMULATION_MODE = simulationToggle.checked;
updateToggleStatus();
console.log(`Simulation mode: ${SIMULATION_MODE}`);
});
}
// Check URL parameters
const urlParams = new URLSearchParams(window.location.search);
const templateParam = urlParams.get('template');
if (templateParam) {
const matchingCard = Array.from(templateCards).find(card => card.dataset.template === templateParam);
if (matchingCard) {
selectTemplate(matchingCard);
if (urlParams.get('deploy') === 'true') {
setTimeout(startDeployment, 500);
}
}
}
}
function updateToggleStatus() {
if (toggleStatus) {
toggleStatus.textContent = SIMULATION_MODE ? 'Active' : 'Inactive';
}
}
function selectTemplate(card) {
templateCards.forEach(c => c.classList.remove('selected'));
card.classList.add('selected');
selectedTemplate = card.dataset.template;
if (deployBtn) deployBtn.disabled = false;
if (previewBtn) previewBtn.disabled = false;
}
function previewTemplate() {
if (!selectedTemplate) return;
alert(`Previewing ${selectedTemplate}... (Replace with actual preview logic)`);
}
function startDeployment() {
if (!selectedTemplate) return;
if (templatesSection) templatesSection.style.display = 'none';
if (deploymentSection) deploymentSection.style.display = 'block';
if (resultSection) resultSection.style.display = 'none';
resetProgressSteps();
updateProgress(5, 'Initializing deployment...');
updateStepStatus('step-init', 'in-progress');
if (deploymentInfo) deploymentInfo.innerHTML = '<p>Preparing deployment environment...</p>';
if (deployBtn) deployBtn.disabled = true;
if (previewBtn) previewBtn.disabled = true;
if (SIMULATION_MODE) {
simulateDeployment();
} else {
makeDeploymentRequest();
}
}
function makeDeploymentRequest() {
const payload = { template_name: selectedTemplate };
fetch(`${API_BASE_URL}/deploy`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
})
.then(response => {
if (!response.ok) {
return response.json().then(err => { throw new Error(err.detail || `Deployment failed: ${response.statusText}`); })
.catch(() => { throw new Error(`Deployment failed: ${response.statusText}`); });
}
return response.json();
})
.then(data => {
deploymentId = data.deployment_id;
console.log('Deployment initiated:', deploymentId);
const initialStatus = { deployment_id: deploymentId, template: selectedTemplate, status: 'initializing', timestamp: new Date().toISOString(), ...data };
updateDeploymentStatus(initialStatus);
pollDeploymentStatus();
})
.catch(error => {
console.error('Deployment error:', error);
updateProgress(0, `Error: ${error.message}`);
updateStepStatus('step-init', 'failed');
if (deploymentInfo) deploymentInfo.innerHTML = `<p style="color: var(--error);"><strong>Error:</strong> ${error.message}</p>`;
if (deployBtn) deployBtn.disabled = false;
if (previewBtn) previewBtn.disabled = false;
});
}
function pollDeploymentStatus() {
if (!deploymentId) return;
if (pollingIntervalId) clearInterval(pollingIntervalId);
pollingIntervalId = setInterval(() => {
fetch(`${API_BASE_URL}/status/${deploymentId}`)
.then(response => {
if (!response.ok) {
if (response.status === 404) { console.warn(`Deployment ${deploymentId} not found yet.`); return null; }
throw new Error(`Status check failed: ${response.statusText}`);
}
return response.json();
})
.then(data => {
if (data === null) return;
updateDeploymentStatus(data);
if (data.status === 'completed' || data.status === 'failed') {
clearInterval(pollingIntervalId);
pollingIntervalId = null;
if (deployBtn) deployBtn.disabled = false;
if (previewBtn) previewBtn.disabled = false;
}
})
.catch(error => {
console.error('Status check error:', error);
clearInterval(pollingIntervalId);
pollingIntervalId = null;
if (deploymentInfo) deploymentInfo.innerHTML += `<p style="color: var(--error);">Status polling failed: ${error.message}</p>`;
if (deployBtn) deployBtn.disabled = false;
if (previewBtn) previewBtn.disabled = false;
});
}, 3000);
}
function updateDeploymentStatus(data) {
deploymentStatus = data.status;
let progressPercent = 0;
let progressMessage = '';
resetProgressStepsVisually();
switch (deploymentStatus) {
case 'initializing':
progressPercent = 20; progressMessage = 'Initializing deployment...';
updateStepStatus('step-init', 'in-progress'); break;
case 'preparing': case 'deploying':
progressPercent = 40; progressMessage = 'Deploying template...';
updateStepStatus('step-init', 'completed'); updateStepStatus('step-template', 'completed'); updateStepStatus('step-deploy', 'in-progress'); break;
case 'publishing':
progressPercent = 70; progressMessage = 'Publishing...';
updateStepStatus('step-init', 'completed'); updateStepStatus('step-template', 'completed'); updateStepStatus('step-deploy', 'completed'); updateStepStatus('step-publish', 'in-progress'); break;
case 'completed':
progressPercent = 100; progressMessage = 'Deployment complete!';
updateStepStatus('step-init', 'completed'); updateStepStatus('step-template', 'completed'); updateStepStatus('step-deploy', 'completed'); updateStepStatus('step-publish', 'completed');
showDeploymentResult(data); break;
case 'failed':
progressPercent = 0; progressMessage = `Deployment failed: ${data.error || 'Unknown error'}`;
updateStepStatus('step-init', 'completed'); // Assume init ok
if (data.failed_step === 'template' || data.failed_step === 'deploy' || data.failed_step === 'publish') updateStepStatus('step-template', 'completed');
if (data.failed_step === 'deploy' || data.failed_step === 'publish') updateStepStatus('step-deploy', 'completed');
// Mark specific failed step
if (data.failed_step === 'init') updateStepStatus('step-init', 'failed');
else if (data.failed_step === 'template') updateStepStatus('step-template', 'failed');
else if (data.failed_step === 'deploy') updateStepStatus('step-deploy', 'failed');
else if (data.failed_step === 'publish') updateStepStatus('step-publish', 'failed');
else { // Fallback
const currentStep = document.querySelector('.deployment-step.in-progress');
if (currentStep) { currentStep.classList.remove('in-progress'); currentStep.classList.add('failed'); }
else { updateStepStatus('step-init', 'failed'); }
}
break;
default: console.warn("Unknown status:", deploymentStatus); progressMessage = `Status: ${deploymentStatus}`; break;
}
updateProgress(progressPercent, progressMessage);
updateDeploymentInfo(data);
}
function simulateDeployment() {
deploymentId = `sim-${Math.random().toString(36).substring(2, 9)}`;
selectedTemplate = selectedTemplate || 'fireworks';
const steps = [ { status: 'initializing', delay: 1500 }, { status: 'deploying', delay: 2500 }, { status: 'publishing', delay: 3000 }, { status: 'completed', delay: 1000 } ];
let currentDelay = 0;
steps.forEach(step => {
currentDelay += step.delay;
setTimeout(() => {
const statusData = { deployment_id: deploymentId, template: selectedTemplate, status: step.status, timestamp: new Date().toISOString() };
updateDeploymentStatus(statusData);
if (step.status === 'completed' || step.status === 'failed') {
if (pollingIntervalId) clearInterval(pollingIntervalId); pollingIntervalId = null;
if (deployBtn) deployBtn.disabled = false; if (previewBtn) previewBtn.disabled = false;
}
}, currentDelay);
});
}
function showDeploymentResult(data) {
const url = data.deployment_url || `https://${data.deployment_id}.${BASE_DOMAIN}`;
if (deploymentUrl) deploymentUrl.href = url;
if (deploymentUrlText) deploymentUrlText.textContent = url.replace(/^https?:\/\//, '');
if (deploymentSection) deploymentSection.style.display = 'none';
if (resultSection) resultSection.style.display = 'block';
}
function resetDeployment() {
if (pollingIntervalId) { clearInterval(pollingIntervalId); pollingIntervalId = null; }
if (resultSection) resultSection.style.display = 'none';
if (deploymentSection) deploymentSection.style.display = 'none';
if (templatesSection) templatesSection.style.display = 'block';
templateCards.forEach(card => card.classList.remove('selected'));
selectedTemplate = null; deploymentId = null; deploymentStatus = 'pending';
if (deployBtn) deployBtn.disabled = true; if (previewBtn) previewBtn.disabled = true;
resetProgressSteps();
if (deploymentInfo) deploymentInfo.innerHTML = '<p>Deployment information will appear here once the process begins.</p>';
}
function resetProgressStepsVisually() {
document.querySelectorAll('.deployment-step').forEach(step => {
step.classList.remove('in-progress', 'completed', 'failed'); step.classList.add('pending');
const icon = step.querySelector('.step-icon');
if(icon) { // Reset icon to number
const stepNumber = step.id.split('-')[1] === 'init' ? 1 : step.id.split('-')[1] === 'template' ? 2 : step.id.split('-')[1] === 'deploy' ? 3 : 4;
icon.innerHTML = stepNumber;
}
});
}
function resetProgressSteps() { resetProgressStepsVisually(); updateProgress(0, ''); }
function updateProgress(percent, message) { if (deploymentProgress) deploymentProgress.style.width = `${percent}%`; if (progressText) progressText.textContent = message; }
function updateStepStatus(stepId, status) {
const step = document.getElementById(stepId);
if (step) {
step.classList.remove('pending', 'in-progress', 'completed', 'failed'); step.classList.add(status);
const icon = step.querySelector('.step-icon');
if (icon) {
if (status === 'completed') icon.innerHTML = '<i class="fas fa-check"></i>';
else if (status === 'failed') icon.innerHTML = '<i class="fas fa-times"></i>';
else if (status === 'in-progress') icon.innerHTML = '<i class="fas fa-spinner fa-spin"></i>';
else { const stepNumber = stepId.split('-')[1] === 'init' ? 1 : stepId.split('-')[1] === 'template' ? 2 : stepId.split('-')[1] === 'deploy' ? 3 : 4; icon.innerHTML = stepNumber; }
}
} else { console.warn(`Step element with ID ${stepId} not found.`); }
}
function updateDeploymentInfo(data) {
if (!deploymentInfo) return;
let html = '<dl>';
if (data.deployment_id) html += `<dt>Deployment ID:</dt><dd>${data.deployment_id}</dd>`;
if (data.template || selectedTemplate) html += `<dt>Template:</dt><dd>${data.template || selectedTemplate}</dd>`;
let statusText = data.status ? data.status.charAt(0).toUpperCase() + data.status.slice(1) : 'Unknown';
html += `<dt>Status:</dt><dd><span class="status-badge status-${data.status || 'unknown'}">${statusText}</span></dd>`;
if (data.timestamp) { try { html += `<dt>Timestamp:</dt><dd>${new Date(data.timestamp).toLocaleString()}</dd>`; } catch (e) { console.error("Invalid timestamp", data.timestamp); } }
if (data.status === 'completed') { const url = data.deployment_url || `https://${data.deployment_id}.${BASE_DOMAIN}`; html += `<dt>URL:</dt><dd><a href="${url}" target="_blank" rel="noopener noreferrer">${url}</a></dd>`; } // Added rel
if (data.status === 'failed' && data.error) html += `<dt style="color: var(--error);">Error:</dt><dd style="color: var(--error);">${data.error}</dd>`;
html += '</dl>';
deploymentInfo.innerHTML = html;
}
// Initialize
initUI();
});
</script>

98
src/pages/api/contact.ts Normal file
View File

@ -0,0 +1,98 @@
import type { APIRoute } from 'astro';
import nodemailer from 'nodemailer';
// Validate environment variables
const emailUser = import.meta.env.EMAIL_USER;
const emailPass = import.meta.env.EMAIL_PASS;
const emailTo = import.meta.env.EMAIL_TO || 'daniel.laforce@argobox.com'; // Default recipient
if (!emailUser || !emailPass) {
console.error("Email credentials (EMAIL_USER, EMAIL_PASS) are not set in environment variables.");
// Optionally, you could throw an error here during build or startup in development
}
// Configure Nodemailer transporter
// Ensure you have configured "less secure app access" in Gmail or use an App Password
// if using 2-Factor Authentication. Consider using a more robust email service
// like SendGrid, Mailgun, or Resend for production.
const transporter = nodemailer.createTransport({
service: 'gmail', // Or your preferred service
auth: {
user: emailUser,
pass: emailPass,
},
});
export const POST: APIRoute = async ({ request }) => {
if (!emailUser || !emailPass) {
return new Response(JSON.stringify({ message: 'Server email configuration error.' }), {
status: 500,
headers: { 'Content-Type': 'application/json' }
});
}
try {
const data = await request.json();
const { name, email, subject, message } = data;
// Basic validation
if (!name || !email || !subject || !message) {
return new Response(JSON.stringify({ message: 'Missing required fields.' }), {
status: 400,
headers: { 'Content-Type': 'application/json' }
});
}
// Email options
const mailOptions = {
from: `"${name}" <${emailUser}>`, // Display name and authorized sender
replyTo: email, // Set reply-to for easy response
to: emailTo, // Recipient address from env or default
subject: `Argobox Contact Form: ${subject}`,
text: `
Name: ${name}
Email: ${email}
Subject: ${subject}
Message:
${message}
`,
html: `
<h3>New Argobox Contact Form Submission</h3>
<p><strong>Name:</strong> ${name}</p>
<p><strong>Email:</strong> <a href="mailto:${email}">${email}</a></p>
<p><strong>Subject:</strong> ${subject}</p>
<h4>Message:</h4>
<p>${message.replace(/\n/g, '<br>')}</p>
`
};
// Send email
await transporter.sendMail(mailOptions);
return new Response(JSON.stringify({ message: 'Message sent successfully!' }), {
status: 200,
headers: { 'Content-Type': 'application/json' }
});
} catch (error) {
console.error('Error processing contact form:', error);
let errorMessage = 'Failed to send message. Please try again later.';
if (error instanceof Error) {
// Check for specific Nodemailer errors if needed
// e.g., if (error.code === 'EAUTH') errorMessage = 'Server authentication error.';
}
return new Response(JSON.stringify({ message: errorMessage }), {
status: 500,
headers: { 'Content-Type': 'application/json' }
});
}
};
// Optional: Add a GET handler if you want to test the endpoint directly
// export const GET: APIRoute = () => {
// return new Response(JSON.stringify({ message: 'Contact API endpoint is active. Use POST to submit.' }), {
// status: 200,
// headers: { 'Content-Type': 'application/json' }
// });
// };

448
src/pages/dashboard.astro Normal file
View File

@ -0,0 +1,448 @@
---
// src/pages/dashboard.astro - Converted from static dashboard.html
import BaseLayout from '../layouts/BaseLayout.astro';
import Header from '../components/Header.astro';
import Footer from '../components/Footer.astro';
const title = "ArgoBox Dashboard | Status and Metrics";
const description = "ArgoBox operations dashboard provides real-time monitoring and status updates for all lab services and infrastructure components.";
// Placeholder data - In a real app, this would come from an API or state management
const systemHealth = { cpu: 23, memory: 42, disk: 78, temp: 52, uptime: "14d 7h 32m" };
const network = { throughput: 12.4, latency: 4, connections: 127, packetLoss: 0.02, dns: 2 };
const kubernetes = { nodes: "3/3 Ready", pods: "42/44 Running", deployments: "11/12 Healthy", memoryPressure: "Low", load: 37 };
const storage = { nasStatus: "Online", raidHealth: "Optimal", iops: 243, diskIO: 2.4, backups: "Completed 07/24" };
const serviceStatus = [
{ name: "Kubernetes API", status: "online" },
{ name: "Container Registry", status: "online" },
{ name: "Monitoring", status: "degraded" },
{ name: "Authentication", status: "online" },
{ name: "Database", status: "online" },
{ name: "Storage", status: "online" },
{ name: "CI/CD", status: "offline" },
{ name: "Logging", status: "online" },
];
const weeklyTraffic = [65, 80, 45, 90, 60, 75, 40]; // Example percentages
const recentAlerts = [
{ name: "CI/CD Pipeline Failure", level: "error", time: "3h ago" },
{ name: "High Memory Usage", level: "warning", time: "6h ago" },
{ name: "Monitoring Service Degraded", level: "warning", time: "12h ago" },
{ name: "Backup Completed", level: "healthy", time: "1d ago" },
{ name: "Security Update Available", level: "warning", time: "2d ago" },
];
// Function to determine status class
const getStatusClass = (value: number | string, type: 'percent' | 'temp' | 'loss' | 'latency' | 'dns' | 'status') => {
if (type === 'status') {
return value === 'Online' || value === 'Optimal' || value === 'Low' || value === 'Ready' || value === 'Running' || value === 'Completed' ? 'healthy' :
value === 'Degraded' || value === 'Warning' ? 'warning' : 'error';
}
if (typeof value !== 'number') return '';
if (type === 'percent') return value > 90 ? 'error' : value > 75 ? 'warning' : 'healthy';
if (type === 'temp') return value > 70 ? 'error' : value > 60 ? 'warning' : 'healthy';
if (type === 'loss') return value > 1 ? 'error' : value > 0.1 ? 'warning' : 'healthy';
if (type === 'latency' || type === 'dns') return value > 50 ? 'error' : value > 10 ? 'warning' : 'healthy';
return 'healthy';
};
---
<BaseLayout {title} {description}>
{/* Add Font Awesome if not loaded globally */}
{/* <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/css/all.min.css" integrity="sha512-z3gLpd7yknf1YoNbCzqRKc4qyor8gaKU1qmn+CShxbuBusANI9QpRohGBreCFkKxLhei6S9CQXFEbbKuqLg0DA==" crossorigin="anonymous" referrerpolicy="no-referrer" /> */}
<Header slot="header" />
<main class="dashboard-container">
<div class="container">
<!-- Offline Notice Banner -->
<div class="offline-notice" id="offline-notice">
<div class="offline-notice-icon">
<i class="fas fa-exclamation-triangle"></i>
</div>
<div class="offline-notice-text">
<h3>Dashboard is Currently Offline</h3>
<p>The live dashboard is currently in simulation mode using placeholder data. Real-time data is not available.</p>
</div>
</div>
<header class="dashboard-header">
<h1 class="dashboard-title">Infrastructure Status Dashboard</h1>
<p class="dashboard-subtitle">Monitoring and metrics for ArgoBox infrastructure components</p>
</header>
<div class="dashboard-grid">
<!-- System Health Card -->
<div class="dashboard-card">
<div class="card-header">
<h2 class="card-title">System Health</h2>
<div class="card-icon"><i class="fas fa-heartbeat"></i></div>
</div>
<div class="metric-list">
<div class="metric-item">
<span class="metric-name">CPU Usage</span>
<span class={`metric-value ${getStatusClass(systemHealth.cpu, 'percent')}`}>{systemHealth.cpu}%</span>
</div>
<div class="metric-item">
<span class="metric-name">Memory Usage</span>
<span class={`metric-value ${getStatusClass(systemHealth.memory, 'percent')}`}>{systemHealth.memory}%</span>
</div>
<div class="metric-item">
<span class="metric-name">Disk Space</span>
<span class={`metric-value ${getStatusClass(systemHealth.disk, 'percent')}`}>{systemHealth.disk}%</span>
</div>
<div class="metric-item">
<span class="metric-name">Temperature</span>
<span class={`metric-value ${getStatusClass(systemHealth.temp, 'temp')}`}>{systemHealth.temp}°C</span>
</div>
<div class="metric-item">
<span class="metric-name">Uptime</span>
<span class="metric-value">{systemHealth.uptime}</span>
</div>
</div>
</div>
<!-- Network Card -->
<div class="dashboard-card">
<div class="card-header">
<h2 class="card-title">Network</h2>
<div class="card-icon"><i class="fas fa-network-wired"></i></div>
</div>
<div class="metric-list">
<div class="metric-item">
<span class="metric-name">Throughput</span>
<span class="metric-value healthy">{network.throughput} MB/s</span>
</div>
<div class="metric-item">
<span class="metric-name">Latency</span>
<span class={`metric-value ${getStatusClass(network.latency, 'latency')}`}>{network.latency}ms</span>
</div>
<div class="metric-item">
<span class="metric-name">Active Connections</span>
<span class="metric-value">{network.connections}</span>
</div>
<div class="metric-item">
<span class="metric-name">Packet Loss</span>
<span class={`metric-value ${getStatusClass(network.packetLoss, 'loss')}`}>{network.packetLoss}%</span>
</div>
<div class="metric-item">
<span class="metric-name">DNS Response</span>
<span class={`metric-value ${getStatusClass(network.dns, 'dns')}`}>{network.dns}ms</span>
</div>
</div>
</div>
<!-- Kubernetes Card -->
<div class="dashboard-card">
<div class="card-header">
<h2 class="card-title">Kubernetes</h2>
<div class="card-icon"><i class="fas fa-dharmachakra"></i></div>
</div>
<div class="metric-list">
<div class="metric-item">
<span class="metric-name">Node Status</span>
<span class={`metric-value ${kubernetes.nodes.includes('Ready') && !kubernetes.nodes.includes('/') ? 'healthy' : 'warning'}`}>{kubernetes.nodes}</span>
</div>
<div class="metric-item">
<span class="metric-name">Pods</span>
<span class={`metric-value ${kubernetes.pods.includes('Running') && !kubernetes.pods.includes('/') ? 'healthy' : 'warning'}`}>{kubernetes.pods}</span>
</div>
<div class="metric-item">
<span class="metric-name">Deployments</span>
<span class={`metric-value ${kubernetes.deployments.includes('Healthy') && !kubernetes.deployments.includes('/') ? 'healthy' : 'warning'}`}>{kubernetes.deployments}</span>
</div>
<div class="metric-item">
<span class="metric-name">Memory Pressure</span>
<span class={`metric-value ${getStatusClass(kubernetes.memoryPressure, 'status')}`}>{kubernetes.memoryPressure}</span>
</div>
<div class="metric-item">
<span class="metric-name">Cluster Load</span>
<span class={`metric-value ${getStatusClass(kubernetes.load, 'percent')}`}>{kubernetes.load}%</span>
</div>
</div>
</div>
<!-- Storage Card -->
<div class="dashboard-card">
<div class="card-header">
<h2 class="card-title">Storage</h2>
<div class="card-icon"><i class="fas fa-hdd"></i></div>
</div>
<div class="metric-list">
<div class="metric-item">
<span class="metric-name">NAS Status</span>
<span class={`metric-value ${getStatusClass(storage.nasStatus, 'status')}`}>{storage.nasStatus}</span>
</div>
<div class="metric-item">
<span class="metric-name">RAID Health</span>
<span class={`metric-value ${getStatusClass(storage.raidHealth, 'status')}`}>{storage.raidHealth}</span>
</div>
<div class="metric-item">
<span class="metric-name">IOPS</span>
<span class="metric-value">{storage.iops}</span>
</div>
<div class="metric-item">
<span class="metric-name">Disk I/O</span>
<span class="metric-value healthy">{storage.diskIO} MB/s</span>
</div>
<div class="metric-item">
<span class="metric-name">Backups</span>
<span class={`metric-value ${getStatusClass(storage.backups, 'status')}`}>{storage.backups}</span>
</div>
</div>
</div>
</div>
<!-- Service Status Card -->
<div class="dashboard-card">
<div class="card-header">
<h2 class="card-title">Service Status</h2>
<div class="card-icon"><i class="fas fa-server"></i></div>
</div>
<div class="status-grid">
{serviceStatus.map(service => (
<div class="status-item">
<div class={`status-indicator ${service.status}`}></div>
<span class="status-name">{service.name}</span>
</div>
))}
</div>
</div>
<!-- Grid for Charts/Alerts -->
<div class="dashboard-grid">
<!-- Weekly Traffic Card -->
<div class="dashboard-card">
<div class="card-header">
<h2 class="card-title">Weekly Traffic</h2>
<div class="card-icon"><i class="fas fa-chart-line"></i></div>
</div>
<div class="chart-container">
{weeklyTraffic.map(value => (
<div class="chart-bar" style={`height: ${value}%;`}></div>
))}
</div>
<div class="chart-labels">
<div class="chart-label">Mon</div>
<div class="chart-label">Tue</div>
<div class="chart-label">Wed</div>
<div class="chart-label">Thu</div>
<div class="chart-label">Fri</div>
<div class="chart-label">Sat</div>
<div class="chart-label">Sun</div>
</div>
</div>
<!-- Recent Alerts Card -->
<div class="dashboard-card">
<div class="card-header">
<h2 class="card-title">Recent Alerts</h2>
<div class="card-icon"><i class="fas fa-bell"></i></div>
</div>
<div class="metric-list">
{recentAlerts.map(alert => (
<div class="metric-item">
<span class={`metric-name alert-${alert.level}`}>{alert.name}</span>
<span class={`metric-value ${alert.level}`}>{alert.time}</span>
</div>
))}
</div>
</div>
</div>
</div>
</main>
<Footer slot="footer" />
</BaseLayout>
<style is:global>
/* Dashboard-specific styles adapted from dashboard.html */
/* Use theme variables */
.dashboard-container {
padding-top: 2rem; /* Add space from header */
padding-bottom: 4rem;
}
.dashboard-header {
margin-bottom: 2rem;
text-align: center; /* Center header */
}
.dashboard-title {
font-size: clamp(1.8rem, 5vw, 2.5rem); /* Responsive title */
font-weight: 700;
margin-bottom: 0.5rem;
color: var(--text-primary);
}
.dashboard-subtitle {
color: var(--text-secondary);
font-size: clamp(1rem, 3vw, 1.1rem);
}
.dashboard-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 1.5rem;
margin-bottom: 2rem;
}
.dashboard-card {
background-color: var(--card-bg);
border: 1px solid var(--border);
border-radius: 1rem;
padding: 1.5rem;
transition: all var(--transition-normal);
}
.dashboard-card:hover {
background-color: var(--card-hover-bg);
transform: translateY(-5px);
box-shadow: var(--card-shadow);
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
padding-bottom: 0.75rem; /* Add padding */
border-bottom: 1px solid var(--border); /* Add border */
}
.card-title {
font-weight: 600;
font-size: 1.1rem;
color: var(--text-primary);
}
.card-icon {
color: var(--accent);
font-size: 1.25rem;
}
.metric-list { display: grid; gap: 1rem; }
.metric-item { display: flex; justify-content: space-between; align-items: center; font-size: 0.9rem; }
.metric-name { color: var(--text-secondary); }
.metric-value { font-family: var(--font-mono); font-weight: 500; color: var(--text-primary); }
.metric-value.healthy { color: var(--success); }
.metric-value.warning { color: var(--warning); }
.metric-value.error { color: var(--error); }
.metric-name.alert-warning { color: var(--warning); font-weight: 500; } /* Style alert names */
.metric-name.alert-error { color: var(--error); font-weight: 500; }
.metric-name.alert-healthy { color: var(--text-secondary); } /* Normal color for healthy alerts */
.status-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); /* Adjust minmax */
gap: 1rem;
}
.status-item {
display: flex;
align-items: center;
gap: 0.75rem; /* Increased gap */
background-color: var(--bg-secondary); /* Use secondary bg */
padding: 0.75rem;
border-radius: 0.5rem;
border: 1px solid var(--border);
}
.status-indicator {
width: 10px; /* Slightly smaller */
height: 10px;
border-radius: 50%;
flex-shrink: 0; /* Prevent shrinking */
}
.status-indicator.online { background-color: var(--success); box-shadow: 0 0 8px rgba(16, 185, 129, 0.4); } /* Adjusted shadow */
.status-indicator.offline { background-color: var(--error); box-shadow: 0 0 8px rgba(239, 68, 68, 0.4); }
.status-indicator.degraded { background-color: var(--warning); box-shadow: 0 0 8px rgba(245, 158, 11, 0.4); }
.status-name { font-size: 0.9rem; color: var(--text-primary); }
.chart-container {
height: 180px; /* Slightly smaller */
margin-top: 1rem;
display: flex;
align-items: flex-end;
gap: 0.5rem;
padding: 0.5rem; /* Add padding */
background-color: var(--bg-secondary); /* Add background */
border-radius: 0.5rem; /* Add radius */
border: 1px solid var(--border);
}
.chart-bar {
flex: 1;
background-color: rgba(59, 130, 246, 0.2);
border-radius: 4px 4px 0 0;
position: relative;
min-height: 5px; /* Ensure visibility */
transition: height 0.5s ease; /* Animate height */
}
/* Removed ::before for simplicity, height set directly */
.chart-labels { display: flex; justify-content: space-between; margin-top: 0.5rem; }
.chart-label { font-size: 0.75rem; color: var(--text-secondary); flex: 1; text-align: center; }
.offline-notice {
background-color: rgba(239, 68, 68, 0.1); /* Use theme error */
border: 1px solid rgba(239, 68, 68, 0.3);
border-left: 4px solid var(--error); /* Use theme error */
border-radius: 0.5rem;
padding: 1.5rem;
margin-bottom: 2rem;
display: flex;
align-items: center;
gap: 1.5rem;
}
.offline-notice-icon { color: var(--error); font-size: 2rem; flex-shrink: 0; }
.offline-notice-text h3 { font-weight: 600; margin-bottom: 0.5rem; color: var(--error); }
.offline-notice-text p { color: var(--text-secondary); margin: 0; }
@media (max-width: 768px) {
.dashboard-grid { grid-template-columns: 1fr; }
.status-grid { grid-template-columns: 1fr 1fr; }
.offline-notice { flex-direction: column; text-align: center; gap: 1rem; }
}
</style>
<script is:inline>
// Placeholder for potential future JS to fetch real data
// For now, it uses the static data from the frontmatter
// Example: Function to update status indicators dynamically
// function updateServiceStatus(serviceName, status) {
// const item = document.querySelector(`.service-${serviceName.toLowerCase().replace(/\s+/g, '-')}`); // Requires adding class to status items
// if (item) {
// const indicator = item.querySelector('.status-indicator');
// indicator.className = `status-indicator ${status}`; // status should be 'online', 'offline', 'degraded'
// }
// }
// Example: Function to update metrics
// function updateMetric(metricNameSelector, value, statusClass = '') {
// const valueEl = document.querySelector(metricNameSelector); // Need unique selectors for each metric value
// if (valueEl) {
// valueEl.textContent = value;
// valueEl.className = `metric-value ${statusClass}`;
// }
// }
// Example: Fetch data and update UI
// async function fetchDashboardData() {
// try {
// const response = await fetch('/api/dashboard-status'); // Your API endpoint
// if (!response.ok) throw new Error('Failed to fetch status');
// const data = await response.json();
//
// // Update UI elements based on data
// // updateMetric('#cpu-usage', data.systemHealth.cpu, getStatusClass(data.systemHealth.cpu, 'percent'));
// // ... update other metrics ...
// // data.serviceStatus.forEach(service => updateServiceStatus(service.name, service.status));
//
// } catch (error) {
// console.error("Error fetching dashboard data:", error);
// // Show error state in UI
// }
// }
// document.addEventListener('DOMContentLoaded', () => {
// // fetchDashboardData(); // Initial fetch
// // setInterval(fetchDashboardData, 60000); // Fetch every minute
// });
</script>

0
src/pages/dashboard.html Normal file
View File

838
src/pages/homelab.astro Normal file
View File

@ -0,0 +1,838 @@
---
// src/pages/homelab.astro - Converted from static homelab.html
import BaseLayout from '../layouts/BaseLayout.astro';
import Header from '../components/Header.astro';
import Footer from '../components/Footer.astro';
const title = "Home Lab | Argobox - LaForceIT Tech Hub";
const description = "ArgoBox - A production-grade Kubernetes homelab for DevOps experimentation, infrastructure automation, and containerized application deployment.";
// Data for services (can be fetched or defined here)
// Example structure - replace with actual data source if needed
const servicesData = {
development: [
{ name: "Gitea", description: "Self-hosted Git service", url: "https://git.argobox.com", status: "live", icon: "fas fa-code-branch", available: true },
{ name: "VS Code Server", description: "Remote development environment", url: "https://code.argobox.com", status: "live", icon: "fas fa-terminal", available: true },
{ name: "Drone CI", description: "Continuous Integration server", url: "https://drone.argobox.com", status: "live", icon: "fas fa-cogs", available: true },
],
media: [
{ name: "Plex", description: "Media streaming server", url: "https://plex.argobox.com", status: "live", icon: "fas fa-play-circle", available: true },
{ name: "Jellyfin", description: "Open source media system", url: "https://jellyfin.argobox.com", status: "live", icon: "fas fa-film", available: true },
{ name: "Sonarr", description: "TV show management", url: "#", status: "restricted", icon: "fas fa-tv", available: false },
{ name: "Radarr", description: "Movie management", url: "#", status: "restricted", icon: "fas fa-video", available: false },
{ name: "Prowlarr", description: "Indexer management", url: "#", status: "restricted", icon: "fas fa-search", available: false },
],
utilities: [
{ name: "File Browser", description: "Web file manager", url: "https://files.argobox.com", status: "live", icon: "fas fa-folder-open", available: true },
{ name: "Vaultwarden", description: "Password manager", url: "https://vault.argobox.com", status: "live", icon: "fas fa-key", available: true },
{ name: "Homepage", description: "Service dashboard", url: "https://dash.argobox.com", status: "live", icon: "fas fa-tachometer-alt", available: true },
{ name: "Uptime Kuma", description: "Status monitoring", url: "https://status.argobox.com", status: "live", icon: "fas fa-heartbeat", available: true },
],
infrastructure: [
{ name: "Proxmox VE", description: "Virtualization platform", url: "#", status: "restricted", icon: "fas fa-server", available: false },
{ name: "Kubernetes (K3s)", description: "Container orchestration", url: "#", status: "restricted", icon: "fas fa-dharmachakra", available: false },
{ name: "Traefik", description: "Ingress controller", url: "#", status: "restricted", icon: "fas fa-route", available: false },
{ name: "OPNsense", description: "Firewall/Router", url: "#", status: "restricted", icon: "fas fa-shield-alt", available: false },
]
};
// Data for projects (can be fetched or defined here)
const projectsData = [
{ title: "Ansible Playbooks", description: "Collection of playbooks for automating system configuration and application deployment.", icon: "fab fa-ansible", tech: ["Ansible", "YAML", "Jinja2"], url: "/resources/iac" }, // Link to relevant resource page
{ title: "Kubernetes Manifests", description: "YAML definitions for deploying various applications and services on Kubernetes.", icon: "fas fa-dharmachakra", tech: ["Kubernetes", "YAML", "Helm"], url: "/resources/kubernetes" },
{ title: "Monitoring Dashboards", description: "Grafana dashboards for visualizing infrastructure and application metrics.", icon: "fas fa-chart-line", tech: ["Grafana", "PromQL", "JSON"], url: "/resources/config-files" }, // Link to relevant resource page
{ title: "Cloudflare Tunnel Setup", description: "Securely exposing home lab services to the internet using Cloudflare Tunnels.", icon: "fas fa-cloud", tech: ["Cloudflare", "Networking", "Security"], url: "/posts/cloudflare-tunnel-setup" } // Link to blog post
];
// Data for dashboards (can be fetched or defined here)
const dashboardsData = [
{ title: "Infrastructure Overview", description: "Key metrics for Proxmox hosts, network devices, and storage.", previewClass: "infrastructure", url: "https://dash.argobox.com/goto/...", icon: "fas fa-server" },
{ title: "Kubernetes Cluster", description: "Detailed view of K3s cluster resources, node status, and pod health.", previewClass: "kubernetes", url: "https://dash.argobox.com/goto/...", icon: "fas fa-dharmachakra" },
{ title: "Network Traffic", description: "Real-time and historical network usage, firewall logs, and connection tracking.", previewClass: "network", url: "https://dash.argobox.com/goto/...", icon: "fas fa-network-wired" },
{ title: "Service Performance", description: "Application-specific metrics, request latency, and error rates.", previewClass: "services", url: "https://dash.argobox.com/goto/...", icon: "fas fa-cogs" }
];
// Data for contact form (if keeping it on this page)
// const contactInfo = { email: "daniel.laforce@argobox.com", /* other info */ };
---
<BaseLayout {title} {description}>
{/* Add Font Awesome if not loaded globally by BaseLayout */}
{/* <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/css/all.min.css" integrity="sha512-z3gLpd7yknf1YoNbCzqRKc4qyor8gaKU1qmn+CShxbuBusANI9QpRohGBreCFkKxLhei6S9CQXFEbbKuqLg0DA==" crossorigin="anonymous" referrerpolicy="no-referrer" /> */}
<Header slot="header" />
<main class="homelab-page">
<!-- Hero Section -->
<section id="home" class="hero">
<div class="particles-container" id="particles-container"></div>
<div class="container">
<div class="hero-content">
<div class="hero-text">
<h1 class="hero-title">
Enterprise-Grade <span class="highlight">Home Lab</span> Environment
</h1>
<p class="hero-description">
A production-ready infrastructure platform for DevOps experimentation, distributed systems, and automating everything with code.
</p>
<div class="hero-stats">
<div class="stat-item">
<div class="stat-icon"><i class="fas fa-microchip"></i></div>
<div class="stat-detail">
<div class="stat-value">32+</div>
<div class="stat-name">CPU Cores</div>
</div>
</div>
<div class="stat-item">
<div class="stat-icon"><i class="fas fa-memory"></i></div>
<div class="stat-detail">
<div class="stat-value">64GB</div>
<div class="stat-name">RAM</div>
</div>
</div>
<div class="stat-item">
<div class="stat-icon"><i class="fas fa-hdd"></i></div>
<div class="stat-detail">
<div class="stat-value">12TB</div>
<div class="stat-name">Storage</div>
</div>
</div>
<div class="stat-item">
<div class="stat-icon"><i class="fas fa-server"></i></div>
<div class="stat-detail">
<div class="stat-value">{Object.values(servicesData).flat().length}+</div>
<div class="stat-name">Services</div>
</div>
</div>
</div>
<div class="cta-buttons">
{/* Link to the Astro page for Ansible Sandbox */}
<a href="/ansible-sandbox" class="btn btn-danger" id="ansible-sandbox-btn">
<i class="fab fa-ansible btn-icon"></i>
<span class="btn-text">Try Ansible Sandbox</span>
<span class="offline-badge">Offline</span> {/* Status updated by JS */}
</a>
<a href="#architecture" class="btn btn-outline">
<i class="fas fa-network-wired btn-icon"></i>
<span class="btn-text">Explore Architecture</span>
</a>
</div>
</div>
<div class="hero-terminal">
<div class="terminal-header">
<div class="terminal-buttons">
<span class="terminal-btn close"></span>
<span class="terminal-btn minimize"></span>
<span class="terminal-btn maximize"></span>
</div>
<div class="terminal-title">argobox ~ k8s-status</div>
</div>
<div class="terminal-body">
<div class="terminal-line">$ kubectl get nodes</div>
<div class="terminal-line output">NAME STATUS ROLES AGE VERSION</div>
<div class="terminal-line output">argobox Ready control-plane,master 154d v1.25.16+k3s1</div>
<div class="terminal-line output">argobox-lite Ready worker 154d v1.25.16+k3s1</div>
<div class="terminal-line blank">&nbsp;</div>
<div class="terminal-line">$ kubectl get pods -A | grep Running | wc -l</div>
<div class="terminal-line output">32</div>
<div class="terminal-line blank">&nbsp;</div>
<div class="terminal-line">$ uptime</div>
<div class="terminal-line output">14:30:25 up 154 days, 23:12, 1 user, load average: 0.22, 0.18, 0.15</div>
<div class="terminal-line blank">&nbsp;</div>
<div class="terminal-line">$ ansible-playbook status.yml</div>
<div class="terminal-line output">PLAY [Check system status] *******************************************</div>
<div class="terminal-line output">TASK [Gathering Facts] **********************************************</div>
<div class="terminal-line output success">ok: [argobox]</div>
<div class="terminal-line output success">ok: [argobox-lite]</div>
<div class="terminal-line output">TASK [Check service status] *****************************************</div>
<div class="terminal-line output success">ok: [argobox]</div>
<div class="terminal-line output success">ok: [argobox-lite]</div>
<div class="terminal-line output">PLAY RECAP **********************************************************</div>
<div class="terminal-line output success">argobox : ok=2 changed=0 unreachable=0 failed=0 skipped=0</div>
<div class="terminal-line output success">argobox-lite: ok=2 changed=0 unreachable=0 failed=0 skipped=0</div>
<div class="terminal-line typing">$ <span class="cursor">|</span></div>
</div>
</div>
</div>
</div>
</section>
<!-- Architecture Section -->
<section id="architecture" class="architecture section-padding">
<div class="container">
<div class="section-header">
<h2 class="section-title">Infrastructure Architecture</h2>
<p class="section-description">
Enterprise-grade network topology with redundancy, virtualization, and secure segmentation.
</p>
</div>
{/* Consider replacing with an actual image or a more maintainable diagram component */}
<img src="/images/homelab/argobox-architecture.svg" alt="ArgoBox Architecture Diagram" class="architecture-diagram-image" />
{/* Fallback or simplified diagram if image isn't available */}
<div class="architecture-details">
<div class="detail-card">
<div class="detail-icon"><i class="fas fa-shield-alt"></i></div>
<h3 class="detail-title">Network Security</h3>
<p class="detail-description">
Enterprise firewall with network segmentation using VLANs and strict access controls. Redundant routing with automatic failover between OPNsense and OpenWrt.
</p>
</div>
<div class="detail-card">
<div class="detail-icon"><i class="fas fa-cloud"></i></div>
<h3 class="detail-title">Virtualization</h3>
<p class="detail-description">
Proxmox virtualization platform with ZFS storage pools in RAID10 configuration. Optimized storage pools for VMs and containers with proper resource allocation.
</p>
</div>
<div class="detail-card">
<div class="detail-icon"><i class="fas fa-route"></i></div>
<h3 class="detail-title">High Availability</h3>
<p class="detail-description">
Full redundancy with failover routing, replicated storage, and resilient services. Automatic service recovery and load balancing across nodes.
</p>
</div>
</div>
</div>
</section>
<!-- Technologies Section -->
<section id="technologies" class="technologies section-padding alt-bg">
<div class="container">
<div class="section-header">
<h2 class="section-title">Core Technologies</h2>
<p class="section-description">
The ArgoBox lab leverages cutting-edge open source technologies to create a powerful, flexible infrastructure.
</p>
</div>
<div class="tech-grid">
{/* Data could be externalized */}
<div class="tech-card">
<div class="tech-icon"><i class="fas fa-dharmachakra"></i></div>
<h3 class="tech-title">Kubernetes (K3s)</h3>
<p class="tech-description">Lightweight Kubernetes distribution running across multiple nodes for container orchestration. Powers all microservices and applications.</p>
<div class="tech-features"><span class="tech-feature">Multi-node cluster</span><span class="tech-feature">Persistent volumes</span><span class="tech-feature">Traefik ingress</span><span class="tech-feature">Auto-healing</span></div>
</div>
<div class="tech-card featured">
<div class="tech-icon"><i class="fab fa-ansible"></i></div>
<h3 class="tech-title">Ansible Automation</h3>
<p class="tech-description">Infrastructure as code platform for automated provisioning, configuration management, and application deployment across the entire environment.</p>
<div class="tech-features"><span class="tech-feature">Playbook library</span><span class="tech-feature">Role-based configs</span><span class="tech-feature">Interactive sandbox</span><span class="tech-feature">Idempotent workflows</span></div>
</div>
<div class="tech-card">
<div class="tech-icon"><i class="fas fa-server"></i></div>
<h3 class="tech-title">Proxmox</h3>
<p class="tech-description">Enterprise-class virtualization platform running virtual machines and containers with ZFS storage backend for data integrity.</p>
<div class="tech-features"><span class="tech-feature">ZFS storage</span><span class="tech-feature">Resource balancing</span><span class="tech-feature">Live migration</span><span class="tech-feature">Hardware passthrough</span></div>
</div>
<div class="tech-card">
<div class="tech-icon"><i class="fas fa-shield-alt"></i></div>
<h3 class="tech-title">Zero Trust Security</h3>
<p class="tech-description">Comprehensive security architecture with Cloudflare tunnels, network segmentation, and authentication at all service boundaries.</p>
<div class="tech-features"><span class="tech-feature">Cloudflare tunnels</span><span class="tech-feature">OPNsense firewall</span><span class="tech-feature">VLAN segmentation</span><span class="tech-feature">WireGuard VPN</span></div>
</div>
<div class="tech-card">
<div class="tech-icon"><i class="fas fa-database"></i></div>
<h3 class="tech-title">PostgreSQL</h3>
<p class="tech-description">Enterprise database cluster for application data storage with automated backups, replication, and performance optimization.</p>
<div class="tech-features"><span class="tech-feature">Automated backups</span><span class="tech-feature">Connection pooling</span><span class="tech-feature">Optimized for K8s</span><span class="tech-feature">Multi-app support</span></div>
</div>
<div class="tech-card">
<div class="tech-icon"><i class="fas fa-chart-line"></i></div>
<h3 class="tech-title">Monitoring Stack</h3>
<p class="tech-description">Comprehensive monitoring with Prometheus, Grafana, and AlertManager for real-time visibility into all infrastructure components.</p>
<div class="tech-features"><span class="tech-feature">Prometheus metrics</span><span class="tech-feature">Grafana dashboards</span><span class="tech-feature">Automated alerts</span><span class="tech-feature">Historical data</span></div>
</div>
</div>
</div>
</section>
<!-- Services Section -->
<section id="services" class="services section-padding">
<div class="container">
<div class="section-header">
<h2 class="section-title">Available Services</h2>
<p class="section-description">
Explore the various services and applications hosted in the ArgoBox environment.
</p>
</div>
<div class="services-info-banner">
<i class="fas fa-info-circle info-icon"></i>
<p>Some services require authentication and are restricted. Available public services are highlighted and clickable.</p>
</div>
<div class="services-grid">
{Object.entries(servicesData).map(([categoryKey, categoryServices]) => (
<div class="services-category">
<h3 class="category-title">
<i class={`fas ${
categoryKey === 'development' ? 'fa-code' :
categoryKey === 'media' ? 'fa-photo-video' :
categoryKey === 'utilities' ? 'fa-tools' :
'fa-cogs' // Default for infrastructure
} category-icon`}></i>
{categoryKey.charAt(0).toUpperCase() + categoryKey.slice(1)} Tools
</h3>
<div class="service-items">
{categoryServices.map(service => (
<a
href={service.available ? service.url : '#'}
class:list={["service-item", { available: service.available }, `service-${service.name.toLowerCase().replace(/\s+/g, '-')}`]}
target={service.available ? "_blank" : undefined}
rel={service.available ? "noopener noreferrer" : undefined}
aria-disabled={!service.available}
>
<div class="service-icon"><i class={service.icon}></i></div>
<div class="service-info">
<div class="service-name">{service.name}</div>
<p class="service-description">{service.description}</p>
</div>
<span class={`service-status ${service.status}`}>
<span class="status-dot"></span>
{service.status.charAt(0).toUpperCase() + service.status.slice(1)}
</span>
</a>
))}
</div>
</div>
))}
</div>
</div>
</section>
<!-- Projects Section -->
<section id="projects" class="projects section-padding alt-bg">
<div class="container">
<div class="section-header">
<h2 class="section-title">Related Projects & Code</h2>
<p class="section-description">
Explore associated projects, configurations, and code repositories related to the ArgoBox lab.
</p>
</div>
<div class="projects-grid">
{projectsData.map(project => (
<a href={project.url} class="project-card" target={project.url.startsWith('http') ? '_blank' : undefined} rel={project.url.startsWith('http') ? 'noopener noreferrer' : undefined}>
<div class="project-header">
<div class="project-icon"><i class={project.icon}></i></div>
<h3 class="project-title">{project.title}</h3>
</div>
<p class="project-description">{project.description}</p>
<div class="project-tech">
{project.tech.map(tech => <span class="tech-badge">{tech}</span>)}
</div>
<div class="project-cta">
<span class="btn btn-sm btn-outline">
{project.url.startsWith('http') ? 'View Project' : 'View Details'} <i class="fas fa-arrow-right"></i>
</span>
</div>
</a>
))}
</div>
</div>
</section>
<!-- Dashboards Section -->
<section id="dashboards" class="dashboards section-padding">
<div class="container">
<div class="section-header">
<h2 class="section-title">Live Dashboards</h2>
<p class="section-description">
Real-time monitoring dashboards providing insights into the lab's performance and status. (Authentication Required)
</p>
</div>
<div class="services-info-banner"> {/* Reusing banner style */}
<i class="fas fa-lock info-icon"></i>
<p>Access to live dashboards requires authentication via Cloudflare Access.</p>
</div>
<div class="dashboard-grid">
{dashboardsData.map(dash => (
<a href={dash.url} class="dashboard-card" target="_blank" rel="noopener noreferrer">
<div class={`dashboard-preview ${dash.previewClass}`}>
<div class="dashboard-overlay">
<div class="overlay-content">
<div class="overlay-icon"><i class={dash.icon}></i></div>
<div class="overlay-text">View Dashboard</div>
</div>
</div>
</div>
<div class="dashboard-info">
<h3 class="dashboard-title">{dash.title}</h3>
<p class="dashboard-description">{dash.description}</p>
<div class="dashboard-cta">
<span class="btn btn-sm btn-primary">Access Dashboard</span>
</div>
</div>
</a>
))}
</div>
</div>
</section>
{/* Contact Section - Optional: Can be moved to a separate page */}
{/*
<section id="contact" class="contact section-padding alt-bg">
<div class="container">
<div class="section-header">
<h2 class="section-title">Get In Touch</h2>
<p class="section-description">Have questions or want to collaborate? Reach out!</p>
</div>
<div class="contact-grid">
<div class="contact-info">
<div class="contact-item">
<div class="contact-icon"><i class="fas fa-envelope"></i></div>
<h3 class="contact-title">Email</h3>
<a href={`mailto:${contactInfo.email}`} class="contact-link">{contactInfo.email}</a>
</div>
Add other contact items like LinkedIn, GitHub etc.
</div>
<div class="contact-form-container">
<form id="contact-form" class="contact-form">
<div class="form-group">
<label for="name">Name</label>
<input type="text" id="name" name="name" required />
</div>
<div class="form-group">
<label for="email">Email</label>
<input type="email" id="email" name="email" required />
</div>
<div class="form-group">
<label for="subject">Subject</label>
<input type="text" id="subject" name="subject" required />
</div>
<div class="form-group">
<label for="message">Message</label>
<textarea id="message" name="message" required></textarea>
</div>
<button type="submit" class="btn btn-primary">Send Message</button>
</form>
</div>
</div>
</div>
</section>
*/}
</main>
<Footer slot="footer" />
</BaseLayout>
<style is:global>
/* Import relevant styles from styles.css, adapted for Astro */
/* NOTE: General resets, body, typography, basic buttons are handled by global.css/theme.css */
/* Hero Section Specific Styles */
.hero {
min-height: 100vh; /* Use min-height instead of height */
display: flex;
align-items: center;
position: relative;
overflow: hidden;
padding-top: 6rem; /* Adjust as needed */
padding-bottom: 4rem; /* Add padding */
background: linear-gradient(180deg, var(--bg-secondary), var(--bg-primary)); /* Use theme vars */
}
.hero-content {
display: grid;
grid-template-columns: 1fr 1fr;
grid-gap: 3rem;
align-items: center;
position: relative; /* Ensure content is above particles */
z-index: 1;
}
.particles-container {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
overflow: hidden;
pointer-events: none;
z-index: 0;
}
.particle {
position: absolute;
background-color: var(--accent); /* Use theme var */
border-radius: 50%;
pointer-events: none;
/* Animation will be added by script */
}
@keyframes float-particle { /* Keep keyframes */
0% { transform: translateY(0) translateX(0); opacity: 0.3; }
50% { transform: translateY(-100px) translateX(50px); opacity: 0.1; }
100% { transform: translateY(0) translateX(0); opacity: 0.3; }
}
.hero-text { /* Styles seem okay */ }
.hero-title {
font-size: clamp(2.5rem, 5vw, 3.25rem); /* Adjust size */
margin-bottom: 1.5rem;
line-height: 1.2;
font-weight: 700; /* Use theme font weight */
color: var(--text-primary); /* Use theme var */
}
.hero-title .highlight {
background: var(--accent-gradient); /* Use theme var */
-webkit-background-clip: text;
background-clip: text;
color: transparent;
position: relative;
}
.hero-title .highlight::after { /* Optional underline effect */
content: '';
position: absolute;
width: 100%;
height: 4px; /* Adjust thickness */
bottom: 2px; /* Adjust position */
left: 0;
background: var(--accent-gradient);
border-radius: 2px;
opacity: 0.3;
z-index: -1;
}
.hero-description {
font-size: var(--font-size-lg); /* Use theme var */
color: var(--text-secondary); /* Use theme var */
margin-bottom: 2rem;
max-width: 550px;
}
.hero-stats {
display: flex;
flex-wrap: wrap;
gap: 1.5rem;
margin: 2rem 0;
}
.stat-item { display: flex; align-items: center; gap: 0.75rem; }
.stat-icon {
width: 3rem; height: 3rem; display: flex; align-items: center; justify-content: center;
background: var(--accent-gradient); border-radius: 0.5rem; color: white; font-size: 1.25rem;
}
.stat-detail { display: flex; flex-direction: column; }
.stat-value { font-weight: 700; font-size: 1.25rem; color: var(--text-primary); }
.stat-name { font-size: 0.875rem; color: var(--text-secondary); }
.cta-buttons { display: flex; flex-wrap: wrap; gap: 1rem; }
/* Use Astro theme button classes (.cta-button.primary/secondary) if possible */
/* Or adapt these styles */
.btn {
display: inline-flex; align-items: center; justify-content: center; padding: 0.75rem 1.5rem;
font-weight: 600; border-radius: 0.5rem; transition: all var(--transition-normal);
cursor: pointer; position: relative; overflow: hidden; border: none; outline: none; gap: 0.75rem;
text-decoration: none; /* Ensure links look like buttons */
}
.btn-primary { background: var(--accent-gradient); color: white; /* ... other styles */ }
.btn-primary:hover { /* ... hover styles */ }
.btn-outline { background-color: transparent; border: 2px solid var(--accent); color: var(--accent); }
.btn-outline:hover { background-color: var(--accent); color: white; }
.btn-danger { background-color: var(--error); border-color: var(--error); color: white; } /* Use theme error color */
.btn-icon { font-size: 0.9rem; }
.offline-badge, .online-badge { /* Styles from inline block */
display: inline-block; padding: 0.25rem 0.5rem; border-radius: 0.25rem;
font-size: 0.75rem; font-weight: 600; margin-left: 0.5rem;
}
.offline-badge { background-color: #991b1b; color: white; } /* Darker red */
.online-badge { background-color: var(--success); color: white; display: none; } /* Use theme success */
/* Hero Terminal Styles */
.hero-terminal {
width: 100%; border-radius: 0.75rem; overflow: hidden;
box-shadow: var(--card-shadow); /* Use theme shadow */
background-color: #0d1117; /* Keep specific terminal bg */
border: 1px solid var(--border); /* Use theme border */
/* transform: perspective(1000px) rotateY(5deg) rotateX(2deg); */ /* Remove or adjust transform */
transition: all 0.3s ease;
}
/* .hero-terminal:hover { transform: perspective(1000px) rotateY(0deg) rotateX(0deg); } */
.terminal-header { display: flex; align-items: center; background-color: #161b22; padding: 0.5rem 1rem; border-bottom: 1px solid var(--border); }
.terminal-buttons { display: flex; gap: 0.5rem; margin-right: 1rem; }
.terminal-btn { width: 12px; height: 12px; border-radius: 50%; }
.terminal-btn.close { background-color: #ff5f56; }
.terminal-btn.minimize { background-color: #ffbd2e; }
.terminal-btn.maximize { background-color: #27c93f; }
.terminal-title { color: var(--text-secondary); font-size: 0.875rem; font-family: var(--font-mono); }
.terminal-body { padding: 1rem; font-family: var(--font-mono); font-size: 0.875rem; color: var(--text-primary); line-height: 1.4; max-height: 400px; overflow-y: auto; }
.terminal-line { margin-bottom: 0.25rem; white-space: pre-wrap; }
.terminal-line.output { color: var(--text-secondary); }
.terminal-line.success { color: var(--success); } /* Use theme color */
.terminal-line.error { color: var(--error); } /* Use theme color */
.terminal-line.typing { color: var(--accent); } /* Use theme color */
.terminal-line .cursor { display: inline-block; width: 8px; height: 15px; background-color: var(--accent); animation: blink 1s step-end infinite; }
@keyframes blink { 0%, 100% { opacity: 1; } 50% { opacity: 0; } }
/* Architecture Section */
.architecture { /* Use section-padding */ }
.section-header { text-align: center; margin-bottom: 3rem; } /* Adjust margin */
.section-title { font-size: var(--font-size-3xl); margin-bottom: 1rem; position: relative; display: inline-block; color: var(--text-primary); }
.section-title::after { content: ''; position: absolute; bottom: -0.5rem; left: 50%; transform: translateX(-50%); width: 80px; height: 4px; background: var(--accent-gradient); border-radius: 2px; }
.section-description { max-width: 700px; margin: 0 auto; color: var(--text-secondary); font-size: var(--font-size-lg); }
.architecture-diagram-image { display: block; max-width: 800px; margin: 3rem auto; border-radius: 8px; box-shadow: var(--card-shadow); }
.architecture-details { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 2rem; margin-top: 3rem; }
.detail-card { background-color: var(--card-bg); border: 1px solid var(--border); border-radius: 0.75rem; padding: 1.5rem; transition: all var(--transition-normal); }
.detail-card:hover { transform: translateY(-5px); box-shadow: var(--card-shadow); border-color: var(--accent); }
.detail-icon { font-size: 2rem; color: var(--accent); margin-bottom: 1rem; }
.detail-title { font-size: 1.25rem; margin-bottom: 0.75rem; color: var(--text-primary); }
.detail-description { color: var(--text-secondary); font-size: var(--font-size-sm); line-height: 1.6; }
/* Technologies Section */
.technologies { /* Use section-padding alt-bg */ }
.tech-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 2rem; margin-top: 3rem; }
.tech-card { background-color: var(--card-bg); border: 1px solid var(--border); border-radius: 0.75rem; padding: 2rem; transition: all var(--transition-normal); display: flex; flex-direction: column; }
.tech-card:hover { transform: translateY(-10px); box-shadow: var(--card-shadow); border-color: var(--accent); }
.tech-card.featured { background: linear-gradient(145deg, rgba(59, 130, 246, 0.1), rgba(37, 99, 235, 0.05)); border-color: var(--accent); position: relative; overflow: hidden; }
.tech-card.featured::before { content: ''; position: absolute; top: 0; right: 0; width: 100px; height: 100px; background: var(--accent-gradient); transform: rotate(45deg) translate(20px, -60px); opacity: 0.3; }
.tech-icon { font-size: 2.5rem; color: var(--accent); margin-bottom: 1.5rem; transition: all var(--transition-normal); }
.tech-card:hover .tech-icon { transform: scale(1.1); /* text-shadow: var(--accent-glow); */ } /* Glow might be too much */
.tech-title { font-size: 1.5rem; margin-bottom: 1rem; color: var(--text-primary); }
.tech-description { color: var(--text-secondary); margin-bottom: 1.5rem; flex-grow: 1; font-size: var(--font-size-sm); }
.tech-features { display: flex; flex-wrap: wrap; gap: 0.5rem; margin-top: auto; } /* Push to bottom */
.tech-feature { background-color: rgba(59, 130, 246, 0.1); border: 1px solid rgba(59, 130, 246, 0.3); color: var(--accent); font-size: 0.8rem; font-weight: 500; padding: 0.25rem 0.75rem; border-radius: 9999px; }
/* Services Section */
.services { /* Use section-padding */ }
.services-info-banner { display: flex; align-items: center; gap: 1rem; background-color: rgba(59, 130, 246, 0.1); border: 1px solid rgba(59, 130, 246, 0.2); border-radius: 0.75rem; padding: 1rem 1.5rem; margin-bottom: 2rem; }
.info-icon { font-size: 1.5rem; color: var(--accent); }
.services-info-banner p { color: var(--text-secondary); margin: 0; font-size: var(--font-size-sm); }
.services-grid { display: flex; flex-direction: column; gap: 2.5rem; margin-top: 2rem; }
.services-category { background-color: var(--card-bg); border: 1px solid var(--border); border-radius: 0.75rem; overflow: hidden; transition: all var(--transition-normal); }
.services-category:hover { transform: translateY(-5px); box-shadow: var(--card-shadow); border-color: var(--accent); }
.category-title { display: flex; align-items: center; gap: 0.75rem; padding: 1.25rem; background-color: rgba(15, 23, 42, 0.5); border-bottom: 1px solid var(--border); font-size: 1.35rem; color: var(--text-primary); }
.category-icon { color: var(--accent); }
.service-items { display: flex; flex-direction: column; }
.service-item { display: flex; align-items: center; padding: 1.25rem; border-bottom: 1px solid var(--border); transition: all var(--transition-normal); color: var(--text-primary); text-decoration: none; }
.service-item:last-child { border-bottom: none; }
.service-item.available { cursor: pointer; }
.service-item.available:hover { background-color: rgba(15, 23, 42, 0.3); }
.service-item:not(.available) { cursor: default; opacity: 0.6; } /* Dim unavailable */
.service-icon { width: 2.5rem; height: 2.5rem; display: flex; align-items: center; justify-content: center; margin-right: 1rem; background-color: rgba(59, 130, 246, 0.1); border-radius: 0.5rem; font-size: 1.25rem; color: var(--accent); transition: all var(--transition-normal); flex-shrink: 0; }
.service-item.available:hover .service-icon { background-color: rgba(59, 130, 246, 0.2); transform: scale(1.1); }
.service-info { flex: 1; }
.service-name { font-size: 1.1rem; font-weight: 600; margin-bottom: 0.25rem; color: var(--text-primary); }
.service-description { font-size: 0.9rem; color: var(--text-secondary); }
.service-status { display: flex; align-items: center; gap: 0.5rem; font-size: 0.875rem; font-weight: 500; padding: 0.25rem 0.75rem; border-radius: 9999px; margin-left: 1rem; }
.service-status.live { background-color: rgba(16, 185, 129, 0.1); color: var(--success); }
.service-status.maintenance { background-color: rgba(245, 158, 11, 0.1); color: var(--warning); }
.service-status.restricted { background-color: rgba(107, 114, 128, 0.1); color: var(--text-secondary); }
.status-dot { width: 8px; height: 8px; border-radius: 50%; }
.service-status.live .status-dot { background-color: var(--success); }
.service-status.maintenance .status-dot { background-color: var(--warning); }
.service-status.restricted .status-dot { background-color: var(--text-secondary); }
/* Offline service styling from inline */
.offline-service { position: relative; }
.offline-service::before { content: ''; position: absolute; top: 0; left: 0; right: 0; bottom: 0; background-color: rgba(239, 68, 68, 0.1); border: 1px solid rgba(239, 68, 68, 0.3); border-radius: 0.5rem; pointer-events: none; z-index: 1; }
.offline-service .service-icon { color: var(--error); }
.offline-service .service-status.offline .status-dot { background-color: var(--error); box-shadow: 0 0 0 3px rgba(239, 68, 68, 0.2); animation: pulse-red 2s infinite; }
@keyframes pulse-red { 0% { box-shadow: 0 0 0 0 rgba(239, 68, 68, 0.4); } 70% { box-shadow: 0 0 0 6px rgba(239, 68, 68, 0); } 100% { box-shadow: 0 0 0 0 rgba(239, 68, 68, 0); } }
/* Projects Section */
.projects { /* Use section-padding alt-bg */ }
.projects-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 2rem; margin-top: 3rem; }
.project-card { background-color: var(--card-bg); border: 1px solid var(--border); border-radius: 0.75rem; padding: 1.5rem; transition: all var(--transition-normal); display: flex; flex-direction: column; text-decoration: none; } /* Added text-decoration */
.project-card:hover { transform: translateY(-10px); box-shadow: var(--card-shadow); border-color: var(--accent); }
.project-header { display: flex; align-items: center; gap: 1rem; margin-bottom: 1rem; }
.project-icon { font-size: 1.75rem; color: var(--accent); transition: all var(--transition-normal); }
.project-card:hover .project-icon { transform: scale(1.1); }
.project-title { font-size: 1.25rem; margin: 0; color: var(--text-primary); }
.project-description { color: var(--text-secondary); margin-bottom: 1.5rem; flex-grow: 1; font-size: var(--font-size-sm); }
.project-tech { display: flex; flex-wrap: wrap; gap: 0.5rem; margin-bottom: 1.5rem; }
.tech-badge { background-color: rgba(59, 130, 246, 0.1); border: 1px solid rgba(59, 130, 246, 0.3); color: var(--accent); font-size: 0.8rem; font-weight: 500; padding: 0.25rem 0.75rem; border-radius: 9999px; }
.project-cta { margin-top: auto; }
.btn-sm { padding: 0.5rem 1rem; font-size: 0.875rem; } /* Define btn-sm if not global */
/* Dashboards Section */
.dashboards { /* Use section-padding */ }
.dashboard-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 1.5rem; margin-top: 3rem; } /* Use auto-fit */
.dashboard-card { background: var(--card-bg); border-radius: 8px; overflow: hidden; transition: transform 0.3s ease, box-shadow 0.3s ease; border: 1px solid var(--border); text-decoration: none; }
.dashboard-card:hover { transform: translateY(-5px); box-shadow: var(--card-shadow); border-color: var(--accent); }
.dashboard-preview { height: 200px; background-size: cover; background-position: center; position: relative; background-color: var(--secondary-bg); /* Fallback bg */ }
/* Add specific preview backgrounds if needed, or use actual images */
.dashboard-preview.infrastructure { /* background-image: ... */ }
.dashboard-preview.kubernetes { /* background-image: ... */ }
.dashboard-preview.network { /* background-image: ... */ }
.dashboard-preview.services { /* background-image: ... */ }
.dashboard-overlay { position: absolute; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0, 0, 0, 0.7); display: flex; align-items: center; justify-content: center; opacity: 0; transition: opacity 0.3s ease; }
.dashboard-card:hover .dashboard-overlay { opacity: 1; }
.overlay-content { text-align: center; color: #ffffff; }
.overlay-icon { font-size: 2rem; margin-bottom: 0.5rem; }
.overlay-text { font-size: 1rem; opacity: 0.8; }
.dashboard-info { padding: 1.5rem; }
.dashboard-title { font-size: 1.25rem; margin-bottom: 0.5rem; color: var(--text-primary); }
.dashboard-description { font-size: 0.9rem; color: var(--text-secondary); margin-bottom: 1rem; }
.dashboard-cta .btn-sm { padding: 0.5rem 1rem; font-size: 0.9rem; }
/* General Responsive */
.section-padding { padding: 4rem 1rem; } /* Add horizontal padding */
.alt-bg {
/* Use padding instead of negative margins for full bleed */
padding-top: 4rem;
padding-bottom: 4rem;
background-color: var(--bg-secondary);
border-top: 1px solid var(--border-secondary);
border-bottom: 1px solid var(--border-secondary);
}
.alt-bg > .container { /* Ensure container inside alt-bg keeps padding */
padding-left: 1rem;
padding-right: 1rem;
}
@media (max-width: 1024px) {
.hero-content { grid-template-columns: 1fr; text-align: center; }
.hero-text { order: 2; }
.hero-terminal { order: 1; margin: 0 auto 2rem; max-width: 600px; }
.hero-stats { justify-content: center; }
.cta-buttons { justify-content: center; }
}
@media (max-width: 768px) {
.section-padding { padding: 3rem 1rem; }
.alt-bg { padding-top: 3rem; padding-bottom: 3rem; }
.hero { padding-top: 5rem; padding-bottom: 3rem; min-height: auto; }
.hero-title { font-size: 2.25rem; }
.section-title { font-size: var(--font-size-2xl); }
.section-header { flex-direction: column; align-items: center; gap: 0.5rem; margin-bottom: 2rem; }
.section-description { font-size: var(--font-size-base); }
.tech-grid, .projects-grid, .dashboard-grid, .architecture-details { grid-template-columns: 1fr; }
.services-grid { gap: 1.5rem; }
.service-item { flex-direction: column; align-items: flex-start; }
.service-status { margin-left: 0; margin-top: 0.5rem; }
}
</style>
<script>
// Adapted from script.js
// --- Particle Animation ---
function createBackgroundParticles(count = 30) { // Reduced default count
const particlesContainer = document.getElementById('particles-container');
if (!particlesContainer) return;
particlesContainer.innerHTML = ''; // Clear existing
for (let i = 0; i < count; i++) {
const particle = document.createElement('div');
particle.classList.add('particle');
const size = Math.random() * 3 + 1; // Smaller size
particle.style.width = `${size}px`;
particle.style.height = `${size}px`;
particle.style.left = `${Math.random() * 100}%`;
particle.style.top = `${Math.random() * 100}%`;
particle.style.opacity = (Math.random() * 0.2 + 0.05).toString(); // More subtle
particle.style.animation = `float-particle ${Math.random() * 25 + 15}s linear infinite`;
particle.style.animationDelay = `${Math.random() * 15}s`;
particlesContainer.appendChild(particle);
}
}
// --- Terminal Cursor ---
function initTerminalTyping() {
const cursor = document.querySelector('.hero-terminal .cursor');
if (cursor) {
setInterval(() => {
cursor.style.opacity = cursor.style.opacity === '0' ? '1' : '0';
}, 600);
}
}
// --- Status Checks (Placeholder - Requires actual API endpoints) ---
function checkAnsibleSandboxStatus() {
const btn = document.getElementById('ansible-sandbox-btn');
const badge = btn?.querySelector('.offline-badge');
if (!btn || !badge) return;
// Replace with actual API call
const isOnline = false; // Simulate offline for now
if (isOnline) {
btn.classList.remove('btn-danger');
btn.classList.add('btn-primary', 'pulse'); // Add pulse if desired
badge.textContent = 'Online';
badge.classList.remove('offline-badge');
badge.classList.add('online-badge');
badge.style.display = 'inline-block';
} else {
btn.classList.remove('btn-primary', 'pulse');
btn.classList.add('btn-danger');
badge.textContent = 'Offline';
badge.classList.remove('online-badge');
badge.classList.add('offline-badge');
badge.style.display = 'inline-block'; // Ensure it's shown
}
}
function checkLiveDashboardStatus() {
const indicator = document.querySelector('.dashboard-link .live-indicator');
if (!indicator) return;
// Replace with actual API call
const isOnline = false; // Simulate offline
if (isOnline) {
indicator.classList.remove('offline');
} else {
indicator.classList.add('offline');
}
}
// --- Initialization ---
document.addEventListener('DOMContentLoaded', function() {
if (document.getElementById('particles-container')) {
createBackgroundParticles();
// Optional: Add resize listener if particle count should change
// window.addEventListener('resize', () => createBackgroundParticles(window.innerWidth <= 768 ? 15 : 30));
}
if (document.querySelector('.hero-terminal .cursor')) {
initTerminalTyping();
}
// Initial status checks
checkAnsibleSandboxStatus();
checkLiveDashboardStatus();
// Optional: Set interval to re-check status periodically
// setInterval(checkAnsibleSandboxStatus, 60000); // Check every minute
// setInterval(checkLiveDashboardStatus, 60000);
});
// --- Contact Form Submission (If form is included on this page) ---
/*
const contactForm = document.getElementById('contact-form');
if (contactForm) {
contactForm.addEventListener('submit', async (e) => {
e.preventDefault();
const submitButton = contactForm.querySelector('button[type="submit"]');
const originalButtonText = submitButton.innerHTML;
const formMessages = contactForm.querySelector('.form-messages') || document.createElement('div'); // Add a div for messages if needed
formMessages.className = 'form-messages';
if (!contactForm.contains(formMessages)) {
contactForm.insertBefore(formMessages, submitButton);
}
formMessages.innerHTML = ''; // Clear previous messages
try {
submitButton.disabled = true;
submitButton.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Sending...';
const formData = new FormData(contactForm);
const data = Object.fromEntries(formData.entries());
const response = await fetch('/api/contact', { // Use the Astro API route
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
const result = await response.json();
if (response.ok) {
formMessages.innerHTML = `<div class="alert alert-success"><i class="fas fa-check-circle"></i> ${result.message}</div>`;
contactForm.reset();
} else {
throw new Error(result.message || 'Failed to send message');
}
} catch (error) {
console.error('Error sending contact form:', error);
formMessages.innerHTML = `<div class="alert alert-error"><i class="fas fa-exclamation-circle"></i> ${error.message}</div>`;
} finally {
submitButton.disabled = false;
submitButton.innerHTML = originalButtonText;
setTimeout(() => { formMessages.innerHTML = ''; }, 5000); // Clear message after 5s
}
});
}
*/
</script>

1067
src/pages/homelab.html Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

620
src/pages/projects.astro Normal file
View File

@ -0,0 +1,620 @@
---
// src/pages/projects.astro
import BaseLayout from '../layouts/BaseLayout.astro';
import '../styles/card-animations.css';
import Header from '../components/Header.astro';
import Footer from '../components/Footer.astro';
const title = "Projects | LaForceIT";
const description = "Explore Daniel LaForce's projects, including infrastructure solutions, automation tools, and home lab configurations.";
// Category icons
const categoryIcons = {
"Infrastructure": `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="2" width="20" height="8" rx="2" ry="2"></rect><rect x="2" y="14" width="20" height="8" rx="2" ry="2"></rect><line x1="6" y1="6" x2="6.01" y2="6"></line><line x1="6" y1="18" x2="6.01" y2="18"></line></svg>`,
"DevOps": `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="16 18 22 12 16 6"></polyline><polyline points="8 6 2 12 8 18"></polyline><line x1="19" y1="12" x2="5" y2="12"></line></svg>`,
"Web Development": `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="16 18 22 12 16 6"></polyline><polyline points="8 6 2 12 8 18"></polyline></svg>`,
"Documentation": `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" 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>`,
"Automation": `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M18 20V10"></path><path d="M12 20V4"></path><path d="M6 20v-6"></path><path d="M18 14a2 2 0 1 0 0-4 2 2 0 0 0 0 4z"></path><path d="M12 8a2 2 0 1 0 0-4 2 2 0 0 0 0 4z"></path><path d="M6 18a2 2 0 1 0 0-4 2 2 0 0 0 0 4z"></path></svg>`
};
// Featured projects
const featuredProjects = [
{
title: "Home Lab Infrastructure",
description: "Enterprise-grade home lab environment with Kubernetes, virtualization, networking, and storage solutions.",
image: "/images/projects/homelab.jpg",
url: "https://argobox.com",
isExternal: true,
technologies: ["Proxmox", "Kubernetes", "pfSense", "TrueNAS"],
category: "Infrastructure"
},
{
title: "Tech Stack",
description: "A detailed overview of the technologies, tools, and platforms that power my projects and home lab environment.",
image: "/images/projects/tech-stack.jpg",
url: "/tech-stack",
isExternal: false,
technologies: ["Kubernetes", "Docker", "Terraform", "Ansible"],
category: "Documentation"
},
{
title: "Infrastructure as Code Templates",
description: "Collection of Terraform and Ansible templates for automating infrastructure deployment.",
image: "/images/projects/iac.jpg",
url: "/projects/infrastructure-templates",
isExternal: false,
technologies: ["Terraform", "Ansible", "AWS", "Azure"],
category: "DevOps"
},
{
title: "Kubernetes Management Dashboard",
description: "Custom web interface for monitoring and managing Kubernetes clusters with enhanced visualization.",
image: "/images/projects/k8s-dashboard.jpg",
url: "/projects/kubernetes-dashboard",
isExternal: false,
technologies: ["React", "Node.js", "Kubernetes API", "Prometheus"],
category: "Web Development"
}
];
// Project categories
const categories = [
"All",
"Infrastructure",
"DevOps",
"Web Development",
"Documentation",
"Automation"
];
---
<BaseLayout title={title} description={description}>
<Header slot="header" />
<div class="container">
<div class="page-header">
<h1 class="page-title">Projects</h1>
<p class="page-description">
Explore my projects focused on infrastructure automation, DevOps solutions, and home lab configurations. These projects demonstrate practical applications of modern technologies in real-world scenarios.
</p>
</div>
<!-- Search & Filters -->
<div class="resource-search">
<div class="search-icon">
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="11" cy="11" r="8"></circle>
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
</svg>
</div>
<input type="text" placeholder="Search projects..." class="search-input" id="project-search">
</div>
<div class="resource-filters">
{categories.map(category => (
<button class={`filter-button ${category === 'All' ? 'active' : ''}`} data-filter={category.toLowerCase().replace(' ', '-')}>
{category}
</button>
))}
</div>
<!-- Projects Grid -->
<div class="resources-grid">
{featuredProjects.map(project => (
<div class="resource-card" data-category={project.category.toLowerCase().replace(' ', '-')}>
<div class="resource-icon-wrapper">
<div class="resource-icon" set:html={categoryIcons[project.category]}></div>
<div class="resource-icon-particles">
<div class="resource-icon-particle"></div>
<div class="resource-icon-particle"></div>
<div class="resource-icon-particle"></div>
<div class="resource-icon-particle"></div>
<div class="resource-icon-particle"></div>
</div>
{/* Add animated dots in the background of each icon area */}
<div class="icon-bg-dot" style={{top: '20%', left: '80%'}}></div>
<div class="icon-bg-dot" style={{top: '70%', left: '15%'}}></div>
<div class="icon-bg-dot" style={{top: '30%', left: '30%'}}></div>
<div class="icon-bg-dot" style={{top: '60%', left: '70%'}}></div>
<div class="icon-bg-dot" style={{top: '10%', left: '50%'}}></div>
</div>
<div class="resource-content">
<h2 class="resource-title">{project.title}</h2>
<p class="resource-description">{project.description}</p>
<a href={project.url} class="resource-link" target={project.isExternal ? "_blank" : undefined} rel={project.isExternal ? "noopener noreferrer" : undefined}>
{project.isExternal ? 'Visit Project' : 'View Project'}
<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="5" y1="12" x2="19" y2="12"></line>
<polyline points="12 5 19 12 12 19"></polyline>
</svg>
</a>
<div class="resource-tags">
{project.technologies.map(tech => (
<span class="resource-tag">{tech}</span>
))}
</div>
</div>
<div class="shine-effect"></div>
</div>
))}
<!-- Empty State (hidden by default) -->
<div class="no-results" style="display: none;">
<div class="no-results-icon">
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1" stroke-linecap="round" stroke-linejoin="round">
<circle cx="11" cy="11" r="8"></circle>
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
<line x1="11" y1="8" x2="11" y2="14"></line>
<line x1="8" y1="11" x2="14" y2="11"></line>
</svg>
</div>
<h3>No matching projects found</h3>
<p>Try adjusting your search terms or filters to find what you're looking for.</p>
</div>
</div>
<div class="github-section">
<div class="github-header">
<div class="github-icon-wrapper">
<svg xmlns="http://www.w3.org/2000/svg" width="36" height="36" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M9 19c-5 1.5-5-2.5-7-3m14 6v-3.87a3.37 3.37 0 0 0-.94-2.61c3.14-.35 6.44-1.54 6.44-7A5.44 5.44 0 0 0 20 4.77 5.07 5.07 0 0 0 19.91 1S18.73.65 16 2.48a13.38 13.38 0 0 0-7 0C6.27.65 5.09 1 5.09 1A5.07 5.07 0 0 0 5 4.77a5.44 5.44 0 0 0-1.5 3.78c0 5.42 3.3 6.61 6.44 7A3.37 3.37 0 0 0 9 18.13V22"></path>
</svg>
<div class="github-icon-particles">
<div class="github-icon-particle"></div>
<div class="github-icon-particle"></div>
<div class="github-icon-particle"></div>
<div class="github-icon-particle"></div>
</div>
</div>
<h2 class="section-title">GitHub Repositories</h2>
<a href="/projects/github" class="view-all">
View All
<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">
<polyline points="9 18 15 12 9 6"></polyline>
</svg>
</a>
</div>
<div class="github-content">
<p class="github-message">
Explore my open-source projects and contributions on GitHub. These repositories include infrastructure configurations, automation scripts, and development tools.
</p>
<a href="/projects/github" class="github-cta">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M9 19c-5 1.5-5-2.5-7-3m14 6v-3.87a3.37 3.37 0 0 0-.94-2.61c3.14-.35 6.44-1.54 6.44-7A5.44 5.44 0 0 0 20 4.77 5.07 5.07 0 0 0 19.91 1S18.73.65 16 2.48a13.38 13.38 0 0 0-7 0C6.27.65 5.09 1 5.09 1A5.07 5.07 0 0 0 5 4.77a5.44 5.44 0 0 0-1.5 3.78c0 5.42 3.3 6.61 6.44 7A3.37 3.37 0 0 0 9 18.13V22"></path>
</svg>
Browse Repositories
</a>
</div>
</div>
</div>
<Footer slot="footer" />
</BaseLayout>
<style>
.container {
max-width: 1280px;
margin: 0 auto;
padding: 0 var(--container-padding, 1.5rem);
}
.page-header {
text-align: center;
margin-bottom: 3rem;
}
.page-title {
font-size: 3rem;
font-weight: 700;
margin-bottom: 1rem;
background: linear-gradient(to right, var(--accent-primary), var(--accent-secondary));
-webkit-background-clip: text;
background-clip: text;
color: transparent;
position: relative;
display: inline-block;
}
.page-title::after {
content: '';
position: absolute;
bottom: -10px;
left: 50%;
transform: translateX(-50%);
width: 100px;
height: 3px;
background: linear-gradient(to right, var(--accent-primary), var(--accent-secondary));
border-radius: 3px;
}
.page-description {
max-width: 800px;
margin: 0 auto;
color: var(--text-secondary);
font-size: 1.1rem;
}
/* Resource Filtering */
.resource-filters {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 0.75rem;
margin: 2rem 0;
}
.filter-button {
padding: 0.5rem 1.25rem;
background: var(--bg-tertiary);
border: 1px solid var(--border-primary);
color: var(--text-secondary);
border-radius: 20px;
font-size: 0.9rem;
cursor: pointer;
transition: all 0.3s ease;
}
.filter-button:hover {
background: linear-gradient(90deg, rgba(6, 182, 212, 0.1), rgba(59, 130, 246, 0.1));
color: var(--text-primary);
border-color: var(--accent-primary);
box-shadow: 0 0 10px var(--glow-primary);
}
.filter-button.active {
background: linear-gradient(90deg, rgba(6, 182, 212, 0.2), rgba(59, 130, 246, 0.2));
color: var(--accent-primary);
border-color: var(--accent-primary);
box-shadow: 0 0 15px var(--glow-primary);
}
/* Search Box */
.resource-search {
display: flex;
margin: 0 auto 2rem;
max-width: 500px;
position: relative;
}
.search-input {
width: 100%;
padding: 0.75rem 1rem 0.75rem 2.75rem;
background: var(--bg-tertiary);
border: 1px solid var(--border-primary);
border-radius: 8px;
color: var(--text-primary);
font-size: 1rem;
transition: all 0.3s ease;
}
.search-input:focus {
outline: none;
border-color: var(--accent-primary);
box-shadow: 0 0 0 3px var(--glow-primary);
}
.search-icon {
position: absolute;
left: 1rem;
top: 50%;
transform: translateY(-50%);
color: var(--text-tertiary);
}
/* Resources Grid */
.resources-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
gap: 2rem;
margin-top: 1rem;
margin-bottom: 4rem;
}
/* Styles for .resource-card, .resource-icon-wrapper, etc. are now in card-animations.css */
.resource-content {
padding: 1.5rem;
flex: 1;
display: flex;
flex-direction: column;
}
.resource-title {
font-size: 1.3rem;
font-weight: 600;
margin-bottom: 0.75rem;
color: var(--text-primary);
}
.resource-description {
color: var(--text-secondary);
margin-bottom: 1.5rem;
flex: 1;
}
.resource-link {
display: inline-flex;
align-items: center;
text-decoration: none;
color: var(--accent-primary);
font-weight: 500;
transition: all 0.3s ease;
position: relative;
align-self: flex-start;
}
.resource-link::after {
content: '';
position: absolute;
bottom: -2px;
left: 0;
width: 100%;
height: 1px;
background: var(--accent-primary);
transform: scaleX(0);
transform-origin: right;
transition: transform 0.3s ease;
}
.resource-link:hover {
color: var(--accent-secondary);
}
.resource-link:hover::after {
transform: scaleX(1);
transform-origin: left;
}
.resource-link svg {
margin-left: 0.5rem;
transition: transform 0.3s ease;
}
.resource-link:hover svg {
transform: translateX(3px);
}
.resource-tags {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
margin-top: 1rem;
}
/* .resource-tag styling is now in card-animations.css */
/* Background dots styling is now in card-animations.css */
/* Shine effect styling is now in card-animations.css */
/* Empty State */
.no-results {
text-align: center;
padding: 3rem;
color: var(--text-secondary);
background: var(--bg-tertiary);
border-radius: 12px;
border: 1px dashed var(--border-primary);
margin-top: 2rem;
display: none; /* Hidden by default */
grid-column: 1 / -1; /* Span all columns */
}
.no-results-icon {
color: var(--text-tertiary);
margin-bottom: 1rem;
}
.no-results-icon svg {
width: 64px;
height: 64px;
stroke-width: 1;
opacity: 0.5;
}
/* GitHub Section (kept from original) */
.github-section {
background: var(--card-bg);
border: 1px solid var(--border-primary);
border-radius: 16px;
padding: 3rem;
margin: 2rem 0 4rem;
position: relative;
overflow: hidden;
}
.github-section::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: radial-gradient(circle at 30% 50%, rgba(6, 182, 212, 0.1), transparent 70%);
pointer-events: none;
}
.github-header {
display: flex;
align-items: center;
gap: 1rem;
margin-bottom: 2rem;
position: relative;
}
.github-icon-wrapper {
position: relative;
width: 64px;
height: 64px;
border-radius: 12px;
background: rgba(0, 0, 0, 0.2);
display: flex;
align-items: center;
justify-content: center;
color: var(--text-primary);
overflow: hidden;
}
.github-icon-particles {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
}
.github-icon-particle {
position: absolute;
width: 4px;
height: 4px;
border-radius: 50%;
background: var(--accent-primary);
opacity: 0.5;
animation: github-particle 3s infinite ease-in-out;
}
.github-icon-particle:nth-child(1) { animation-delay: 0s; }
.github-icon-particle:nth-child(2) { animation-delay: 0.5s; }
.github-icon-particle:nth-child(3) { animation-delay: 1s; }
.github-icon-particle:nth-child(4) { animation-delay: 1.5s; }
@keyframes github-particle {
0% { transform: translate(0, 0); opacity: 0; }
25% { opacity: 0.5; }
50% { transform: translate(20px, -20px); opacity: 0.8; }
75% { opacity: 0.5; }
100% { transform: translate(40px, -40px); opacity: 0; }
}
.section-title {
font-size: 2rem;
color: var(--text-primary);
margin: 0;
position: relative;
flex: 1;
}
.view-all {
display: flex;
align-items: center;
gap: 0.5rem;
color: var(--accent-primary);
font-size: var(--font-size-sm);
text-decoration: none;
transition: all 0.3s ease;
position: relative;
}
.view-all::after {
content: '';
position: absolute;
bottom: -2px;
left: 0;
width: 100%;
height: 1px;
background: var(--accent-primary);
transform: scaleX(0);
transform-origin: right;
transition: transform 0.3s ease;
}
.view-all:hover {
color: var(--accent-secondary);
transform: translateX(3px);
}
.view-all:hover::after {
transform: scaleX(1);
transform-origin: left;
}
.view-all svg {
transition: transform 0.3s ease;
}
.view-all:hover svg {
transform: translateX(3px);
}
.github-content {
display: flex;
justify-content: space-between;
align-items: center;
gap: 2rem;
}
.github-message {
color: var(--text-secondary);
font-size: var(--font-size-lg);
line-height: 1.7;
max-width: 70%;
margin: 0;
}
.github-cta {
display: flex;
align-items: center;
gap: 0.75rem;
background: #24292e;
color: white;
padding: 0.75rem 1.25rem;
border-radius: 8px;
text-decoration: none;
font-weight: 500;
transition: all 0.3s ease;
white-space: nowrap;
position: relative;
overflow: hidden;
}
.github-cta::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(135deg, rgba(255, 255, 255, 0.1), transparent);
opacity: 0;
transition: opacity 0.3s ease;
}
.github-cta:hover {
transform: translateY(-3px);
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.2);
}
.github-cta:hover::before {
opacity: 1;
}
/* Keyframes are now in card-animations.css */
@media (max-width: 768px) {
.page-title {
font-size: 2.5rem;
}
.github-section {
padding: 2rem 1.5rem;
}
.github-content {
flex-direction: column;
text-align: center;
gap: 1.5rem;
}
.github-message {
max-width: 100%;
}
.github-header {
flex-direction: column;
gap: 1rem;
text-align: center;
}
}

View File

@ -0,0 +1,192 @@
---
// src/pages/projects/github.astro
import BaseLayout from '../../layouts/BaseLayout.astro';
import Header from '../../components/Header.astro';
import Footer from '../../components/Footer.astro';
const title = "GitHub Repositories | LaForceIT";
const description = "Explore Daniel LaForce's GitHub projects, infrastructure code, automation scripts, and open-source contributions.";
---
<BaseLayout title={title} description={description}>
<Header slot="header" />
<div class="container">
<div class="page-header">
<h1>GitHub Repositories</h1>
<div class="header-accent"></div>
</div>
<div class="coming-soon-container">
<div class="coming-soon-card">
<div class="icon-container">
<svg xmlns="http://www.w3.org/2000/svg" width="60" height="60" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M9 19c-5 1.5-5-2.5-7-3m14 6v-3.87a3.37 3.37 0 0 0-.94-2.61c3.14-.35 6.44-1.54 6.44-7A5.44 5.44 0 0 0 20 4.77 5.07 5.07 0 0 0 19.91 1S18.73.65 16 2.48a13.38 13.38 0 0 0-7 0C6.27.65 5.09 1 5.09 1A5.07 5.07 0 0 0 5 4.77a5.44 5.44 0 0 0-1.5 3.78c0 5.42 3.3 6.61 6.44 7A3.37 3.37 0 0 0 9 18.13V22"></path>
</svg>
</div>
<h2>Coming Soon</h2>
<p class="description">
This section will showcase my public GitHub repositories, infrastructure-as-code projects, automation scripts, and open-source contributions. You'll be able to browse repositories by category, see detailed descriptions, and directly access the code.
</p>
<div class="features">
<div class="feature-item">
<span class="feature-icon">📊</span>
<span class="feature-text">DevOps Repositories</span>
</div>
<div class="feature-item">
<span class="feature-icon">🔄</span>
<span class="feature-text">CI/CD Pipelines</span>
</div>
<div class="feature-item">
<span class="feature-icon">🛠️</span>
<span class="feature-text">Infrastructure Tools</span>
</div>
<div class="feature-item">
<span class="feature-icon">🧩</span>
<span class="feature-text">Open Source Contributions</span>
</div>
</div>
</div>
</div>
</div>
<Footer slot="footer" />
</BaseLayout>
<style>
.page-header {
margin: 3rem 0 4rem;
position: relative;
}
h1 {
font-size: var(--font-size-4xl);
background: linear-gradient(90deg, var(--accent-primary), var(--accent-secondary));
-webkit-background-clip: text;
background-clip: text;
color: transparent;
display: inline-block;
margin-bottom: 0.5rem;
}
.header-accent {
width: 80px;
height: 4px;
background: linear-gradient(90deg, var(--accent-primary), var(--accent-secondary));
border-radius: 2px;
}
.coming-soon-container {
display: flex;
justify-content: center;
padding: 2rem 0 4rem;
}
.coming-soon-card {
background: var(--card-bg);
border: 1px solid var(--card-border);
border-radius: 16px;
padding: 2.5rem;
max-width: 800px;
width: 100%;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
position: relative;
overflow: hidden;
animation: cardPulse 4s infinite alternate ease-in-out;
}
@keyframes cardPulse {
0% {
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
border-color: var(--card-border);
}
100% {
box-shadow: 0 15px 40px rgba(6, 182, 212, 0.15);
border-color: rgba(6, 182, 212, 0.4);
}
}
.coming-soon-card::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: radial-gradient(circle at 50% 0%, rgba(6, 182, 212, 0.1), transparent 70%);
pointer-events: none;
}
.icon-container {
display: flex;
justify-content: center;
margin-bottom: 1.5rem;
color: var(--accent-primary);
}
h2 {
text-align: center;
margin-bottom: 1.5rem;
font-size: var(--font-size-3xl);
color: var(--text-primary);
}
.description {
text-align: center;
margin-bottom: 2.5rem;
color: var(--text-secondary);
font-size: var(--font-size-lg);
line-height: 1.7;
}
.features {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 1.5rem;
margin-top: 2rem;
}
.feature-item {
display: flex;
align-items: center;
background: rgba(30, 41, 59, 0.5);
padding: 1rem;
border-radius: 8px;
border: 1px solid var(--border-secondary);
transition: all 0.3s ease;
}
.feature-item:hover {
transform: translateY(-3px);
background: rgba(30, 41, 59, 0.8);
border-color: var(--accent-primary);
}
.feature-icon {
font-size: 1.5rem;
margin-right: 0.75rem;
}
.feature-text {
color: var(--text-secondary);
font-size: var(--font-size-md);
}
@media (max-width: 768px) {
.features {
grid-template-columns: 1fr;
}
.coming-soon-card {
padding: 2rem 1.5rem;
}
h1 {
font-size: var(--font-size-3xl);
}
h2 {
font-size: var(--font-size-2xl);
}
}
</style>

View File

@ -0,0 +1,237 @@
---
// src/pages/projects/obsidian.astro
import BaseLayout from '../../layouts/BaseLayout.astro';
import Header from '../../components/Header.astro';
import Footer from '../../components/Footer.astro';
const title = "Obsidian Templates | LaForceIT";
const description = "A collection of Obsidian templates for DevOps and IT professionals to better organize their technical knowledge.";
---
<BaseLayout title={title} description={description}>
<Header slot="header" />
<div class="container">
<div class="page-header">
<h1>Obsidian Templates</h1>
<div class="header-accent"></div>
</div>
<div class="coming-soon-container">
<div class="coming-soon-card">
<div class="icon-container">
<svg xmlns="http://www.w3.org/2000/svg" width="60" height="60" viewBox="0 0 100 100" fill="none" stroke="currentColor" stroke-width="4" stroke-linecap="round" stroke-linejoin="round">
<path d="M50 6.25L6.25 34.375V65.625L50 93.75L93.75 65.625V34.375L50 6.25Z" />
<path d="M50 65.625L6.25 34.375" />
<path d="M50 65.625L93.75 34.375" />
<path d="M50 6.25V37.5" />
</svg>
</div>
<h2>Obsidian Templates Coming Soon</h2>
<p class="description">
This section will feature curated Obsidian templates designed specifically for IT professionals, DevOps engineers, and tech enthusiasts. These templates will help you better organize your technical knowledge, document your infrastructure, and streamline your note-taking process.
</p>
<div class="template-cards">
<div class="template-card">
<div class="template-icon">📋</div>
<h3>Project Documentation</h3>
<p>Structured templates for documenting technical projects and infrastructure.</p>
</div>
<div class="template-card">
<div class="template-icon">🔄</div>
<h3>Runbooks & Procedures</h3>
<p>Templates for creating operational runbooks and standard procedures.</p>
</div>
<div class="template-card">
<div class="template-icon">🧠</div>
<h3>Technical Notes</h3>
<p>Organized formats for technical notes with code blocks and references.</p>
</div>
<div class="template-card">
<div class="template-icon">📊</div>
<h3>Dashboard Layouts</h3>
<p>Custom dashboard layouts for monitoring your technical knowledge base.</p>
</div>
</div>
<div class="cta-button-container">
<button class="cta-button">
<span class="button-text">Notify Me When Available</span>
<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="5" y1="12" x2="19" y2="12"></line>
<polyline points="12 5 19 12 12 19"></polyline>
</svg>
</button>
</div>
</div>
</div>
</div>
<Footer slot="footer" />
</BaseLayout>
<style>
.page-header {
margin: 3rem 0 4rem;
position: relative;
}
h1 {
font-size: var(--font-size-4xl);
background: linear-gradient(90deg, var(--accent-primary), var(--accent-tertiary));
-webkit-background-clip: text;
background-clip: text;
color: transparent;
display: inline-block;
margin-bottom: 0.5rem;
}
.header-accent {
width: 80px;
height: 4px;
background: linear-gradient(90deg, var(--accent-primary), var(--accent-tertiary));
border-radius: 2px;
}
.coming-soon-container {
display: flex;
justify-content: center;
padding: 2rem 0 4rem;
}
.coming-soon-card {
background: var(--card-bg);
border: 1px solid var(--card-border);
border-radius: 16px;
padding: 2.5rem;
max-width: 800px;
width: 100%;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
position: relative;
overflow: hidden;
}
.coming-soon-card::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: radial-gradient(circle at 50% 50%, rgba(139, 92, 246, 0.1), transparent 70%);
pointer-events: none;
}
.icon-container {
display: flex;
justify-content: center;
margin-bottom: 1.5rem;
color: var(--accent-tertiary);
}
h2 {
text-align: center;
margin-bottom: 1.5rem;
font-size: var(--font-size-3xl);
color: var(--text-primary);
}
.description {
text-align: center;
margin-bottom: 2.5rem;
color: var(--text-secondary);
font-size: var(--font-size-lg);
line-height: 1.7;
}
.template-cards {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 1.5rem;
margin-bottom: 2.5rem;
}
.template-card {
background: rgba(30, 41, 59, 0.5);
border: 1px solid var(--border-secondary);
border-radius: 12px;
padding: 1.5rem;
transition: all 0.3s ease;
}
.template-card:hover {
transform: translateY(-5px);
box-shadow: 0 10px 20px rgba(139, 92, 246, 0.1);
border-color: var(--accent-tertiary);
}
.template-icon {
font-size: 2rem;
margin-bottom: 1rem;
}
.template-card h3 {
font-size: var(--font-size-lg);
margin-bottom: 0.75rem;
color: var(--text-primary);
}
.template-card p {
color: var(--text-tertiary);
font-size: var(--font-size-sm);
line-height: 1.6;
}
.cta-button-container {
display: flex;
justify-content: center;
margin-top: 1rem;
}
.cta-button {
display: flex;
align-items: center;
gap: 0.75rem;
background: linear-gradient(90deg, var(--accent-primary), var(--accent-tertiary));
color: var(--bg-primary);
border: none;
padding: 0.75rem 1.5rem;
border-radius: 8px;
font-weight: 500;
font-size: var(--font-size-md);
cursor: pointer;
transition: all 0.3s ease;
box-shadow: 0 5px 15px rgba(139, 92, 246, 0.2);
}
.cta-button:hover {
transform: translateY(-2px);
box-shadow: 0 8px 20px rgba(139, 92, 246, 0.3);
}
.button-text {
font-weight: 500;
}
@media (max-width: 768px) {
.template-cards {
grid-template-columns: 1fr;
}
.coming-soon-card {
padding: 2rem 1.5rem;
}
h1 {
font-size: var(--font-size-3xl);
}
h2 {
font-size: var(--font-size-2xl);
}
}
</style>

View File

@ -0,0 +1,292 @@
---
// src/pages/projects/services.astro
import BaseLayout from '../../layouts/BaseLayout.astro';
import Header from '../../components/Header.astro';
import Footer from '../../components/Footer.astro';
const title = "Live Services | LaForceIT";
const description = "Explore the live services running in Daniel LaForce's home lab infrastructure environment.";
---
<BaseLayout title={title} description={description}>
<Header slot="header" />
<div class="container">
<div class="page-header">
<h1>Live Services</h1>
<div class="header-accent"></div>
</div>
<div class="coming-soon-container">
<div class="coming-soon-card">
<div class="icon-container">
<svg xmlns="http://www.w3.org/2000/svg" width="60" height="60" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<rect x="2" y="2" width="20" height="8" rx="2" ry="2"></rect>
<rect x="2" y="14" width="20" height="8" rx="2" ry="2"></rect>
<line x1="6" y1="6" x2="6.01" y2="6"></line>
<line x1="6" y1="18" x2="6.01" y2="18"></line>
</svg>
</div>
<h2>Services Dashboard Coming Soon</h2>
<p class="description">
This page will provide real-time status and information about services running in my home lab environment. You'll be able to see uptime statistics, resource usage, and detailed descriptions of each service's purpose and technology stack.
</p>
<div class="service-preview">
<div class="service-preview-header">
<h3>Services Preview</h3>
<div class="preview-pulse"></div>
</div>
<div class="service-list">
<div class="service-item">
<div class="service-status active"></div>
<div class="service-info">
<span class="service-name">ArgoBox</span>
<span class="service-description">Kubernetes Dashboard &amp; GitOps</span>
</div>
<div class="service-action">Coming Soon</div>
</div>
<div class="service-item">
<div class="service-status active"></div>
<div class="service-info">
<span class="service-name">Git Server</span>
<span class="service-description">Self-hosted Git repositories</span>
</div>
<div class="service-action">Coming Soon</div>
</div>
<div class="service-item">
<div class="service-status active"></div>
<div class="service-info">
<span class="service-name">Kubernetes Cluster</span>
<span class="service-description">Container orchestration</span>
</div>
<div class="service-action">Coming Soon</div>
</div>
<div class="service-item">
<div class="service-status active"></div>
<div class="service-info">
<span class="service-name">Media Server</span>
<span class="service-description">Media streaming solution</span>
</div>
<div class="service-action">Coming Soon</div>
</div>
</div>
</div>
</div>
</div>
</div>
<Footer slot="footer" />
</BaseLayout>
<style>
.page-header {
margin: 3rem 0 4rem;
position: relative;
}
h1 {
font-size: var(--font-size-4xl);
background: linear-gradient(90deg, var(--accent-primary), var(--accent-secondary));
-webkit-background-clip: text;
background-clip: text;
color: transparent;
display: inline-block;
margin-bottom: 0.5rem;
}
.header-accent {
width: 80px;
height: 4px;
background: linear-gradient(90deg, var(--accent-primary), var(--accent-secondary));
border-radius: 2px;
}
.coming-soon-container {
display: flex;
justify-content: center;
padding: 2rem 0 4rem;
}
.coming-soon-card {
background: var(--card-bg);
border: 1px solid var(--card-border);
border-radius: 16px;
padding: 2.5rem;
max-width: 800px;
width: 100%;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
position: relative;
overflow: hidden;
}
.coming-soon-card::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: radial-gradient(circle at 50% 0%, rgba(6, 182, 212, 0.1), transparent 70%);
pointer-events: none;
}
.icon-container {
display: flex;
justify-content: center;
margin-bottom: 1.5rem;
color: var(--accent-primary);
}
h2 {
text-align: center;
margin-bottom: 1.5rem;
font-size: var(--font-size-3xl);
color: var(--text-primary);
}
.description {
text-align: center;
margin-bottom: 2.5rem;
color: var(--text-secondary);
font-size: var(--font-size-lg);
line-height: 1.7;
}
.service-preview {
margin-top: 2rem;
border: 1px solid var(--border-primary);
border-radius: 12px;
overflow: hidden;
}
.service-preview-header {
background: rgba(30, 41, 59, 0.7);
padding: 1rem 1.5rem;
display: flex;
justify-content: space-between;
align-items: center;
}
h3 {
color: var(--text-primary);
font-size: var(--font-size-lg);
margin: 0;
}
.preview-pulse {
width: 12px;
height: 12px;
border-radius: 50%;
background: #10b981;
position: relative;
}
.preview-pulse::after {
content: '';
position: absolute;
width: 100%;
height: 100%;
border-radius: 50%;
background: #10b981;
animation: pulse 2s infinite;
}
@keyframes pulse {
0% {
transform: scale(1);
opacity: 0.7;
}
70% {
transform: scale(2);
opacity: 0;
}
100% {
transform: scale(1);
opacity: 0;
}
}
.service-list {
background: rgba(15, 23, 42, 0.5);
}
.service-item {
display: flex;
align-items: center;
padding: 1rem 1.5rem;
border-bottom: 1px solid var(--border-secondary);
}
.service-item:last-child {
border-bottom: none;
}
.service-status {
width: 10px;
height: 10px;
border-radius: 50%;
margin-right: 1rem;
}
.service-status.active {
background: #10b981;
box-shadow: 0 0 10px rgba(16, 185, 129, 0.5);
}
.service-status.warning {
background: #f59e0b;
box-shadow: 0 0 10px rgba(245, 158, 11, 0.5);
}
.service-status.offline {
background: #ef4444;
box-shadow: 0 0 10px rgba(239, 68, 68, 0.5);
}
.service-info {
flex: 1;
display: flex;
flex-direction: column;
}
.service-name {
color: var(--text-primary);
font-weight: 500;
font-size: var(--font-size-md);
margin-bottom: 0.25rem;
}
.service-description {
color: var(--text-tertiary);
font-size: var(--font-size-sm);
}
.service-action {
color: var(--accent-primary);
font-size: var(--font-size-sm);
font-weight: 500;
}
@media (max-width: 768px) {
.coming-soon-card {
padding: 2rem 1.5rem;
}
h1 {
font-size: var(--font-size-3xl);
}
h2 {
font-size: var(--font-size-2xl);
}
.service-item {
padding: 1rem;
}
}
</style>

View File

@ -0,0 +1,492 @@
---
// src/pages/projects/tech-stack.astro
import BaseLayout from '../layouts/BaseLayout.astro';
import Header from '../components/Header.astro';
import Footer from '../components/Footer.astro';
const title = "Tech Stack | LaForceIT";
const description = "Explore the technology stack that powers LaForceIT projects, infrastructure, and home lab environment.";
// Define tech stack categories and items
const techCategories = [
{
name: "Infrastructure & Hosting",
icon: "🏗️",
technologies: [
{
name: "Proxmox VE",
description: "Hypervisor for virtualization and container management",
url: "https://www.proxmox.com/en/proxmox-ve",
logo: "/images/tech/proxmox.svg"
},
{
name: "TrueNAS Scale",
description: "Storage platform with built-in virtualization capabilities",
url: "https://www.truenas.com/truenas-scale/",
logo: "/images/tech/truenas.svg"
},
{
name: "pfSense",
description: "Open source firewall/router with advanced features",
url: "https://www.pfsense.org/",
logo: "/images/tech/pfsense.svg"
},
{
name: "UniFi Network",
description: "Enterprise-grade networking equipment and management",
url: "https://ui.com/",
logo: "/images/tech/unifi.svg"
}
]
},
{
name: "Container Orchestration",
icon: "🚢",
technologies: [
{
name: "Kubernetes",
description: "Container orchestration platform for application scaling",
url: "https://kubernetes.io/",
logo: "/images/tech/kubernetes.svg"
},
{
name: "Rancher",
description: "Multi-cluster Kubernetes management interface",
url: "https://rancher.com/",
logo: "/images/tech/rancher.svg"
},
{
name: "ArgoCD",
description: "GitOps continuous delivery tool for Kubernetes",
url: "https://argoproj.github.io/argo-cd/",
logo: "/images/tech/argocd.svg"
},
{
name: "Longhorn",
description: "Cloud-native distributed storage for Kubernetes",
url: "https://longhorn.io/",
logo: "/images/tech/longhorn.svg"
}
]
},
{
name: "CI/CD & Automation",
icon: "🔄",
technologies: [
{
name: "Terraform",
description: "Infrastructure as Code for cloud and on-premises resources",
url: "https://www.terraform.io/",
logo: "/images/tech/terraform.svg"
},
{
name: "Ansible",
description: "Automation platform for configuration management",
url: "https://www.ansible.com/",
logo: "/images/tech/ansible.svg"
},
{
name: "GitHub Actions",
description: "CI/CD pipeline automation integrated with GitHub",
url: "https://github.com/features/actions",
logo: "/images/tech/github-actions.svg"
},
{
name: "Flux CD",
description: "GitOps tool for keeping Kubernetes clusters in sync",
url: "https://fluxcd.io/",
logo: "/images/tech/flux.svg"
}
]
},
{
name: "Monitoring & Observability",
icon: "📊",
technologies: [
{
name: "Prometheus",
description: "Monitoring system with time-series database",
url: "https://prometheus.io/",
logo: "/images/tech/prometheus.svg"
},
{
name: "Grafana",
description: "Visualization and analytics platform for metrics",
url: "https://grafana.com/",
logo: "/images/tech/grafana.svg"
},
{
name: "Loki",
description: "Log aggregation system designed for Kubernetes",
url: "https://grafana.com/oss/loki/",
logo: "/images/tech/loki.svg"
},
{
name: "Uptime Kuma",
description: "Self-hosted monitoring tool for websites and services",
url: "https://github.com/louislam/uptime-kuma",
logo: "/images/tech/uptime-kuma.svg"
}
]
},
{
name: "Development & Web",
icon: "💻",
technologies: [
{
name: "Astro",
description: "Modern static site builder with minimal JavaScript",
url: "https://astro.build/",
logo: "/images/tech/astro.svg"
},
{
name: "Docker",
description: "Container platform for application packaging",
url: "https://www.docker.com/",
logo: "/images/tech/docker.svg"
},
{
name: "Tailwind CSS",
description: "Utility-first CSS framework for rapid UI development",
url: "https://tailwindcss.com/",
logo: "/images/tech/tailwind.svg"
},
{
name: "Node.js",
description: "JavaScript runtime for building server-side applications",
url: "https://nodejs.org/",
logo: "/images/tech/nodejs.svg"
}
]
}
];
---
<BaseLayout title={title} description={description}>
<Header slot="header" />
<div class="container">
<div class="page-header">
<h1>Tech Stack</h1>
<div class="header-accent"></div>
<p class="intro-text">
Explore the technologies, tools, and platforms that power the LaForceIT infrastructure, projects, and home lab environment. This curated stack focuses on enterprise-grade solutions that balance performance, reliability, and maintainability.
</p>
</div>
<div class="stack-container">
{techCategories.map(category => (
<div class="category-section">
<div class="category-header">
<div class="category-icon">{category.icon}</div>
<h2 class="category-title">{category.name}</h2>
</div>
<div class="technologies-grid">
{category.technologies.map(tech => (
<a href={tech.url} target="_blank" rel="noopener noreferrer" class="tech-card">
<div class="tech-logo-placeholder">
<span class="tech-initials">{tech.name.split(' ').map(word => word[0]).join('')}</span>
</div>
<div class="tech-info">
<h3 class="tech-name">{tech.name}</h3>
<p class="tech-description">{tech.description}</p>
</div>
<div class="tech-arrow">
<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="5" y1="12" x2="19" y2="12"></line>
<polyline points="12 5 19 12 12 19"></polyline>
</svg>
</div>
</a>
))}
</div>
</div>
))}
</div>
<div class="stack-cta">
<div class="cta-content">
<h2 class="cta-title">Build Your Own Home Lab</h2>
<p class="cta-text">Interested in setting up a similar tech stack in your own environment? Check out my detailed tutorials and guides for step-by-step instructions.</p>
</div>
<div class="cta-buttons">
<a href="/resources/tutorials" class="cta-button primary">View Tutorials</a>
<a href="/projects/services" class="cta-button secondary">Explore Live Services</a>
</div>
</div>
</div>
<Footer slot="footer" />
</BaseLayout>
<style>
.page-header {
margin: 3rem 0 4rem;
position: relative;
text-align: center;
max-width: 800px;
margin-left: auto;
margin-right: auto;
}
h1 {
font-size: var(--font-size-4xl);
background: linear-gradient(90deg, var(--accent-secondary), var(--accent-primary));
-webkit-background-clip: text;
background-clip: text;
color: transparent;
display: inline-block;
margin-bottom: 0.5rem;
}
.header-accent {
width: 80px;
height: 4px;
background: linear-gradient(90deg, var(--accent-secondary), var(--accent-primary));
border-radius: 2px;
margin: 0.5rem auto 2rem;
}
.intro-text {
color: var(--text-secondary);
font-size: var(--font-size-lg);
line-height: 1.7;
margin-bottom: 3rem;
}
.stack-container {
display: flex;
flex-direction: column;
gap: 4rem;
margin-bottom: 4rem;
}
.category-section {
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.category-header {
display: flex;
align-items: center;
gap: 1rem;
margin-bottom: 0.5rem;
}
.category-icon {
font-size: 2rem;
}
.category-title {
font-size: var(--font-size-2xl);
color: var(--text-primary);
position: relative;
}
.category-title::after {
content: '';
position: absolute;
bottom: -0.5rem;
left: 0;
width: 50px;
height: 3px;
background: linear-gradient(90deg, var(--accent-secondary), transparent);
border-radius: 2px;
}
.technologies-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 1.5rem;
}
.tech-card {
display: flex;
align-items: center;
gap: 1rem;
background: var(--card-bg);
border: 1px solid var(--border-primary);
border-radius: 12px;
padding: 1.25rem;
transition: all 0.3s ease;
text-decoration: none;
position: relative;
overflow: hidden;
}
.tech-card::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: linear-gradient(135deg, rgba(6, 182, 212, 0.05), transparent 60%);
opacity: 0;
transition: opacity 0.3s ease;
}
.tech-card:hover {
transform: translateY(-5px);
border-color: var(--accent-primary);
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.2);
}
.tech-card:hover::before {
opacity: 1;
}
.tech-logo-placeholder {
width: 50px;
height: 50px;
border-radius: 10px;
background: linear-gradient(135deg, var(--accent-secondary), var(--accent-primary));
display: flex;
align-items: center;
justify-content: center;
color: var(--bg-primary);
font-weight: bold;
font-size: var(--font-size-lg);
flex-shrink: 0;
}
.tech-info {
flex: 1;
}
.tech-name {
font-size: var(--font-size-lg);
color: var(--text-primary);
margin-bottom: 0.5rem;
}
.tech-description {
font-size: var(--font-size-sm);
color: var(--text-tertiary);
margin: 0;
line-height: 1.5;
}
.tech-arrow {
color: var(--accent-primary);
opacity: 0;
transform: translateX(-10px);
transition: all 0.3s ease;
}
.tech-card:hover .tech-arrow {
opacity: 1;
transform: translateX(0);
}
.stack-cta {
background: var(--card-bg);
border: 1px solid var(--card-border);
border-radius: 16px;
padding: 3rem;
margin: 6rem 0 4rem;
display: flex;
align-items: center;
justify-content: space-between;
gap: 2rem;
position: relative;
overflow: hidden;
}
.stack-cta::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: radial-gradient(circle at 30% 50%, rgba(6, 182, 212, 0.1), transparent 70%);
pointer-events: none;
}
.cta-content {
flex: 1;
}
.cta-title {
font-size: var(--font-size-3xl);
color: var(--text-primary);
margin-bottom: 1rem;
}
.cta-text {
color: var(--text-secondary);
font-size: var(--font-size-lg);
line-height: 1.7;
margin: 0;
}
.cta-buttons {
display: flex;
gap: 1rem;
}
.cta-button {
display: inline-block;
padding: 0.75rem 1.5rem;
border-radius: 8px;
font-weight: 500;
font-size: var(--font-size-md);
text-decoration: none;
transition: all 0.3s ease;
}
.cta-button.primary {
background: linear-gradient(90deg, var(--accent-secondary), var(--accent-primary));
color: var(--bg-primary);
box-shadow: 0 5px 15px rgba(6, 182, 212, 0.2);
}
.cta-button.secondary {
background: rgba(15, 23, 42, 0.5);
border: 1px solid var(--border-primary);
color: var(--text-primary);
}
.cta-button.primary:hover {
transform: translateY(-3px);
box-shadow: 0 8px 20px rgba(6, 182, 212, 0.3);
}
.cta-button.secondary:hover {
transform: translateY(-3px);
border-color: var(--accent-primary);
background: rgba(15, 23, 42, 0.8);
}
@media (max-width: 768px) {
.technologies-grid {
grid-template-columns: 1fr;
}
.stack-cta {
flex-direction: column;
text-align: center;
padding: 2rem 1.5rem;
}
.cta-buttons {
flex-direction: column;
width: 100%;
}
h1 {
font-size: var(--font-size-3xl);
}
.cta-title {
font-size: var(--font-size-2xl);
}
.category-title {
font-size: var(--font-size-xl);
}
}
</style>

441
src/pages/resources.astro Normal file
View File

@ -0,0 +1,441 @@
---
import BaseLayout from '../layouts/BaseLayout.astro';
import Header from '../components/Header.astro';
import '../styles/card-animations.css';
import Footer from '../components/Footer.astro';
---
<BaseLayout title="Resources - LaForceIT">
<Header slot="header" />
<!-- Main Content from public/resources.html -->
<main>
<div class="container">
<div class="page-header">
<h1 class="page-title">Resources</h1>
<p class="page-description">
A comprehensive collection of technical resources, configuration templates, and guides for infrastructure, containerization, and DevOps automation.
</p>
</div>
<!-- Search & Filters -->
<div class="resource-search">
<div class="search-icon">
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="11" cy="11" r="8"></circle>
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
</svg>
</div>
<input type="text" placeholder="Search resources..." class="search-input" id="resource-search-input">
</div>
<div class="resource-filters">
<button class="filter-button active" data-filter="all">All Resources</button>
<button class="filter-button" data-filter="kubernetes">Kubernetes</button>
<button class="filter-button" data-filter="docker">Docker</button>
<button class="filter-button" data-filter="config">Configurations</button>
<button class="filter-button" data-filter="iac">Infrastructure</button>
<button class="filter-button" data-filter="tutorials">Tutorials</button>
</div>
<!-- Resources Grid -->
<div class="resources-grid">
<!-- K8s Configurations -->
<div class="resource-card" data-category="kubernetes">
<div class="resource-icon-wrapper">
<svg class="resource-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M17.8 19.2L16 11l3.5-3.5a2.1 2.1 0 0 0 0-3 2.1 2.1 0 0 0-3 0L13 8l-8.2-1.8a.8.8 0 0 0-.9 1.2l5 8-5 8a.8.8 0 0 0 .9 1.2L13 22.7l2.8-2.9m-2.9-2.9l-4-6.5m7.9 8.7a2 2 0 1 0 2.3-3.2m-6.2-9l4-3.8"></path>
</svg>
<div class="resource-icon-particles">
<div class="resource-icon-particle"></div> <div class="resource-icon-particle"></div> <div class="resource-icon-particle"></div> <div class="resource-icon-particle"></div> <div class="resource-icon-particle"></div>
</div>
</div>
<div class="resource-content">
<h2 class="resource-title">K8s Configurations</h2>
<p class="resource-description">Curated collection of production-ready Kubernetes configuration templates for deployments, services, ingress, storage, and more, optimized for home lab environments.</p>
<a href="/resources/kubernetes" class="resource-link">Explore K8s Configurations <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="5" y1="12" x2="19" y2="12"></line><polyline points="12 5 19 12 12 19"></polyline></svg></a>
<div class="resource-tags"><span class="resource-tag">Kubernetes</span><span class="resource-tag">YAML</span><span class="resource-tag">Configurations</span></div>
</div>
<div class="shine-effect"></div>
</div>
<!-- Docker Compose -->
<div class="resource-card" data-category="docker">
<div class="resource-icon-wrapper">
<svg class="resource-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M22 12.5a2.5 2.5 0 0 1-2.5 2.5h-15a2.5 2.5 0 0 1 0-5h15a2.5 2.5 0 0 1 2.5 2.5Z"></path><path d="M5 10V7a1 1 0 0 1 1-1h4.586a1 1 0 0 1 .707.293l1.414 1.414a1 1 0 0 0 .707.293H15a1 1 0 0 1 1 1v1"></path><path d="M5 15v2a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-2"></path></svg>
<div class="resource-icon-particles"><div class="resource-icon-particle"></div><div class="resource-icon-particle"></div><div class="resource-icon-particle"></div><div class="resource-icon-particle"></div><div class="resource-icon-particle"></div></div>
</div>
<div class="resource-content">
<h2 class="resource-title">Docker Compose</h2>
<p class="resource-description">Ready-to-use Docker Compose templates for running various services and applications. Includes networking, volume management, and resource optimization for home labs.</p>
<a href="/resources/docker-compose" class="resource-link">Explore Docker Compose <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="5" y1="12" x2="19" y2="12"></line><polyline points="12 5 19 12 12 19"></polyline></svg></a>
<div class="resource-tags"><span class="resource-tag">Docker</span><span class="resource-tag">Compose</span><span class="resource-tag">Containers</span></div>
</div>
<div class="shine-effect"></div>
</div>
<!-- Configuration Files -->
<div class="resource-card" data-category="config">
<div class="resource-icon-wrapper">
<svg class="resource-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" 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><path d="M9 15v-1h6v1"></path><path d="M11 11v6"></path><path d="M9 11h6"></path></svg>
<div class="resource-icon-particles"><div class="resource-icon-particle"></div><div class="resource-icon-particle"></div><div class="resource-icon-particle"></div><div class="resource-icon-particle"></div><div class="resource-icon-particle"></div></div>
</div>
<div class="resource-content">
<h2 class="resource-title">Configuration Files</h2>
<p class="resource-description">Essential configuration files for popular services and tools like Nginx, HAProxy, Prometheus, Grafana, and more, optimized for home lab and personal infrastructure.</p>
<a href="/resources/config-files" class="resource-link">Explore Configuration Files <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="5" y1="12" x2="19" y2="12"></line><polyline points="12 5 19 12 12 19"></polyline></svg></a>
<div class="resource-tags"><span class="resource-tag">Config</span><span class="resource-tag">Services</span><span class="resource-tag">Tools</span></div>
</div>
<div class="shine-effect"></div>
</div>
<!-- Infrastructure Code -->
<div class="resource-card" data-category="iac">
<div class="resource-icon-wrapper">
<svg class="resource-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="16 18 22 12 16 6"></polyline><polyline points="8 6 2 12 8 18"></polyline><line x1="19" y1="12" x2="5" y2="12"></line></svg>
<div class="resource-icon-particles"><div class="resource-icon-particle"></div><div class="resource-icon-particle"></div><div class="resource-icon-particle"></div><div class="resource-icon-particle"></div><div class="resource-icon-particle"></div></div>
</div>
<div class="resource-content">
<h2 class="resource-title">Infrastructure Code</h2>
<p class="resource-description">Infrastructure as Code (IaC) templates using Terraform, Ansible, and other tools. Deploy virtualization, networking, storage, and services with automated, repeatable configurations.</p>
<a href="/resources/iac" class="resource-link">Explore Infrastructure Code <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="5" y1="12" x2="19" y2="12"></line><polyline points="12 5 19 12 12 19"></polyline></svg></a>
<div class="resource-tags"><span class="resource-tag">Terraform</span><span class="resource-tag">Ansible</span><span class="resource-tag">IaC</span></div>
</div>
<div class="shine-effect"></div>
</div>
<!-- Tutorials -->
<div class="resource-card" data-category="tutorials">
<div class="resource-icon-wrapper">
<svg class="resource-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z"></path><path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"></path></svg>
<div class="resource-icon-particles"><div class="resource-icon-particle"></div><div class="resource-icon-particle"></div><div class="resource-icon-particle"></div><div class="resource-icon-particle"></div><div class="resource-icon-particle"></div></div>
<div class="dots-animation"><div class="dot"></div><div class="dot"></div><div class="dot"></div></div>
</div>
<div class="resource-content">
<h2 class="resource-title">Tutorials</h2>
<p class="resource-description">Step-by-step guides and tutorials for setting up, configuring, and optimizing various infrastructure components. From beginner to advanced topics for home lab enthusiasts.</p>
<a href="/resources/tutorials" class="resource-link">Explore Tutorials <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="5" y1="12" x2="19" y2="12"></line><polyline points="12 5 19 12 12 19"></polyline></svg></a>
<div class="resource-tags"><span class="resource-tag">Guides</span><span class="resource-tag">Tutorials</span><span class="resource-tag">How-To</span></div>
</div>
<div class="shine-effect"></div>
</div>
<!-- Empty State (hidden by default) -->
<div class="no-results" style="display: none;">
<div class="no-results-icon">
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line><line x1="11" y1="8" x2="11" y2="14"></line><line x1="8" y1="11" x2="14" y2="11"></line></svg>
</div>
<h3>No matching resources found</h3>
<p>Try adjusting your search terms or filters to find what you're looking for.</p>
</div>
</div>
</div>
</main>
<Footer slot="footer" />
</BaseLayout>
<script>
// Script from public/resources.html (for filtering/search)
document.addEventListener('DOMContentLoaded', () => {
const filterButtons = document.querySelectorAll('.filter-button');
const resourceCards = document.querySelectorAll('.resource-card');
const noResults = document.querySelector('.no-results');
const searchInput = document.getElementById('resource-search-input');
function filterAndSearch() {
const searchTerm = searchInput ? searchInput.value.toLowerCase().trim() : '';
const activeFilter = document.querySelector('.filter-button.active')?.getAttribute('data-filter') || 'all';
let visibleCount = 0;
resourceCards.forEach(card => {
const title = card.querySelector('.resource-title')?.textContent.toLowerCase() || '';
const description = card.querySelector('.resource-description')?.textContent.toLowerCase() || '';
const tags = Array.from(card.querySelectorAll('.resource-tag')).map(tag => tag.textContent.toLowerCase());
const cardCategory = card.getAttribute('data-category');
const matchesSearch = searchTerm === '' || title.includes(searchTerm) || description.includes(searchTerm) || tags.some(tag => tag.includes(searchTerm));
const matchesFilter = activeFilter === 'all' || cardCategory === activeFilter;
if (matchesSearch && matchesFilter) {
card.style.display = 'flex'; // Use flex as cards are flex containers
visibleCount++;
} else {
card.style.display = 'none';
}
});
if (noResults) {
noResults.style.display = visibleCount === 0 ? 'block' : 'none';
}
}
filterButtons.forEach(button => {
button.addEventListener('click', () => {
filterButtons.forEach(btn => btn.classList.remove('active'));
button.classList.add('active');
filterAndSearch(); // Re-run search when filter changes
});
});
if (searchInput) {
searchInput.addEventListener('input', filterAndSearch);
}
// Initial filter/search on load
filterAndSearch();
// Card hover effect enhancement (particle animation)
resourceCards.forEach(card => {
card.addEventListener('mouseenter', () => {
const particles = card.querySelectorAll('.resource-icon-particle');
particles.forEach(particle => {
const x = Math.random() * 100;
const y = Math.random() * 100;
particle.style.top = `${y}%`;
particle.style.left = `${x}%`;
});
});
});
});
</script>
<style>
/* Styles specific to this page, if any, can go here */
/* Most styling should come from global.css and theme.css */
.page-header {
text-align: center;
margin-bottom: 3rem;
}
.page-title {
font-size: 3rem;
font-weight: 700;
margin-bottom: 1rem;
background: linear-gradient(to right, var(--accent-primary), var(--accent-secondary));
-webkit-background-clip: text;
background-clip: text;
color: transparent;
position: relative;
display: inline-block;
}
.page-title::after {
content: '';
position: absolute;
bottom: -10px;
left: 50%;
transform: translateX(-50%);
width: 100px;
height: 3px;
background: linear-gradient(to right, var(--accent-primary), var(--accent-secondary));
border-radius: 3px;
}
.page-description {
max-width: 800px;
margin: 0 auto;
color: var(--text-secondary);
font-size: 1.1rem;
}
/* Resource Filtering */
.resource-filters {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 0.75rem;
margin: 2rem 0;
}
.filter-button {
padding: 0.5rem 1.25rem;
background: var(--bg-tertiary);
border: 1px solid var(--border-primary);
color: var(--text-secondary);
border-radius: 20px;
font-size: 0.9rem;
cursor: pointer;
transition: all 0.3s ease;
}
.filter-button:hover {
background: linear-gradient(90deg, rgba(6, 182, 212, 0.1), rgba(59, 130, 246, 0.1));
color: var(--text-primary);
border-color: var(--accent-primary);
box-shadow: 0 0 10px var(--glow-primary);
}
.filter-button.active {
background: linear-gradient(90deg, rgba(6, 182, 212, 0.2), rgba(59, 130, 246, 0.2));
color: var(--accent-primary);
border-color: var(--accent-primary);
box-shadow: 0 0 15px var(--glow-primary);
}
/* Search Box */
.resource-search {
display: flex;
margin: 0 auto 2rem;
max-width: 500px;
position: relative;
}
.search-input {
width: 100%;
padding: 0.75rem 1rem 0.75rem 2.75rem;
background: var(--bg-tertiary);
border: 1px solid var(--border-primary);
border-radius: 8px;
color: var(--text-primary);
font-size: 1rem;
transition: all 0.3s ease;
}
.search-input:focus {
outline: none;
border-color: var(--accent-primary);
box-shadow: 0 0 0 3px var(--glow-primary);
}
.search-icon {
position: absolute;
left: 1rem;
top: 50%;
transform: translateY(-50%);
color: var(--text-tertiary);
}
/* Resources Grid */
.resources-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
gap: 2rem;
margin-top: 1rem;
}
/* Styles for .resource-card, .resource-icon-wrapper, etc. are now in card-animations.css */
.resource-content {
padding: 1.5rem;
flex: 1;
display: flex;
flex-direction: column;
}
.resource-title {
font-size: 1.3rem;
font-weight: 600;
margin-bottom: 0.75rem;
color: var(--text-primary);
}
.resource-description {
color: var(--text-secondary);
margin-bottom: 1.5rem;
flex: 1;
}
.resource-link {
display: inline-flex;
align-items: center;
text-decoration: none;
color: var(--accent-primary);
font-weight: 500;
transition: all 0.3s ease;
position: relative;
align-self: flex-start;
}
.resource-link::after {
content: '';
position: absolute;
bottom: -2px;
left: 0;
width: 100%;
height: 1px;
background: var(--accent-primary);
transform: scaleX(0);
transform-origin: right;
transition: transform 0.3s ease;
}
.resource-link:hover {
color: var(--accent-secondary);
}
.resource-link:hover::after {
transform: scaleX(1);
transform-origin: left;
}
.resource-link svg {
margin-left: 0.5rem;
transition: transform 0.3s ease;
}
.resource-link:hover svg {
transform: translateX(3px);
}
.resource-tags {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
margin-top: 1rem;
}
/* .resource-tag styling is now in card-animations.css */
/* Shine effect styling is now in card-animations.css */
/* Empty State */
.no-results {
text-align: center;
padding: 3rem;
color: var(--text-secondary);
background: var(--bg-tertiary);
border-radius: 12px;
border: 1px dashed var(--border-primary);
margin-top: 2rem;
display: none; /* Hidden by default */
}
.no-results-icon {
color: var(--text-tertiary);
margin-bottom: 1rem;
}
.no-results-icon svg {
width: 64px;
height: 64px;
stroke-width: 1;
opacity: 0.5;
}
/* Hovering dot animation */
.dots-animation {
position: absolute;
bottom: 20px;
right: 20px;
width: 50px;
height: 20px;
display: flex;
align-items: center;
justify-content: space-between;
}
.dot {
width: 6px;
height: 6px;
border-radius: 50%;
background-color: var(--accent-primary);
animation: dot-bounce 1.4s infinite ease-in-out both;
}
.dot:nth-child(1) { animation-delay: -0.32s; }
.dot:nth-child(2) { animation-delay: -0.16s; }
/* Keyframes are now in card-animations.css */
</style>

View File

@ -0,0 +1,393 @@
---
// src/pages/resources/config-files.astro
import BaseLayout from '../../layouts/BaseLayout.astro';
import Header from '../../components/Header.astro';
import Footer from '../../components/Footer.astro';
const title = "Configuration Files | LaForceIT";
const description = "A curated collection of configuration files and templates for various DevOps tools and infrastructure components.";
---
<BaseLayout title={title} description={description}>
<Header slot="header" />
<div class="container">
<div class="page-header">
<h1>Configuration Files</h1>
<div class="header-accent"></div>
</div>
<div class="coming-soon-container">
<div class="coming-soon-card">
<div class="icon-container">
<svg xmlns="http://www.w3.org/2000/svg" width="60" height="60" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" 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>
</div>
<h2>Configuration Files Coming Soon</h2>
<p class="description">
This section will feature a carefully curated collection of configuration files, templates, and examples for various DevOps tools, infrastructure components, and applications. All files will include detailed comments explaining configuration options, best practices, and security considerations.
</p>
<div class="config-categories">
<div class="category-block">
<h3 class="category-title">Web Servers</h3>
<ul class="file-list">
<li class="file-item">
<span class="file-icon">📄</span>
<span class="file-name">nginx.conf</span>
<span class="file-tag">Performance</span>
</li>
<li class="file-item">
<span class="file-icon">📄</span>
<span class="file-name">apache2.conf</span>
<span class="file-tag">Security</span>
</li>
<li class="file-item">
<span class="file-icon">📄</span>
<span class="file-name">caddy.json</span>
<span class="file-tag">HTTPS</span>
</li>
</ul>
</div>
<div class="category-block">
<h3 class="category-title">Databases</h3>
<ul class="file-list">
<li class="file-item">
<span class="file-icon">📄</span>
<span class="file-name">postgresql.conf</span>
<span class="file-tag">Tuning</span>
</li>
<li class="file-item">
<span class="file-icon">📄</span>
<span class="file-name">my.cnf</span>
<span class="file-tag">MySQL</span>
</li>
<li class="file-item">
<span class="file-icon">📄</span>
<span class="file-name">redis.conf</span>
<span class="file-tag">Caching</span>
</li>
</ul>
</div>
<div class="category-block">
<h3 class="category-title">CI/CD Tools</h3>
<ul class="file-list">
<li class="file-item">
<span class="file-icon">📄</span>
<span class="file-name">.gitlab-ci.yml</span>
<span class="file-tag">Pipeline</span>
</li>
<li class="file-item">
<span class="file-icon">📄</span>
<span class="file-name">Jenkinsfile</span>
<span class="file-tag">Multi-stage</span>
</li>
<li class="file-item">
<span class="file-icon">📄</span>
<span class="file-name">github-actions.yml</span>
<span class="file-tag">Workflow</span>
</li>
</ul>
</div>
<div class="category-block">
<h3 class="category-title">Monitoring</h3>
<ul class="file-list">
<li class="file-item">
<span class="file-icon">📄</span>
<span class="file-name">prometheus.yml</span>
<span class="file-tag">Metrics</span>
</li>
<li class="file-item">
<span class="file-icon">📄</span>
<span class="file-name">grafana.ini</span>
<span class="file-tag">Dashboard</span>
</li>
<li class="file-item">
<span class="file-icon">📄</span>
<span class="file-name">node_exporter.service</span>
<span class="file-tag">Systemd</span>
</li>
</ul>
</div>
</div>
<div class="file-preview">
<div class="file-preview-header">
<span class="preview-filename">nginx.conf</span>
<span class="preview-badge">Coming Soon</span>
</div>
<pre class="preview-content"><code is:raw># NGINX Configuration with optimizations for performance and security
# This template includes best practices for production environments
user nginx;
worker_processes auto;
worker_rlimit_nofile 65535;
pid /run/nginx.pid;
events {
worker_connections 4096;
multi_accept on;
use epoll;
}
http {
# Basic Settings
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;
server_tokens off;
# MIME Types
include /etc/nginx/mime.types;
default_type application/octet-stream;
# SSL Settings
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers on;
ssl_session_cache shared:SSL:10m;
# Logging
access_log /var/log/nginx/access.log;
error_log /var/log/nginx/error.log;
# Gzip Settings
gzip on;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 6;
gzip_types text/plain text/css application/json application/javascript text/xml;
# Virtual Host Configs
include /etc/nginx/conf.d/*.conf;
# Additional settings and security headers will be included here...
}</code></pre>
</div>
</div>
</div>
</div>
<Footer slot="footer" />
</BaseLayout>
<style>
.page-header {
margin: 3rem 0 4rem;
position: relative;
}
h1 {
font-size: var(--font-size-4xl);
background: linear-gradient(90deg, var(--accent-secondary), var(--accent-tertiary));
-webkit-background-clip: text;
background-clip: text;
color: transparent;
display: inline-block;
margin-bottom: 0.5rem;
}
.header-accent {
width: 80px;
height: 4px;
background: linear-gradient(90deg, var(--accent-secondary), var(--accent-tertiary));
border-radius: 2px;
}
.coming-soon-container {
display: flex;
justify-content: center;
padding: 2rem 0 4rem;
}
.coming-soon-card {
background: var(--card-bg);
border: 1px solid var(--card-border);
border-radius: 16px;
padding: 2.5rem;
max-width: 800px;
width: 100%;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
position: relative;
overflow: hidden;
}
.coming-soon-card::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: radial-gradient(circle at 50% 50%, rgba(139, 92, 246, 0.1), transparent 70%);
pointer-events: none;
}
.icon-container {
display: flex;
justify-content: center;
margin-bottom: 1.5rem;
color: var(--accent-tertiary);
}
h2 {
text-align: center;
margin-bottom: 1.5rem;
font-size: var(--font-size-3xl);
color: var(--text-primary);
}
.description {
text-align: center;
margin-bottom: 2.5rem;
color: var(--text-secondary);
font-size: var(--font-size-lg);
line-height: 1.7;
}
.config-categories {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 1.5rem;
margin-bottom: 2.5rem;
}
.category-block {
background: rgba(30, 41, 59, 0.5);
border: 1px solid var(--border-secondary);
border-radius: 10px;
padding: 1.5rem;
transition: all 0.3s ease;
}
.category-block:hover {
transform: translateY(-3px);
border-color: var(--accent-tertiary);
box-shadow: 0 8px 20px rgba(139, 92, 246, 0.1);
}
.category-title {
font-size: var(--font-size-lg);
color: var(--text-primary);
margin-bottom: 1rem;
position: relative;
padding-bottom: 0.5rem;
}
.category-title::after {
content: '';
position: absolute;
left: 0;
bottom: 0;
width: 40px;
height: 2px;
background: var(--accent-tertiary);
}
.file-list {
list-style-type: none;
padding: 0;
margin: 0;
}
.file-item {
display: flex;
align-items: center;
padding: 0.75rem 0;
border-bottom: 1px solid var(--border-secondary);
}
.file-item:last-child {
border-bottom: none;
}
.file-icon {
margin-right: 0.75rem;
font-size: 1.25rem;
}
.file-name {
flex: 1;
color: var(--text-secondary);
font-family: var(--font-mono);
font-size: var(--font-size-sm);
}
.file-tag {
font-size: var(--font-size-xs);
color: var(--accent-tertiary);
background: rgba(139, 92, 246, 0.1);
padding: 0.25rem 0.5rem;
border-radius: 4px;
border: 1px solid rgba(139, 92, 246, 0.2);
}
.file-preview {
margin-top: 2rem;
border: 1px solid var(--border-primary);
border-radius: 8px;
overflow: hidden;
}
.file-preview-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.75rem 1.25rem;
background: rgba(15, 23, 42, 0.7);
border-bottom: 1px solid var(--border-secondary);
}
.preview-filename {
font-family: var(--font-mono);
font-size: var(--font-size-sm);
color: var(--text-secondary);
}
.preview-badge {
font-size: var(--font-size-xs);
color: var(--accent-tertiary);
background: rgba(139, 92, 246, 0.1);
padding: 0.25rem 0.5rem;
border-radius: 4px;
border: 1px solid rgba(139, 92, 246, 0.2);
}
.preview-content {
margin: 0;
padding: 1.25rem;
font-family: var(--font-mono);
font-size: var(--font-size-sm);
line-height: 1.6;
overflow-x: auto;
background: var(--bg-code);
color: var(--text-secondary);
}
@media (max-width: 768px) {
.config-categories {
grid-template-columns: 1fr;
}
.coming-soon-card {
padding: 2rem 1.5rem;
}
h1 {
font-size: var(--font-size-3xl);
}
h2 {
font-size: var(--font-size-2xl);
}
}
</style>

View File

@ -0,0 +1,462 @@
---
// src/pages/resources/docker-compose.astro
import BaseLayout from '../../layouts/BaseLayout.astro';
import Header from '../../components/Header.astro';
import Footer from '../../components/Footer.astro';
import '../../styles/card-animations.css'; // Assuming cards might be used
const title = "Docker Compose Resources | LaForceIT";
const description = "A collection of Docker Compose files for various services and applications, optimized for home lab environments.";
// Placeholder data - replace with actual data
const composeFiles = [
{
title: "Media Server Stack",
description: "Compose file for Plex, Sonarr, Radarr, and Prowlarr.",
filename: "media-stack.yml",
code: `# docker-compose.yml for Media Stack
version: "3.8"
services:
plex:
image: lscr.io/linuxserver/plex:latest
container_name: plex
sonarr:
image: lscr.io/linuxserver/sonarr:latest
container_name: sonarr
radarr:
image: lscr.io/linuxserver/radarr:latest
container_name: radarr
prowlarr:
image: lscr.io/linuxserver/prowlarr:develop
container_name: prowlarr`,
tags: ["docker-compose", "media-server", "plex", "arr-stack"],
lastUpdated: "April 20, 2025",
docLink: "#"
},
// Add more compose file objects here
];
const filters = ["all", "media", "networking", "utilities"]; // Example filters
---
<BaseLayout {title} {description}>
<Header slot="header" />
<div class="container">
<div class="page-header">
<h1>Docker Compose Resources</h1>
<div class="header-accent"></div>
</div>
<div class="resources-container">
<div class="resource-search">
<div class="search-icon">
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="11" cy="11" r="8"></circle>
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
</svg>
</div>
<input type="text" placeholder="Search compose files..." class="search-input" id="compose-search-input">
</div>
<div class="resource-filters">
{filters.map(filter => (
<button class={`filter-button ${filter === 'all' ? 'active' : ''}`} data-filter={filter}>
{filter.charAt(0).toUpperCase() + filter.slice(1)}
</button>
))}
</div>
<h2 class="section-title">Docker Compose Files</h2>
<p class="section-description">
Ready-to-use Docker Compose templates for running various services and applications. Includes networking, volume management, and resource optimization for home labs.
</p>
<div class="resources-grid">
{composeFiles.length > 0 ? composeFiles.map(compose => (
<div class="resource-card" data-category={compose.tags.includes('media-server') ? 'media' : 'utilities'}> {/* Example category logic */}
<div class="resource-header">
<div class="resource-icon">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M22 12.5a2.5 2.5 0 0 1-2.5 2.5h-15a2.5 2.5 0 0 1 0-5h15a2.5 2.5 0 0 1 2.5 2.5Z"></path><path d="M5 10V7a1 1 0 0 1 1-1h4.586a1 1 0 0 1 .707.293l1.414 1.414a1 1 0 0 0 .707.293H15a1 1 0 0 1 1 1v1"></path><path d="M5 15v2a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-2"></path></svg>
</div>
<div>
<h3 class="resource-title">{compose.title}</h3>
{/* <p class="resource-description">{compose.description}</p> */}
</div>
</div>
<div class="resource-body">
<p class="resource-description">{compose.description}</p>
<div class="code-preview">
<div class="code-header">
<span class="filename">{compose.filename}</span>
<div class="code-actions">
<button class="code-action copy-button">Copy</button>
</div>
</div>
<pre class="code-block"><code>{compose.code}</code></pre>
</div>
<div class="tags">
{compose.tags.map(tag => <span class="tag">{tag}</span>)}
</div>
</div>
<div class="resource-footer">
<span class="last-updated">Last updated: {compose.lastUpdated}</span>
<a href={compose.docLink} class="resource-link" target="_blank" rel="noopener noreferrer">
View Documentation
<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="5" y1="12" x2="19" y2="12"></line>
<polyline points="12 5 19 12 12 19"></polyline>
</svg>
</a>
</div>
</div>
)) : (
<p>Docker Compose resources coming soon...</p>
)}
</div>
</div>
</div>
<Footer slot="footer" />
</BaseLayout>
<style is:global>
/* Styles specific to this page, if any, can go here */
/* Most styling should come from global.css, theme.css, and card-animations.css */
.container {
max-width: 1200px;
margin: 0 auto;
padding: 0 1.5rem;
}
.page-header {
margin: 3rem 0 4rem;
position: relative;
}
h1 {
font-size: var(--font-size-4xl);
background: linear-gradient(90deg, var(--accent-secondary), var(--accent-primary));
-webkit-background-clip: text;
background-clip: text;
color: transparent;
display: inline-block;
margin-bottom: 0.5rem;
}
.header-accent {
width: 80px;
height: 4px;
background: linear-gradient(90deg, var(--accent-secondary), var(--accent-primary));
border-radius: 2px;
}
.resources-container {
padding: 1rem 0 4rem;
}
.resource-search {
display: flex;
margin: 0 auto 2rem;
max-width: 500px;
position: relative;
}
.search-input {
width: 100%;
padding: 0.75rem 1rem 0.75rem 2.75rem;
background: var(--bg-tertiary);
border: 1px solid var(--border-primary);
border-radius: 8px;
color: var(--text-primary);
font-size: 1rem;
transition: all 0.3s ease;
}
.search-input:focus {
outline: none;
border-color: var(--accent-primary);
box-shadow: 0 0 0 3px var(--glow-primary);
}
.search-icon {
position: absolute;
left: 1rem;
top: 50%;
transform: translateY(-50%);
color: var(--text-tertiary);
}
.resource-filters {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 0.75rem;
margin: 2rem 0;
}
.filter-button {
padding: 0.5rem 1.25rem;
background: var(--bg-tertiary);
border: 1px solid var(--border-primary);
color: var(--text-secondary);
border-radius: 20px;
font-size: 0.9rem;
cursor: pointer;
transition: all 0.3s ease;
}
.filter-button:hover {
background: linear-gradient(90deg, rgba(6, 182, 212, 0.1), rgba(59, 130, 246, 0.1));
color: var(--text-primary);
border-color: var(--accent-primary);
box-shadow: 0 0 10px var(--glow-primary);
}
.filter-button.active {
background: linear-gradient(90deg, rgba(6, 182, 212, 0.2), rgba(59, 130, 246, 0.2));
color: var(--accent-primary);
border-color: var(--accent-primary);
box-shadow: 0 0 15px var(--glow-primary);
}
.section-title {
font-size: var(--font-size-2xl);
margin-bottom: 1rem;
color: var(--text-primary);
}
.section-description {
font-size: var(--font-size-lg);
color: var(--text-secondary);
margin-bottom: 2rem;
max-width: 800px;
}
.resources-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
gap: 1.5rem;
margin-bottom: 3rem;
}
.resource-header {
padding: 1.25rem;
border-bottom: 1px solid var(--border-secondary);
display: flex;
align-items: center;
gap: 1rem;
}
.resource-icon {
color: var(--accent-secondary);
flex-shrink: 0;
}
.resource-icon svg {
width: 24px;
height: 24px;
}
.resource-title {
font-size: var(--font-size-xl);
color: var(--text-primary);
margin-bottom: 0.25rem;
}
.resource-description {
font-size: var(--font-size-sm);
color: var(--text-secondary);
margin-top: 0.5rem;
flex-grow: 1;
}
.resource-body {
padding: 1.25rem;
flex-grow: 1;
display: flex;
flex-direction: column;
}
.code-preview {
background: var(--bg-code);
border-radius: 8px;
overflow: hidden;
margin-top: 1rem;
border: 1px solid var(--border-secondary);
flex-grow: 1;
display: flex;
flex-direction: column;
min-height: 200px;
}
.code-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.75rem 1rem;
background: rgba(0, 0, 0, 0.2);
border-bottom: 1px solid var(--border-secondary);
}
.filename {
font-family: var(--font-mono);
font-size: var(--font-size-sm);
color: var(--text-secondary);
}
.code-actions {
display: flex;
align-items: center;
gap: 0.5rem;
}
.code-action {
font-size: var(--font-size-xs);
color: var(--text-secondary);
background: rgba(30, 41, 59, 0.5);
padding: 0.25rem 0.5rem;
border-radius: 4px;
cursor: pointer;
transition: all 0.2s ease;
border: none;
}
.code-action:hover {
background: rgba(59, 130, 246, 0.2);
color: var(--accent-secondary);
}
.code-block {
margin: 0 !important;
padding: 1rem !important;
background: transparent !important;
font-family: var(--font-mono) !important;
font-size: var(--font-size-sm) !important;
line-height: 1.6 !important;
overflow: auto !important;
color: var(--text-secondary) !important;
flex-grow: 1;
max-height: 300px;
}
.code-block pre {
height: 100%;
margin: 0;
padding: 0;
}
.tags {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
margin-top: 1rem;
}
.resource-footer {
padding: 1rem 1.25rem;
border-top: 1px solid var(--border-secondary);
display: flex;
justify-content: space-between;
align-items: center;
margin-top: auto;
}
.last-updated {
font-size: var(--font-size-xs);
color: var(--text-tertiary);
}
.resource-link {
font-size: var(--font-size-sm);
color: var(--accent-secondary);
text-decoration: none;
display: flex;
align-items: center;
gap: 0.5rem;
}
.resource-link:hover {
text-decoration: underline;
}
@media (max-width: 768px) {
.resources-grid {
grid-template-columns: 1fr;
}
h1 { font-size: var(--font-size-3xl); }
.section-title { font-size: var(--font-size-xl); }
.section-description { font-size: var(--font-size-base); }
}
</style>
<script>
// Copy button functionality & Filter functionality
document.addEventListener('DOMContentLoaded', () => {
// Copy Button
document.querySelectorAll('.copy-button').forEach(button => {
button.addEventListener('click', (event) => {
const codeBlock = event.target.closest('.code-preview').querySelector('.code-block code');
if (codeBlock) {
navigator.clipboard.writeText(codeBlock.textContent || '')
.then(() => {
event.target.textContent = 'Copied!';
setTimeout(() => { event.target.textContent = 'Copy'; }, 2000);
})
.catch(err => {
console.error('Failed to copy text: ', err);
event.target.textContent = 'Error';
setTimeout(() => { event.target.textContent = 'Copy'; }, 2000);
});
}
});
});
// Filter Functionality
const filterButtons = document.querySelectorAll('.filter-button');
const resourceCards = document.querySelectorAll('.resource-card');
const noResults = document.querySelector('.no-results'); // Assuming you might add this later
const searchInput = document.getElementById('compose-search-input');
const filters = ["all", "media", "networking", "utilities"]; // Match filters defined in frontmatter
function filterAndSearch() {
const searchTerm = searchInput ? searchInput.value.toLowerCase().trim() : '';
const activeFilter = document.querySelector('.filter-button.active')?.getAttribute('data-filter') || 'all';
let visibleCount = 0;
resourceCards.forEach(card => {
const title = card.querySelector('.resource-title')?.textContent.toLowerCase() || '';
const description = card.querySelector('.resource-description')?.textContent.toLowerCase() || '';
const tags = Array.from(card.querySelectorAll('.tag')).map(tag => tag.textContent.toLowerCase());
const cardCategory = card.getAttribute('data-category') || ''; // Get category from data attribute
const matchesSearch = searchTerm === '' || title.includes(searchTerm) || description.includes(searchTerm) || tags.some(tag => tag.includes(searchTerm));
const matchesFilter = activeFilter === 'all' || cardCategory === activeFilter;
if (matchesSearch && matchesFilter) {
card.style.display = 'flex';
visibleCount++;
} else {
card.style.display = 'none';
}
});
if (noResults) {
noResults.style.display = visibleCount === 0 ? 'block' : 'none';
}
}
filterButtons.forEach(button => {
button.addEventListener('click', () => {
filterButtons.forEach(btn => btn.classList.remove('active'));
button.classList.add('active');
filterAndSearch();
});
});
if (searchInput) {
searchInput.addEventListener('input', filterAndSearch);
}
// Initial filter/search on load
filterAndSearch();
});
</script>

View File

@ -0,0 +1,424 @@
---
// src/pages/resources/iac.astro
import BaseLayout from '../../layouts/BaseLayout.astro';
import Header from '../../components/Header.astro';
import Footer from '../../components/Footer.astro';
const title = "Infrastructure as Code | LaForceIT";
const description = "A collection of Terraform, Ansible, and other IaC templates and modules for efficient infrastructure management.";
// Define the Terraform example as a string to avoid parsing issues
const terraformExample = `module "vpc" {
source = "terraform-aws-modules/vpc/aws"
version = "~> 4.0"
name = "homelab-vpc"
cidr = "10.0.0.0/16"
azs = ["us-west-2a", "us-west-2b", "us-west-2c"]
private_subnets = ["10.0.1.0/24", "10.0.2.0/24", "10.0.3.0/24"]
public_subnets = ["10.0.101.0/24", "10.0.102.0/24", "10.0.103.0/24"]
enable_nat_gateway = true
single_nat_gateway = true
enable_vpn_gateway = false
# Enable DNS support and hostnames
enable_dns_hostnames = true
enable_dns_support = true
# Add tags to all resources
tags = {
Environment = "prod"
Terraform = "true"
Project = "homelab"
Owner = "infrastructure-team"
ManagedBy = "terraform"
}
# Add specific tags to subnets for load balancer configuration
public_subnet_tags = {
"kubernetes.io/role/elb" = "1"
"Tier" = "public"
}
private_subnet_tags = {
"kubernetes.io/role/internal-elb" = "1"
"Tier" = "private"
}
}`;
---
<BaseLayout title={title} description={description}>
<Header slot="header" />
<div class="container">
<div class="page-header">
<h1>Infrastructure as Code</h1>
<div class="header-accent"></div>
</div>
<div class="coming-soon-container">
<div class="coming-soon-card">
<div class="icon-container">
<svg xmlns="http://www.w3.org/2000/svg" width="60" height="60" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<polyline points="16 18 22 12 16 6"></polyline>
<polyline points="8 6 2 12 8 18"></polyline>
<line x1="12" y1="2" x2="12" y2="22"></line>
</svg>
</div>
<h2>Infrastructure as Code Resources Coming Soon</h2>
<p class="description">
This section will feature a comprehensive collection of Infrastructure as Code (IaC) templates, modules, and examples using tools like Terraform, Ansible, Pulumi, and CloudFormation. These resources will help you automate infrastructure provisioning and configuration across various cloud providers and on-premises environments.
</p>
<div class="tools-grid">
<div class="tool-card terraform">
<div class="tool-header">
<div class="tool-icon">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<polygon points="12 2 22 8 22 16 12 22 2 16 2 8 12 2"></polygon>
</svg>
</div>
<h3 class="tool-name">Terraform</h3>
</div>
<p class="tool-description">
Infrastructure provisioning modules and templates for AWS, Azure, GCP, and multi-cloud environments.
</p>
<div class="tool-tags">
<span class="tool-tag">AWS</span>
<span class="tool-tag">Azure</span>
<span class="tool-tag">GCP</span>
<span class="tool-tag">Modules</span>
</div>
</div>
<div class="tool-card ansible">
<div class="tool-header">
<div class="tool-icon">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M19 3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2z"></path>
<polyline points="8 9 12 5 16 9"></polyline>
<line x1="12" y1="5" x2="12" y2="19"></line>
</svg>
</div>
<h3 class="tool-name">Ansible</h3>
</div>
<p class="tool-description">
Playbooks and roles for configuration management, application deployment, and system administration.
</p>
<div class="tool-tags">
<span class="tool-tag">Playbooks</span>
<span class="tool-tag">Roles</span>
<span class="tool-tag">Config</span>
<span class="tool-tag">Deploy</span>
</div>
</div>
<div class="tool-card kubernetes">
<div class="tool-header">
<div class="tool-icon">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="10"></circle>
<line x1="14.31" y1="8" x2="20.05" y2="17.94"></line>
<line x1="9.69" y1="8" x2="21.17" y2="8"></line>
<line x1="7.38" y1="12" x2="13.12" y2="2.06"></line>
<line x1="9.69" y1="16" x2="3.95" y2="6.06"></line>
<line x1="14.31" y1="16" x2="2.83" y2="16"></line>
<line x1="16.62" y1="12" x2="10.88" y2="21.94"></line>
</svg>
</div>
<h3 class="tool-name">Kubernetes</h3>
</div>
<p class="tool-description">
Helm charts, operators, and custom resources for managing applications and services in Kubernetes.
</p>
<div class="tool-tags">
<span class="tool-tag">Helm</span>
<span class="tool-tag">CRDs</span>
<span class="tool-tag">Operators</span>
<span class="tool-tag">Templates</span>
</div>
</div>
<div class="tool-card multi">
<div class="tool-header">
<div class="tool-icon">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"></path>
<polyline points="7.5 4.21 12 6.81 16.5 4.21"></polyline>
<polyline points="7.5 19.79 7.5 14.6 3 12"></polyline>
<polyline points="21 12 16.5 14.6 16.5 19.79"></polyline>
<polyline points="3.27 6.96 12 12.01 20.73 6.96"></polyline>
<line x1="12" y1="22.08" x2="12" y2="12"></line>
</svg>
</div>
<h3 class="tool-name">Multi-Tool Solutions</h3>
</div>
<p class="tool-description">
End-to-end infrastructure solutions combining multiple IaC tools for comprehensive deployments.
</p>
<div class="tool-tags">
<span class="tool-tag">CI/CD</span>
<span class="tool-tag">Pipelines</span>
<span class="tool-tag">GitOps</span>
<span class="tool-tag">Templates</span>
</div>
</div>
</div>
<div class="code-preview-container">
<div class="preview-header">
<div class="file-path">terraform/aws/vpc/main.tf</div>
<div class="preview-badge">Coming Soon</div>
</div>
<pre class="code-preview"><code set:html={terraformExample}></code></pre>
</div>
</div>
</div>
</div>
<Footer slot="footer" />
</BaseLayout>
<style>
.container {
width: 100%;
max-width: 1200px;
margin: 0 auto;
padding: 0 1.5rem;
}
.page-header {
margin: 3rem 0 4rem;
position: relative;
}
h1 {
font-size: var(--font-size-4xl);
background: linear-gradient(90deg, #2563eb, #06b6d4);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
display: inline-block;
margin-bottom: 0.5rem;
}
.header-accent {
width: 80px;
height: 4px;
background: linear-gradient(90deg, #2563eb, #06b6d4);
border-radius: 2px;
}
.coming-soon-container {
display: flex;
justify-content: center;
padding: 2rem 0 4rem;
}
.coming-soon-card {
background: var(--card-bg);
border: 1px solid var(--card-border);
border-radius: 16px;
padding: 2.5rem;
max-width: 800px;
width: 100%;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
position: relative;
overflow: hidden;
}
.coming-soon-card::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: radial-gradient(circle at 50% 50%, rgba(6, 182, 212, 0.1), transparent 70%);
pointer-events: none;
}
.icon-container {
display: flex;
justify-content: center;
margin-bottom: 1.5rem;
color: #06b6d4;
}
h2 {
text-align: center;
margin-bottom: 1.5rem;
font-size: var(--font-size-3xl);
color: var(--text-primary);
}
.description {
text-align: center;
margin-bottom: 2.5rem;
color: var(--text-secondary);
font-size: var(--font-size-lg);
line-height: 1.7;
}
.tools-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 1.5rem;
margin-bottom: 2.5rem;
}
.tool-card {
border-radius: 10px;
padding: 1.5rem;
transition: all 0.3s ease;
position: relative;
overflow: hidden;
}
.tool-card.terraform {
background: linear-gradient(135deg, rgba(106, 90, 205, 0.1), rgba(0, 0, 0, 0));
border: 1px solid rgba(106, 90, 205, 0.2);
}
.tool-card.ansible {
background: linear-gradient(135deg, rgba(255, 0, 0, 0.1), rgba(0, 0, 0, 0));
border: 1px solid rgba(255, 0, 0, 0.2);
}
.tool-card.kubernetes {
background: linear-gradient(135deg, rgba(0, 156, 222, 0.1), rgba(0, 0, 0, 0));
border: 1px solid rgba(0, 156, 222, 0.2);
}
.tool-card.multi {
background: linear-gradient(135deg, rgba(6, 182, 212, 0.1), rgba(0, 0, 0, 0));
border: 1px solid rgba(6, 182, 212, 0.2);
}
.tool-card:hover {
transform: translateY(-5px);
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.2);
}
.tool-header {
display: flex;
align-items: center;
margin-bottom: 1rem;
}
.tool-icon {
display: flex;
align-items: center;
justify-content: center;
width: 40px;
height: 40px;
border-radius: 8px;
margin-right: 1rem;
}
.terraform .tool-icon {
color: #6a5acd;
}
.ansible .tool-icon {
color: #ff0000;
}
.kubernetes .tool-icon {
color: #009cde;
}
.multi .tool-icon {
color: #06b6d4;
}
.tool-name {
font-size: var(--font-size-lg);
margin: 0;
color: var(--text-primary);
}
.tool-description {
color: var(--text-secondary);
font-size: var(--font-size-sm);
margin-bottom: 1.25rem;
line-height: 1.6;
}
.tool-tags {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
.tool-tag {
font-size: var(--font-size-xs);
padding: 0.25rem 0.5rem;
border-radius: 4px;
background: rgba(15, 23, 42, 0.5);
color: var(--text-tertiary);
border: 1px solid var(--border-secondary);
}
.code-preview-container {
background: var(--bg-code);
border: 1px solid var(--border-primary);
border-radius: 8px;
overflow: hidden;
margin-top: 2rem;
}
.preview-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.75rem 1.25rem;
background: rgba(15, 23, 42, 0.7);
border-bottom: 1px solid var(--border-secondary);
}
.file-path {
font-family: var(--font-mono);
font-size: var(--font-size-sm);
color: var(--text-secondary);
}
.preview-badge {
font-size: var(--font-size-xs);
color: var(--accent-primary);
background: rgba(6, 182, 212, 0.1);
padding: 0.25rem 0.5rem;
border-radius: 4px;
border: 1px solid rgba(6, 182, 212, 0.3);
}
.code-preview {
margin: 0;
padding: 1.25rem;
font-family: var(--font-mono);
font-size: var(--font-size-sm);
line-height: 1.6;
overflow-x: auto;
color: var(--text-secondary);
}
@media (max-width: 768px) {
.tools-grid {
grid-template-columns: 1fr;
}
.coming-soon-card {
padding: 2rem 1.5rem;
}
h1 {
font-size: var(--font-size-3xl);
}
h2 {
font-size: var(--font-size-2xl);
}
}
</style>

View File

@ -0,0 +1,604 @@
---
// src/pages/resources/kubernetes.astro
import BaseLayout from '../../layouts/BaseLayout.astro';
import Header from '../../components/Header.astro';
import Footer from '../../components/Footer.astro';
// Assuming card animations are needed if resource cards are used
import '../../styles/card-animations.css';
const title = "Kubernetes Resources | LaForceIT";
const description = "A collection of Kubernetes manifests, deployment configurations, and best practices for container orchestration.";
// Placeholder data - replace with actual data fetching or static data
const yamlManifests = [
{
title: "Deployment",
description: "Standard Deployment with resource limits, probes, and environment variables",
filename: "deployment.yaml",
code: `apiVersion: apps/v1
kind: Deployment
metadata:
name: web-application
namespace: production
labels:
app: web-application
tier: frontend
spec:
replicas: 3
selector:
matchLabels:
app: web-application
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
template:
metadata:
labels:
app: web-application
tier: frontend
spec:
containers:
- name: web-application
image: nginx:1.21-alpine
imagePullPolicy: Always
ports:
- containerPort: 80
name: http
resources:
limits:
cpu: "500m"
memory: "512Mi"
requests:
cpu: "100m"
memory: "128Mi"
livenessProbe:
httpGet:
path: /health
port: http
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /ready
port: http
initialDelaySeconds: 5
periodSeconds: 5
env:
- name: NODE_ENV
value: "production"
volumeMounts:
- name: config-volume
mountPath: /etc/nginx/conf.d
volumes:
- name: config-volume
configMap:
name: nginx-config`,
tags: ["deployment", "production", "nginx", "probes"],
lastUpdated: "April 24, 2025",
docLink: "#" // Replace with actual link
},
{
title: "StatefulSet",
description: "StatefulSet configuration for database deployments with persistent volumes",
filename: "statefulset.yaml",
code: `apiVersion: apps/v1
kind: StatefulSet
metadata:
name: postgres-db
namespace: database
labels:
app: postgres-db
spec:
serviceName: "postgres"
replicas: 3
selector:
matchLabels:
app: postgres-db
template:
metadata:
labels:
app: postgres-db
spec:
terminationGracePeriodSeconds: 60
containers:
- name: postgres
image: postgres:14.2
ports:
- containerPort: 5432
name: postgres
env:
- name: POSTGRES_USER
valueFrom:
secretKeyRef:
name: postgres-secret
key: username
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: postgres-secret
key: password
volumeMounts:
- name: postgres-data
mountPath: /var/lib/postgresql/data
volumeClaimTemplates:
- metadata:
name: postgres-data
spec:
accessModes: [ "ReadWriteOnce" ]
storageClassName: "longhorn" # Example storage class
resources:
requests:
storage: 10Gi`,
tags: ["statefulset", "database", "postgres", "persistentvolume"],
lastUpdated: "April 23, 2025",
docLink: "#" // Replace with actual link
}
// Add more YAML manifest objects here
];
const helmCharts = [ /* Placeholder for Helm Chart data */ ];
const operators = [ /* Placeholder for Operator data */ ];
const bestPractices = [ /* Placeholder for Best Practice data */ ];
---
<BaseLayout {title} {description}>
<Header slot="header" />
<div class="container">
<div class="page-header">
<h1>Kubernetes Resources</h1>
<div class="header-accent"></div>
</div>
<div class="resources-container">
<div class="resources-tabs">
<button class="resources-tab active" data-tab="yaml-manifests">YAML Manifests</button>
<button class="resources-tab" data-tab="helm-charts">Helm Charts</button>
<button class="resources-tab" data-tab="operators">Operators</button>
<button class="resources-tab" data-tab="best-practices">Best Practices</button>
</div>
<!-- YAML Manifests Tab Content -->
<div class="tab-content active" id="yaml-manifests">
<h2 class="section-title">Kubernetes YAML Manifests</h2>
<p class="section-description">
A collection of production-ready Kubernetes manifest examples for various workload types and configuration patterns.
</p>
<div class="resources-grid">
{yamlManifests.map(manifest => (
<div class="resource-card">
<div class="resource-header">
<div class="resource-icon">
{/* Placeholder Icon - Consider dynamic icons based on kind */}
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" 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>
</div>
<div>
<h3 class="resource-title">{manifest.title}</h3>
<p class="resource-description">{manifest.description}</p>
</div>
</div>
<div class="resource-body">
<div class="code-preview">
<div class="code-header">
<span class="filename">{manifest.filename}</span>
<div class="code-actions">
<button class="code-action copy-button">Copy</button>
{/* <button class="code-action">Download</button> */}
</div>
</div>
<pre class="code-block"><code>{manifest.code}</code></pre>
</div>
<div class="tags">
{manifest.tags.map(tag => <span class="tag">{tag}</span>)}
</div>
</div>
<div class="resource-footer">
<span class="last-updated">Last updated: {manifest.lastUpdated}</span>
<a href={manifest.docLink} class="resource-link" target="_blank" rel="noopener noreferrer">
View Documentation
<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="5" y1="12" x2="19" y2="12"></line>
<polyline points="12 5 19 12 12 19"></polyline>
</svg>
</a>
</div>
</div>
))}
</div>
</div>
<!-- Helm Charts Tab Content (Placeholder) -->
<div class="tab-content" id="helm-charts">
<h2 class="section-title">Helm Charts</h2>
<p class="section-description">Curated Helm charts for deploying common applications and services on Kubernetes.</p>
<div class="resources-grid">
<p>Helm chart resources coming soon...</p>
</div>
</div>
<!-- Operators Tab Content (Placeholder) -->
<div class="tab-content" id="operators">
<h2 class="section-title">Kubernetes Operators</h2>
<p class="section-description">Examples and guides for using Kubernetes Operators to automate application lifecycle management.</p>
<div class="resources-grid">
<p>Operator resources coming soon...</p>
</div>
</div>
<!-- Best Practices Tab Content (Placeholder) -->
<div class="tab-content" id="best-practices">
<h2 class="section-title">Kubernetes Best Practices</h2>
<p class="section-description">Tips, tricks, and best practices for managing Kubernetes clusters effectively and securely.</p>
<ul class="best-practices-list">
<li class="best-practice-item">
<div class="best-practice-icon">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"></path></svg>
</div>
<div class="best-practice-content">
<h3>Security Hardening</h3>
<p>Implement Role-Based Access Control (RBAC), Network Policies, and Secrets Management.</p>
</div>
</li>
<li class="best-practice-item">
<div class="best-practice-icon">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="10" x2="6" y2="10"></line><line x1="21" y1="6" x2="3" y2="6"></line><line x1="21" y1="14" x2="3" y2="14"></line><line x1="18" y1="18" x2="6" y2="18"></line></svg>
</div>
<div class="best-practice-content">
<h3>Resource Management</h3>
<p>Define resource requests and limits for all workloads to ensure stability and fair resource allocation.</p>
</div>
</li>
<!-- Add more best practices -->
</ul>
</div>
</div>
</div>
<Footer slot="footer" />
</BaseLayout>
<style>
/* Page-specific styles - Keep minimal, rely on global styles */
.container {
max-width: 1200px;
margin: 0 auto;
padding: 0 1.5rem;
}
.page-header {
margin: 3rem 0 4rem;
position: relative;
}
h1 {
font-size: var(--font-size-4xl);
background: linear-gradient(90deg, var(--accent-secondary), var(--accent-primary));
-webkit-background-clip: text;
background-clip: text;
color: transparent;
display: inline-block;
margin-bottom: 0.5rem;
}
.header-accent {
width: 80px;
height: 4px;
background: linear-gradient(90deg, var(--accent-secondary), var(--accent-primary));
border-radius: 2px;
}
.resources-container {
padding: 1rem 0 4rem;
}
.resources-tabs {
display: flex;
margin-bottom: 1.5rem;
border-bottom: 1px solid var(--border-primary);
overflow-x: auto;
-webkit-overflow-scrolling: touch; /* Smooth scrolling on iOS */
scrollbar-width: none; /* Hide scrollbar for Firefox */
}
.resources-tabs::-webkit-scrollbar {
display: none; /* Hide scrollbar for Chrome, Safari, Edge */
}
.resources-tab {
padding: 0.75rem 1.5rem;
font-size: var(--font-size-base);
color: var(--text-secondary);
cursor: pointer;
transition: all 0.3s ease;
border-bottom: 2px solid transparent;
white-space: nowrap; /* Prevent tabs from wrapping */
flex-shrink: 0; /* Prevent tabs from shrinking */
}
.resources-tab.active {
color: var(--accent-secondary);
border-bottom-color: var(--accent-secondary);
font-weight: 600;
}
.resources-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
gap: 1.5rem;
margin-bottom: 3rem;
}
/* Use styles from card-animations.css for .resource-card */
.resource-header {
padding: 1.25rem;
border-bottom: 1px solid var(--border-secondary);
display: flex;
align-items: center;
gap: 1rem;
}
.resource-icon {
color: var(--accent-secondary);
flex-shrink: 0;
}
.resource-icon svg {
width: 24px;
height: 24px;
}
.resource-title {
font-size: var(--font-size-xl);
color: var(--text-primary);
margin-bottom: 0.25rem;
}
.resource-description {
font-size: var(--font-size-sm);
color: var(--text-secondary);
margin-top: 0.5rem;
}
.resource-body {
padding: 1.25rem;
flex-grow: 1; /* Ensure body takes remaining space */
display: flex;
flex-direction: column;
}
.code-preview {
background: var(--bg-code);
border-radius: 8px;
overflow: hidden;
margin-top: 1rem;
border: 1px solid var(--border-secondary);
flex-grow: 1; /* Allow preview to grow */
display: flex;
flex-direction: column;
min-height: 200px; /* Ensure a minimum height */
}
.code-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.75rem 1rem;
background: rgba(0, 0, 0, 0.2);
border-bottom: 1px solid var(--border-secondary);
}
.filename {
font-family: var(--font-mono);
font-size: var(--font-size-sm);
color: var(--text-secondary);
}
.code-actions {
display: flex;
align-items: center;
gap: 0.5rem;
}
.code-action {
font-size: var(--font-size-xs);
color: var(--text-secondary);
background: rgba(30, 41, 59, 0.5);
padding: 0.25rem 0.5rem;
border-radius: 4px;
cursor: pointer;
transition: all 0.2s ease;
border: none;
}
.code-action:hover {
background: rgba(59, 130, 246, 0.2);
color: var(--accent-secondary);
}
.code-block {
margin: 0 !important; /* Override potential global styles */
padding: 1rem !important;
background: transparent !important;
font-family: var(--font-mono) !important;
font-size: var(--font-size-sm) !important;
line-height: 1.6 !important;
overflow: auto !important; /* Changed from overflow-x */
color: var(--text-secondary) !important;
flex-grow: 1; /* Allow code block to fill space */
max-height: 300px; /* Limit height */
}
/* Ensure pre takes full height */
.code-block pre {
height: 100%;
margin: 0;
padding: 0;
}
.tags {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
margin-top: 1rem;
}
/* .tag styling is now in card-animations.css */
.resource-footer {
padding: 1rem 1.25rem;
border-top: 1px solid var(--border-secondary);
display: flex;
justify-content: space-between;
align-items: center;
margin-top: auto; /* Push footer to bottom */
}
.last-updated {
font-size: var(--font-size-xs);
color: var(--text-tertiary);
}
.resource-link {
font-size: var(--font-size-sm);
color: var(--accent-secondary);
text-decoration: none;
display: flex;
align-items: center;
gap: 0.5rem;
}
.resource-link:hover {
text-decoration: underline;
}
.best-practices-list {
list-style: none;
}
.best-practice-item {
display: flex;
gap: 1rem;
padding: 1rem 0;
border-bottom: 1px solid var(--border-secondary);
}
.best-practice-item:last-child {
border-bottom: none;
}
.best-practice-icon {
color: var(--accent-secondary);
flex-shrink: 0;
margin-top: 0.25rem; /* Align icon better */
}
.best-practice-icon svg {
width: 20px;
height: 20px;
}
.best-practice-content h3 {
font-size: var(--font-size-lg);
margin-bottom: 0.5rem;
color: var(--text-primary);
}
.best-practice-content p {
font-size: var(--font-size-sm);
color: var(--text-secondary);
}
.section-title {
font-size: var(--font-size-2xl);
margin-bottom: 1.5rem;
color: var(--text-primary);
}
.section-description {
font-size: var(--font-size-lg);
color: var(--text-secondary);
margin-bottom: 2rem;
max-width: 800px;
}
.tab-content {
display: none;
}
.tab-content.active {
display: block;
}
@media (max-width: 768px) {
.resources-grid {
grid-template-columns: 1fr;
}
h1 {
font-size: var(--font-size-3xl);
}
.section-title {
font-size: var(--font-size-xl);
}
.section-description {
font-size: var(--font-size-base);
}
}
</style>
<script>
document.addEventListener('DOMContentLoaded', () => {
const tabs = document.querySelectorAll('.resources-tab');
const tabContents = document.querySelectorAll('.tab-content');
tabs.forEach(tab => {
tab.addEventListener('click', () => {
const targetTab = tab.getAttribute('data-tab');
// Deactivate all tabs and content
tabs.forEach(t => t.classList.remove('active'));
tabContents.forEach(c => c.classList.remove('active'));
// Activate the clicked tab and corresponding content
tab.classList.add('active');
const activeContent = document.getElementById(targetTab);
if (activeContent) {
activeContent.classList.add('active');
}
});
});
// Copy button functionality
document.querySelectorAll('.copy-button').forEach(button => {
button.addEventListener('click', (event) => {
const codeBlock = event.target.closest('.code-preview').querySelector('.code-block code');
if (codeBlock) {
navigator.clipboard.writeText(codeBlock.textContent || '')
.then(() => {
event.target.textContent = 'Copied!';
setTimeout(() => {
event.target.textContent = 'Copy';
}, 2000);
})
.catch(err => {
console.error('Failed to copy text: ', err);
event.target.textContent = 'Error';
setTimeout(() => {
event.target.textContent = 'Copy';
}, 2000);
});
}
});
});
});
</script>

View File

@ -0,0 +1,538 @@
---
// src/pages/resources/tutorials.astro
import BaseLayout from '../../layouts/BaseLayout.astro';
import Header from '../../components/Header.astro';
import Footer from '../../components/Footer.astro';
const title = "Tutorials | LaForceIT";
const description = "Step-by-step tutorials on DevOps, Kubernetes, home lab setup, infrastructure automation, and more.";
---
<BaseLayout title={title} description={description}>
<Header slot="header" />
<div class="container">
<div class="page-header">
<h1>Tutorials</h1>
<div class="header-accent"></div>
</div>
<div class="coming-soon-container">
<div class="coming-soon-card">
<div class="icon-container">
<svg xmlns="http://www.w3.org/2000/svg" width="60" height="60" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z"></path>
<path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"></path>
</svg>
</div>
<h2>Tutorials Coming Soon</h2>
<p class="description">
This section will feature comprehensive, step-by-step tutorials on a wide range of DevOps, infrastructure automation, and home lab topics. Each tutorial will include detailed explanations, code samples, diagrams, and real-world examples to help you master these technologies.
</p>
<div class="featured-tutorials">
<div class="featured-header">
<div class="featured-title">Featured Tutorials Preview</div>
<div class="featured-subtitle">Coming Soon</div>
</div>
<div class="tutorials-grid">
<div class="tutorial-card">
<div class="tutorial-top">
<div class="tutorial-icon">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M21 10H3M21 6H3M21 14H3M21 18H3"></path>
</svg>
</div>
<div class="tutorial-tags">
<span class="tutorial-tag">Kubernetes</span>
<span class="tutorial-tag">Beginner</span>
</div>
</div>
<h3 class="tutorial-title">Setting Up a Kubernetes Cluster with K3s</h3>
<p class="tutorial-excerpt">
A comprehensive guide to setting up a lightweight Kubernetes cluster using K3s on minimal hardware.
</p>
<div class="tutorial-meta">
<div class="tutorial-duration">
<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">
<circle cx="12" cy="12" r="10"></circle>
<polyline points="12 6 12 12 16 14"></polyline>
</svg>
<span>30 min read</span>
</div>
<span class="tutorial-status">Coming Soon</span>
</div>
</div>
<div class="tutorial-card">
<div class="tutorial-top">
<div class="tutorial-icon">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"></path>
<polyline points="7.5 4.21 12 6.81 16.5 4.21"></polyline>
<polyline points="7.5 19.79 7.5 14.6 3 12"></polyline>
<polyline points="21 12 16.5 14.6 16.5 19.79"></polyline>
<polyline points="3.27 6.96 12 12.01 20.73 6.96"></polyline>
<line x1="12" y1="22.08" x2="12" y2="12"></line>
</svg>
</div>
<div class="tutorial-tags">
<span class="tutorial-tag">Terraform</span>
<span class="tutorial-tag">AWS</span>
</div>
</div>
<h3 class="tutorial-title">Infrastructure as Code with Terraform on AWS</h3>
<p class="tutorial-excerpt">
Learn how to provision and manage AWS infrastructure using Terraform modules and best practices.
</p>
<div class="tutorial-meta">
<div class="tutorial-duration">
<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">
<circle cx="12" cy="12" r="10"></circle>
<polyline points="12 6 12 12 16 14"></polyline>
</svg>
<span>45 min read</span>
</div>
<span class="tutorial-status">Coming Soon</span>
</div>
</div>
<div class="tutorial-card">
<div class="tutorial-top">
<div class="tutorial-icon">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M5 5.5A3.5 3.5 0 0 1 8.5 2H12v7H8.5A3.5 3.5 0 0 1 5 5.5z"></path>
<path d="M12 2h3.5a3.5 3.5 0 1 1 0 7H12V2z"></path>
<path d="M12 12.5a3.5 3.5 0 1 1 7 0 3.5 3.5 0 1 1-7 0z"></path>
<path d="M5 19.5A3.5 3.5 0 0 1 8.5 16H12v3.5a3.5 3.5 0 1 1-7 0z"></path>
<path d="M5 12.5A3.5 3.5 0 0 1 8.5 9H12v7H8.5A3.5 3.5 0 0 1 5 12.5z"></path>
</svg>
</div>
<div class="tutorial-tags">
<span class="tutorial-tag">GitOps</span>
<span class="tutorial-tag">ArgoCD</span>
</div>
</div>
<h3 class="tutorial-title">Setting Up GitOps Workflows with ArgoCD</h3>
<p class="tutorial-excerpt">
A step-by-step guide to implementing GitOps principles using ArgoCD for
<h3 class="tutorial-title">Setting Up GitOps Workflows with ArgoCD</h3>
<p class="tutorial-excerpt">
A step-by-step guide to implementing GitOps principles using ArgoCD for continuous deployment to Kubernetes clusters.
</p>
<div class="tutorial-meta">
<div class="tutorial-duration">
<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">
<circle cx="12" cy="12" r="10"></circle>
<polyline points="12 6 12 12 16 14"></polyline>
</svg>
<span>50 min read</span>
</div>
<span class="tutorial-status">Coming Soon</span>
</div>
</div>
<div class="tutorial-card">
<div class="tutorial-top">
<div class="tutorial-icon">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M22 12H2"></path>
<path d="M5.45 5.11L2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z"></path>
<line x1="6" y1="16" x2="6.01" y2="16"></line>
<line x1="10" y1="16" x2="10.01" y2="16"></line>
</svg>
</div>
<div class="tutorial-tags">
<span class="tutorial-tag">Networking</span>
<span class="tutorial-tag">Home Lab</span>
</div>
</div>
<h3 class="tutorial-title">Building a Secure Home Lab Network</h3>
<p class="tutorial-excerpt">
Learn how to design and implement a secure, segmented network for your home lab environment with VLANs and proper firewall rules.
</p>
<div class="tutorial-meta">
<div class="tutorial-duration">
<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">
<circle cx="12" cy="12" r="10"></circle>
<polyline points="12 6 12 12 16 14"></polyline>
</svg>
<span>35 min read</span>
</div>
<span class="tutorial-status">Coming Soon</span>
</div>
</div>
</div>
</div>
<div class="topic-categories">
<h3 class="categories-title">Tutorial Categories</h3>
<div class="categories-grid">
<div class="category-pill">
<div class="category-icon">🚢</div>
<span class="category-name">Kubernetes</span>
</div>
<div class="category-pill">
<div class="category-icon">🏠</div>
<span class="category-name">Home Lab</span>
</div>
<div class="category-pill">
<div class="category-icon">🔄</div>
<span class="category-name">CI/CD</span>
</div>
<div class="category-pill">
<div class="category-icon">📦</div>
<span class="category-name">Docker</span>
</div>
<div class="category-pill">
<div class="category-icon">🌐</div>
<span class="category-name">Networking</span>
</div>
<div class="category-pill">
<div class="category-icon">🔒</div>
<span class="category-name">Security</span>
</div>
<div class="category-pill">
<div class="category-icon">📊</div>
<span class="category-name">Monitoring</span>
</div>
<div class="category-pill">
<div class="category-icon">⚙️</div>
<span class="category-name">Automation</span>
</div>
</div>
</div>
<div class="newsletter-signup">
<div class="newsletter-content">
<h3 class="newsletter-title">Get Notified About New Tutorials</h3>
<p class="newsletter-text">Subscribe to receive notifications when new tutorials are published.</p>
</div>
<div class="newsletter-form">
<input type="email" placeholder="Enter your email" class="newsletter-input" />
<button class="newsletter-button">Subscribe</button>
</div>
</div>
</div>
</div>
</div>
</div>
<Footer slot="footer" />
</BaseLayout>
<style>
.page-header {
margin: 3rem 0 4rem;
position: relative;
}
h1 {
font-size: var(--font-size-4xl);
background: linear-gradient(90deg, var(--accent-tertiary), var(--accent-primary));
-webkit-background-clip: text;
background-clip: text;
color: transparent;
display: inline-block;
margin-bottom: 0.5rem;
}
.header-accent {
width: 80px;
height: 4px;
background: linear-gradient(90deg, var(--accent-tertiary), var(--accent-primary));
border-radius: 2px;
}
.coming-soon-container {
display: flex;
justify-content: center;
padding: 2rem 0 4rem;
}
.coming-soon-card {
background: var(--card-bg);
border: 1px solid var(--card-border);
border-radius: 16px;
padding: 2.5rem;
max-width: 800px;
width: 100%;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
position: relative;
overflow: hidden;
}
.coming-soon-card::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: radial-gradient(circle at 50% 50%, rgba(139, 92, 246, 0.1), transparent 70%);
pointer-events: none;
}
.icon-container {
display: flex;
justify-content: center;
margin-bottom: 1.5rem;
color: var(--accent-tertiary);
}
h2 {
text-align: center;
margin-bottom: 1.5rem;
font-size: var(--font-size-3xl);
color: var(--text-primary);
}
.description {
text-align: center;
margin-bottom: 2.5rem;
color: var(--text-secondary);
font-size: var(--font-size-lg);
line-height: 1.7;
}
.featured-tutorials {
margin-top: 2rem;
}
.featured-header {
display: flex;
justify-content: space-between;
align-items: baseline;
margin-bottom: 1.5rem;
}
.featured-title {
font-size: var(--font-size-xl);
color: var(--text-primary);
font-weight: 600;
}
.featured-subtitle {
font-size: var(--font-size-sm);
color: var(--accent-tertiary);
}
.tutorials-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 1.5rem;
margin-bottom: 2.5rem;
}
.tutorial-card {
background: rgba(30, 41, 59, 0.5);
border: 1px solid var(--border-secondary);
border-radius: 12px;
padding: 1.5rem;
transition: all 0.3s ease;
display: flex;
flex-direction: column;
}
.tutorial-card:hover {
transform: translateY(-5px);
border-color: var(--accent-tertiary);
box-shadow: 0 10px 20px rgba(139, 92, 246, 0.1);
}
.tutorial-top {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 1rem;
}
.tutorial-icon {
color: var(--accent-tertiary);
}
.tutorial-tags {
display: flex;
gap: 0.5rem;
}
.tutorial-tag {
font-size: var(--font-size-xs);
padding: 0.25rem 0.5rem;
border-radius: 4px;
background: rgba(139, 92, 246, 0.1);
color: var(--accent-tertiary);
border: 1px solid rgba(139, 92, 246, 0.2);
}
.tutorial-title {
font-size: var(--font-size-lg);
color: var(--text-primary);
margin-bottom: 0.75rem;
line-height: 1.4;
}
.tutorial-excerpt {
color: var(--text-secondary);
font-size: var(--font-size-sm);
line-height: 1.6;
margin-bottom: 1.5rem;
flex-grow: 1;
}
.tutorial-meta {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: auto;
}
.tutorial-duration {
display: flex;
align-items: center;
gap: 0.5rem;
color: var(--text-tertiary);
font-size: var(--font-size-xs);
}
.tutorial-status {
font-size: var(--font-size-xs);
color: var(--accent-primary);
}
.topic-categories {
margin-bottom: 2.5rem;
}
.categories-title {
font-size: var(--font-size-lg);
color: var(--text-primary);
margin-bottom: 1.25rem;
}
.categories-grid {
display: flex;
flex-wrap: wrap;
gap: 1rem;
}
.category-pill {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 1rem;
background: rgba(30, 41, 59, 0.5);
border: 1px solid var(--border-secondary);
border-radius: 50px;
transition: all 0.3s ease;
}
.category-pill:hover {
background: rgba(30, 41, 59, 0.8);
transform: translateY(-2px);
border-color: var(--accent-tertiary);
}
.category-icon {
font-size: 1.25rem;
}
.category-name {
color: var(--text-secondary);
font-size: var(--font-size-sm);
}
.newsletter-signup {
margin-top: 2.5rem;
background: linear-gradient(135deg, rgba(139, 92, 246, 0.1), rgba(6, 182, 212, 0.1));
border: 1px solid rgba(139, 92, 246, 0.2);
border-radius: 12px;
padding: 1.5rem 2rem;
display: flex;
align-items: center;
justify-content: space-between;
}
.newsletter-content {
flex: 1;
}
.newsletter-title {
font-size: var(--font-size-lg);
color: var(--text-primary);
margin-bottom: 0.5rem;
}
.newsletter-text {
color: var(--text-secondary);
font-size: var(--font-size-sm);
margin: 0;
}
.newsletter-form {
display: flex;
gap: 0.75rem;
}
.newsletter-input {
background: rgba(15, 23, 42, 0.6);
border: 1px solid var(--border-secondary);
border-radius: 8px;
padding: 0.75rem 1rem;
color: var(--text-primary);
font-size: var(--font-size-sm);
min-width: 250px;
}
.newsletter-input:focus {
outline: none;
border-color: var(--accent-tertiary);
box-shadow: 0 0 0 2px rgba(139, 92, 246, 0.2);
}
.newsletter-button {
background: linear-gradient(90deg, var(--accent-tertiary), var(--accent-primary));
color: var(--bg-primary);
border: none;
padding: 0.75rem 1.25rem;
border-radius: 8px;
font-weight: 500;
font-size: var(--font-size-sm);
cursor: pointer;
transition: all 0.3s ease;
}
.newsletter-button:hover {
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(139, 92, 246, 0.2);
}
@media (max-width: 768px) {
.tutorials-grid {
grid-template-columns: 1fr;
}
.newsletter-signup {
flex-direction: column;
align-items: stretch;
gap: 1.5rem;
}
.newsletter-form {
flex-direction: column;
}
.coming-soon-card {
padding: 2rem 1.5rem;
}
h1 {
font-size: var(--font-size-3xl);
}
h2 {
font-size: var(--font-size-2xl);
}
}
</style>

688
src/pages/tech-stack.astro Normal file
View File

@ -0,0 +1,688 @@
---
// src/pages/tech-stack.astro
import BaseLayout from '../layouts/BaseLayout.astro';
import '../styles/card-animations.css';
import Header from '../components/Header.astro';
import Footer from '../components/Footer.astro';
const title = "Tech Stack | LaForceIT";
const description = "Explore the technology stack that powers LaForceIT projects, infrastructure, and home lab environment.";
// Define tech categories
const techCategories = [
{
name: "Infrastructure & Hosting",
icon: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="2" width="20" height="8" rx="2" ry="2"></rect><rect x="2" y="14" width="20" height="8" rx="2" ry="2"></rect><line x1="6" y1="6" x2="6.01" y2="6"></line><line x1="6" y1="18" x2="6.01" y2="18"></line></svg>`,
filter: "infrastructure",
technologies: [
{
name: "Proxmox VE",
description: "Hypervisor for virtualization and container management",
url: "https://www.proxmox.com/en/proxmox-ve",
},
{
name: "TrueNAS Scale",
description: "Storage platform with built-in virtualization capabilities",
url: "https://www.truenas.com/truenas-scale/",
},
{
name: "pfSense",
description: "Open source firewall/router with advanced features",
url: "https://www.pfsense.org/",
},
{
name: "UniFi Network",
description: "Enterprise-grade networking equipment and management",
url: "https://ui.com/",
}
]
},
{
name: "Container Orchestration",
icon: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2L3 7l9 5 9-5-9-5z"></path><path d="M3 7v10l9 5 9-5V7"></path><line x1="13.73" y1="21.755" x2="20.95" y2="16.265"></line><line x1="3.05" y1="7.735" x2="10.27" y2="13.225"></line><line x1="13.73" y1="13.225" x2="20.95" y2="7.735"></line><line x1="3.05" y1="16.265" x2="10.27" y2="21.755"></line></svg>`,
filter: "containers",
technologies: [
{
name: "Kubernetes",
description: "Container orchestration platform for application scaling",
url: "https://kubernetes.io/",
},
{
name: "Rancher",
description: "Multi-cluster Kubernetes management interface",
url: "https://rancher.com/",
},
{
name: "ArgoCD",
description: "GitOps continuous delivery tool for Kubernetes",
url: "https://argoproj.github.io/argo-cd/",
},
{
name: "Longhorn",
description: "Cloud-native distributed storage for Kubernetes",
url: "https://longhorn.io/",
}
]
},
{
name: "CI/CD & Automation",
icon: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="16 18 22 12 16 6"></polyline><polyline points="8 6 2 12 8 18"></polyline><line x1="19" y1="12" x2="5" y2="12"></line></svg>`,
filter: "automation",
technologies: [
{
name: "Terraform",
description: "Infrastructure as Code for cloud and on-premises resources",
url: "https://www.terraform.io/",
},
{
name: "Ansible",
description: "Automation platform for configuration management",
url: "https://www.ansible.com/",
},
{
name: "GitHub Actions",
description: "CI/CD pipeline automation integrated with GitHub",
url: "https://github.com/features/actions",
},
{
name: "Flux CD",
description: "GitOps tool for keeping Kubernetes clusters in sync",
url: "https://fluxcd.io/",
}
]
},
{
name: "Monitoring & Observability",
icon: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect><circle cx="8.5" cy="8.5" r="1.5"></circle><polyline points="21 15 16 10 5 21"></polyline></svg>`,
filter: "monitoring",
technologies: [
{
name: "Prometheus",
description: "Monitoring system with time-series database",
url: "https://prometheus.io/",
},
{
name: "Grafana",
description: "Visualization and analytics platform for metrics",
url: "https://grafana.com/",
},
{
name: "Loki",
description: "Log aggregation system designed for Kubernetes",
url: "https://grafana.com/oss/loki/",
},
{
name: "Uptime Kuma",
description: "Self-hosted monitoring tool for websites and services",
url: "https://github.com/louislam/uptime-kuma",
}
]
},
{
name: "Development & Web",
icon: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="16 18 22 12 16 6"></polyline><polyline points="8 6 2 12 8 18"></polyline></svg>`,
filter: "development",
technologies: [
{
name: "Astro",
description: "Modern static site builder with minimal JavaScript",
url: "https://astro.build/",
},
{
name: "Docker",
description: "Container platform for application packaging",
url: "https://www.docker.com/",
},
{
name: "Tailwind CSS",
description: "Utility-first CSS framework for rapid UI development",
url: "https://tailwindcss.com/",
},
{
name: "Node.js",
description: "JavaScript runtime for building server-side applications",
url: "https://nodejs.org/",
}
]
}
];
// Tech stack filters
const filters = [
"all",
"infrastructure",
"containers",
"automation",
"monitoring",
"development"
];
---
<BaseLayout title={title} description={description}>
<Header slot="header" />
<main>
<div class="container">
<div class="page-header">
<h1 class="page-title">Tech Stack</h1>
<p class="page-description">
Explore the technologies, tools, and platforms that power the LaForceIT infrastructure, projects, and home lab environment. This curated stack focuses on enterprise-grade solutions that balance performance, reliability, and maintainability.
</p>
</div>
<!-- Search & Filters -->
<div class="resource-search">
<div class="search-icon">
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="11" cy="11" r="8"></circle>
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
</svg>
</div>
<input type="text" placeholder="Search technologies..." class="search-input" id="tech-search-input">
</div>
<div class="resource-filters">
{filters.map(filter => (
<button class={`filter-button ${filter === 'all' ? 'active' : ''}`} data-filter={filter}>
{filter.charAt(0).toUpperCase() + filter.slice(1)}
</button>
))}
</div>
<!-- Tech Stack Grid -->
<div class="tech-categories">
{techCategories.map((category, index) => (
<div class="tech-category-section" id={`category-${index}`}>
<div class="category-header">
<div class="category-icon" set:html={category.icon}></div>
<h2 class="category-title">{category.name}</h2>
</div>
<div class="technology-grid">
{category.technologies.map(tech => (
<a href={tech.url} target="_blank" rel="noopener noreferrer" class={`resource-card ${category.filter}`}>
<div class="resource-icon-wrapper">
<div class="tech-logo">{tech.name.substring(0, 2)}</div>
<div class="resource-icon-particles">
<div class="resource-icon-particle"></div>
<div class="resource-icon-particle"></div>
<div class="resource-icon-particle"></div>
<div class="resource-icon-particle"></div>
<div class="resource-icon-particle"></div>
</div>
</div>
<div class="resource-content">
<h3 class="resource-title">{tech.name}</h3>
<p class="resource-description">{tech.description}</p>
<div class="resource-link">
Learn More
<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="5" y1="12" x2="19" y2="12"></line>
<polyline points="12 5 19 12 12 19"></polyline>
</svg>
</div>
<div class="resource-tags">
<span class="resource-tag">{category.filter}</span>
</div>
</div>
<div class="shine-effect"></div>
</a>
))}
</div>
</div>
))}
<!-- Empty State (hidden by default) -->
<div class="no-results" style="display: none;">
<div class="no-results-icon">
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1" stroke-linecap="round" stroke-linejoin="round">
<circle cx="11" cy="11" r="8"></circle>
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
<line x1="11" y1="8" x2="11" y2="14"></line>
<line x1="8" y1="11" x2="14" y2="11"></line>
</svg>
</div>
<h3>No matching technologies found</h3>
<p>Try adjusting your search terms or filters to find what you're looking for.</p>
</div>
</div>
<!-- CTA Section -->
<div class="cta-section">
<div class="cta-content">
<h2 class="cta-title">Build Your Own Tech Stack</h2>
<p class="cta-text">Interested in setting up a similar tech stack in your own environment? Check out my detailed tutorials and guides for step-by-step instructions on deploying and configuring these technologies.</p>
</div>
<div class="cta-buttons">
<a href="/resources/tutorials" class="cta-button primary">View Tutorials</a>
<a href="https://argobox.com" class="cta-button secondary" target="_blank" rel="noopener noreferrer">Explore HomeLab</a>
</div>
</div>
</div>
</main>
<Footer slot="footer" />
</BaseLayout>
<script>
document.addEventListener('DOMContentLoaded', () => {
// Filter functionality
const filterButtons = document.querySelectorAll('.filter-button');
const resourceCards = document.querySelectorAll('.resource-card');
const noResults = document.querySelector('.no-results');
const searchInput = document.getElementById('tech-search-input');
const categoryContainers = document.querySelectorAll('.tech-category-section');
function filterAndSearch() {
const searchTerm = searchInput ? searchInput.value.toLowerCase().trim() : '';
const activeFilter = document.querySelector('.filter-button.active')?.getAttribute('data-filter') || 'all';
let visibleCount = 0;
resourceCards.forEach(card => {
const title = card.querySelector('.resource-title')?.textContent.toLowerCase() || '';
const description = card.querySelector('.resource-description')?.textContent.toLowerCase() || '';
const cardClass = Array.from(card.classList).find(cls => filters.includes(cls)) || '';
const matchesSearch = searchTerm === '' || title.includes(searchTerm) || description.includes(searchTerm);
const matchesFilter = activeFilter === 'all' || cardClass === activeFilter;
if (matchesSearch && matchesFilter) {
card.style.display = 'flex';
visibleCount++;
} else {
card.style.display = 'none';
}
});
// Show/hide category sections based on visible cards
categoryContainers.forEach(container => {
const hasVisibleCards = Array.from(container.querySelectorAll('.resource-card')).some(card => card.style.display !== 'none');
container.style.display = hasVisibleCards ? 'block' : 'none';
});
if (noResults) {
noResults.style.display = visibleCount === 0 ? 'block' : 'none';
}
}
filterButtons.forEach(button => {
button.addEventListener('click', () => {
filterButtons.forEach(btn => btn.classList.remove('active'));
button.classList.add('active');
filterAndSearch(); // Re-run search when filter changes
});
});
if (searchInput) {
searchInput.addEventListener('input', filterAndSearch);
}
// Initial filter/search on load
filterAndSearch();
// Card hover effect enhancement (particle animation)
resourceCards.forEach(card => {
card.addEventListener('mouseenter', () => {
const particles = card.querySelectorAll('.resource-icon-particle');
particles.forEach(particle => {
const x = Math.random() * 100;
const y = Math.random() * 100;
particle.style.top = `${y}%`;
particle.style.left = `${x}%`;
});
});
});
});
</script>
<style>
.page-header {
text-align: center;
margin-bottom: 3rem;
}
.page-title {
font-size: 3rem;
font-weight: 700;
margin-bottom: 1rem;
background: linear-gradient(to right, var(--accent-primary), var(--accent-secondary));
-webkit-background-clip: text;
background-clip: text;
color: transparent;
position: relative;
display: inline-block;
}
.page-title::after {
content: '';
position: absolute;
bottom: -10px;
left: 50%;
transform: translateX(-50%);
width: 100px;
height: 3px;
background: linear-gradient(to right, var(--accent-primary), var(--accent-secondary));
border-radius: 3px;
}
.page-description {
max-width: 800px;
margin: 0 auto;
color: var(--text-secondary);
font-size: 1.1rem;
}
/* Resource Filtering */
.resource-filters {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 0.75rem;
margin: 2rem 0;
}
.filter-button {
padding: 0.5rem 1.25rem;
background: var(--bg-tertiary);
border: 1px solid var(--border-primary);
color: var(--text-secondary);
border-radius: 20px;
font-size: 0.9rem;
cursor: pointer;
transition: all 0.3s ease;
}
.filter-button:hover {
background: linear-gradient(90deg, rgba(6, 182, 212, 0.1), rgba(59, 130, 246, 0.1));
color: var(--text-primary);
border-color: var(--accent-primary);
box-shadow: 0 0 10px var(--glow-primary);
}
.filter-button.active {
background: linear-gradient(90deg, rgba(6, 182, 212, 0.2), rgba(59, 130, 246, 0.2));
color: var(--accent-primary);
border-color: var(--accent-primary);
box-shadow: 0 0 15px var(--glow-primary);
}
/* Search Box */
.resource-search {
display: flex;
margin: 0 auto 2rem;
max-width: 500px;
position: relative;
}
.search-input {
width: 100%;
padding: 0.75rem 1rem 0.75rem 2.75rem;
background: var(--bg-tertiary);
border: 1px solid var(--border-primary);
border-radius: 8px;
color: var(--text-primary);
font-size: 1rem;
transition: all 0.3s ease;
}
.search-input:focus {
outline: none;
border-color: var(--accent-primary);
box-shadow: 0 0 0 3px var(--glow-primary);
}
.search-icon {
position: absolute;
left: 1rem;
top: 50%;
transform: translateY(-50%);
color: var(--text-tertiary);
}
/* Tech Categories */
.tech-categories {
display: flex;
flex-direction: column;
gap: 4rem;
margin-bottom: 4rem;
}
.tech-category-section {
margin-bottom: 2rem;
}
.category-header {
display: flex;
align-items: center;
gap: 1rem;
margin-bottom: 1.5rem;
position: relative;
}
.category-icon {
width: 48px;
height: 48px;
padding: 0.5rem;
border-radius: 12px;
background: linear-gradient(135deg, rgba(6, 182, 212, 0.1), rgba(59, 130, 246, 0.1));
display: flex;
align-items: center;
justify-content: center;
color: var(--accent-primary);
}
.category-icon svg {
width: 24px;
height: 24px;
}
.category-title {
font-size: 1.5rem;
color: var(--text-primary);
position: relative;
}
.category-title::after {
content: '';
position: absolute;
bottom: -0.5rem;
left: 0;
width: 50px;
height: 2px;
background: linear-gradient(90deg, var(--accent-primary), transparent);
border-radius: 2px;
}
.technology-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 1.5rem;
}
/* Styles for .resource-card, .resource-icon-wrapper, etc. are now in card-animations.css */
.resource-content {
padding: 1.5rem;
flex: 1;
display: flex;
flex-direction: column;
}
.resource-title {
font-size: 1.3rem;
font-weight: 600;
margin-bottom: 0.75rem;
color: var(--text-primary);
}
.resource-description {
color: var(--text-secondary);
margin-bottom: 1.5rem;
flex: 1;
}
.resource-link {
display: inline-flex;
align-items: center;
text-decoration: none;
color: var(--accent-primary);
font-weight: 500;
transition: all 0.3s ease;
position: relative;
margin-bottom: 1rem;
}
.resource-link svg {
margin-left: 0.5rem;
transition: transform 0.3s ease;
}
.resource-card:hover .resource-link svg {
transform: translateX(5px);
}
.resource-tags {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
margin-top: auto;
}
/* .resource-tag styling is now in card-animations.css */
/* Shine effect styling is now in card-animations.css */
/* Empty State */
.no-results {
text-align: center;
padding: 3rem;
color: var(--text-secondary);
background: var(--bg-tertiary);
border-radius: 12px;
border: 1px dashed var(--border-primary);
margin-top: 2rem;
display: none; /* Hidden by default */
}
.no-results-icon {
color: var(--text-tertiary);
margin-bottom: 1rem;
}
.no-results-icon svg {
width: 64px;
height: 64px;
stroke-width: 1;
opacity: 0.5;
}
/* CTA Section */
.cta-section {
background: var(--card-bg);
border: 1px solid var(--card-border);
border-radius: 16px;
padding: 3rem;
margin: 2rem 0 4rem;
display: flex;
align-items: center;
justify-content: space-between;
gap: 2rem;
position: relative;
overflow: hidden;
}
.cta-section::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: radial-gradient(circle at 30% 50%, rgba(6, 182, 212, 0.1), transparent 70%);
pointer-events: none;
}
.cta-content {
flex: 1;
}
.cta-title {
font-size: 2rem;
color: var(--text-primary);
margin-bottom: 1rem;
}
.cta-text {
color: var(--text-secondary);
font-size: 1.1rem;
line-height: 1.7;
margin: 0;
}
.cta-buttons {
display: flex;
gap: 1rem;
}
.cta-button {
display: inline-block;
padding: 0.75rem 1.5rem;
border-radius: 8px;
font-weight: 500;
font-size: 1rem;
text-decoration: none;
transition: all 0.3s ease;
}
.cta-button.primary {
background: linear-gradient(90deg, var(--accent-secondary), var(--accent-primary));
color: var(--bg-primary);
box-shadow: 0 5px 15px rgba(6, 182, 212, 0.2);
}
.cta-button.secondary {
background: rgba(15, 23, 42, 0.5);
border: 1px solid var(--border-primary);
color: var(--text-primary);
}
.cta-button.primary:hover {
transform: translateY(-3px);
box-shadow: 0 8px 20px rgba(6, 182, 212, 0.3);
}
.cta-button.secondary:hover {
transform: translateY(-3px);
border-color: var(--accent-primary);
background: rgba(15, 23, 42, 0.8);
}
/* Keyframes are now in card-animations.css */
/* Responsive Adjustments */
@media (max-width: 768px) {
.technology-grid {
grid-template-columns: 1fr;
}
.cta-section {
flex-direction: column;
text-align: center;
padding: 2rem 1.5rem;
}
.cta-buttons {
flex-direction: column;
width: 100%;
}
.page-title {
font-size: 2.5rem;
}
.cta-title {
font-size: 1.75rem;
}
}
</style>

View File

@ -0,0 +1,224 @@
/* src/styles/card-animations.css */
/* Resource Cards */
.resource-card {
position: relative;
border-radius: 12px;
background: rgba(15, 23, 42, 0.7);
backdrop-filter: blur(10px);
border: 1px solid rgba(6, 182, 212, 0.1);
overflow: hidden;
transition: all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
height: 100%;
display: flex;
flex-direction: column;
text-decoration: none;
}
.resource-card:hover {
transform: translateY(-8px);
box-shadow: 0 15px 30px rgba(0, 0, 0, 0.3),
0 0 20px rgba(6, 182, 212, 0.3);
border-color: rgba(6, 182, 212, 0.4);
}
/* Icon Wrapper */
.resource-icon-wrapper {
display: flex;
align-items: center;
justify-content: center;
padding: 2rem;
background: #0f1e2a; /* Darker background for icon area */
position: relative;
overflow: hidden;
}
/* Icon Styles */
.resource-icon {
width: 64px;
height: 64px;
color: #06b6d4; /* Brighter cyan color for icons */
z-index: 1;
transition: transform 0.5s ease, color 0.3s ease;
}
.resource-card:hover .resource-icon {
transform: scale(1.1);
color: #0fcfe7; /* Even brighter on hover */
}
/* Tech Logo (for tech-stack.astro) */
.tech-logo {
width: 64px;
height: 64px;
border-radius: 12px;
background: linear-gradient(135deg, var(--accent-primary), var(--accent-secondary));
color: var(--bg-primary);
display: flex;
align-items: center;
justify-content: center;
font-size: 1.75rem;
font-weight: bold;
z-index: 1;
transition: transform 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
}
.resource-card:hover .tech-logo {
transform: scale(1.1);
box-shadow: 0 0 15px rgba(6, 182, 212, 0.3);
}
/* Particles */
.resource-icon-particles {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
}
.resource-icon-particle {
position: absolute;
width: 5px;
height: 5px;
border-radius: 50%;
background: var(--accent-primary);
opacity: 0; /* Hidden by default */
animation: particle-float 3s infinite ease-in-out;
}
.resource-card:hover .resource-icon-particle {
opacity: 0.7; /* Only visible on hover */
}
/* Particle and dot positions and timing */
.resource-icon-particle:nth-child(1) {
animation-duration: 3s;
animation-delay: 0s;
top: 30%;
left: 40%;
}
.resource-icon-particle:nth-child(2) {
animation-duration: 4s;
animation-delay: 0.3s;
top: 60%;
left: 30%;
}
.resource-icon-particle:nth-child(3) {
animation-duration: 3.5s;
animation-delay: 0.7s;
top: 40%;
left: 60%;
}
.resource-icon-particle:nth-child(4) {
animation-duration: 4.5s;
animation-delay: 1s;
top: 70%;
left: 50%;
}
.resource-icon-particle:nth-child(5) {
animation-duration: 5s;
animation-delay: 1.5s;
top: 50%;
left: 70%;
}
/* Background dots */
.icon-bg-dot {
position: absolute;
width: 4px;
height: 4px;
border-radius: 50%;
background-color: #06b6d4;
opacity: 0; /* Hidden by default */
z-index: 0;
}
.resource-card:hover .icon-bg-dot {
opacity: 0.3; /* Only visible on hover */
animation: float-dot 8s infinite ease-in-out;
}
.icon-bg-dot:nth-child(odd) {
animation-duration: 6s;
animation-direction: reverse;
}
/* Glow effect */
.resource-icon-wrapper::before {
content: '';
position: absolute;
width: 120px;
height: 120px;
background: radial-gradient(circle, rgba(6, 182, 212, 0.2), transparent 70%);
opacity: 0; /* Hidden by default */
transform: scale(0.8);
transition: transform 0.5s ease, opacity 0.5s ease;
}
.resource-card:hover .resource-icon-wrapper::before {
transform: scale(1.2);
opacity: 0.7; /* Only visible on hover */
}
/* Card Shine Effect */
.shine-effect {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: linear-gradient(
135deg,
rgba(255, 255, 255, 0) 0%,
rgba(255, 255, 255, 0.05) 50%,
rgba(255, 255, 255, 0) 100%
);
transform: translateX(-100%);
transition: transform 0s;
pointer-events: none;
}
.resource-card:hover .shine-effect {
transform: translateX(100%);
transition: transform 0.5s ease-in-out;
}
/* Tag styling */
.resource-tag {
font-size: 0.75rem;
padding: 0.25rem 0.75rem;
background: rgba(6, 182, 212, 0.1);
color: var(--accent-primary);
border-radius: 20px;
white-space: nowrap;
transition: all 0.3s ease;
}
.resource-tag:hover {
background: rgba(6, 182, 212, 0.2);
transform: translateY(-2px);
box-shadow: 0 3px 6px rgba(6, 182, 212, 0.2);
}
/* Animations */
@keyframes particle-float {
0% { transform: translate(0, 0) scale(0.5); opacity: 0; }
25% { opacity: 0.6; transform: translate(10px, -10px) scale(1); }
50% { transform: translate(20px, -15px) scale(1.2); opacity: 0.8; }
75% { opacity: 0.6; transform: translate(30px, -5px) scale(1); }
100% { transform: translate(40px, 0) scale(0.5); opacity: 0; }
}
@keyframes float-dot {
0% { transform: translate(0, 0); opacity: 0.2; }
25% { transform: translate(5px, -10px); opacity: 0.5; }
50% { transform: translate(15px, -5px); opacity: 0.3; }
75% { transform: translate(10px, 5px); opacity: 0.4; }
100% { transform: translate(0, 0); opacity: 0.2; }
}

View File

@ -39,27 +39,48 @@
/* Enhanced Light Mode Variables - More tech-focused, less plain white */
:root.light-mode {
--bg-primary: #f0f4f8; /* Subtle blue-gray instead of white */
--bg-secondary: #e5eaf2; /* Slightly darker secondary */
--bg-tertiary: #dae2ef; /* Even more blue tint for tertiary */
--bg-code: #e5edf7;
--text-primary: #1e293b; /* Darker primary text */
--text-secondary: #475569; /* Darker secondary text */
--text-tertiary: #64748b; /* Darker tertiary text */
--accent-primary: #0891b2; /* Slightly darker cyan */
--accent-secondary: #2563eb; /* Slightly darker blue */
--accent-tertiary: #7c3aed; /* Slightly darker violet */
--glow-primary: rgba(8, 145, 178, 0.15);
--glow-secondary: rgba(37, 99, 235, 0.15);
--glow-tertiary: rgba(124, 58, 237, 0.15);
--border-primary: rgba(37, 99, 235, 0.15); /* More visible blue-tinted borders */
--border-secondary: rgba(8, 145, 178, 0.1);
--card-bg: rgba(255, 255, 255, 0.6); /* More transparent card background */
--card-border: rgba(37, 99, 235, 0.2); /* Subtle blue border */
--ui-element: rgba(226, 232, 240, 0.7); /* More transparent UI elements */
--ui-element-hover: rgba(203, 213, 225, 0.8);
/* Base Colors - Rich, sophisticated cool neutral palette with blue undertones */
--bg-primary: #f0f4f8; /* Slightly cooler, less stark than pure white */
--bg-secondary: #e6edf5; /* Subtle blue-gray secondary that's visibly different */
--bg-tertiary: #d8e2f0; /* Deeper blue tint for tertiary with clear contrast */
--bg-code: #eaeff5; /* Distinctly colored code background for readability */
/* Text Colors - High contrast for exceptional readability */
--text-primary: #0b1b32; /* Near black with blue undertone - sharp contrast */
--text-secondary: #2d3e50; /* Rich blue-gray for secondary text with good readability */
--text-tertiary: #4a617a; /* Lighter blue-gray for tertiary text that's still readable */
/* Accent Colors - Deep, vibrant colors that pop against light backgrounds */
--accent-primary: #0277b6; /* Strong blue that stands out clearly */
--accent-secondary: #1e56b1; /* Deep, rich blue */
--accent-tertiary: #7132db; /* Vibrant violet with better contrast */
/* Enhanced Glows - More vibrant and visible */
--glow-primary: rgba(2, 119, 182, 0.25);
--glow-secondary: rgba(30, 86, 177, 0.25);
--glow-tertiary: rgba(113, 50, 219, 0.25);
/* Refined Borders - More defined with higher contrast */
--border-primary: rgba(30, 86, 177, 0.3); /* More visible blue-tinted borders */
--border-secondary: rgba(2, 119, 182, 0.2);
/* Card and UI Elements - More opaque for better definition */
--card-bg: rgba(255, 255, 255, 0.85); /* Less transparent for better contrast */
--card-border: rgba(30, 86, 177, 0.25); /* Stronger blue border */
--ui-element: rgba(230, 237, 245, 0.9); /* Less transparent UI elements */
--ui-element-hover: rgba(216, 226, 240, 0.95);
/* RGB values for gradients and advanced effects */
--bg-primary-rgb: 240, 244, 248; /* RGB for gradients */
--bg-secondary-rgb: 229, 234, 242; /* RGB for gradients */
--bg-secondary-rgb: 230, 237, 245; /* RGB for gradients */
/* Knowledge Graph specific enhancements - Critical improvements */
--graph-bg: rgba(240, 244, 248, 0.85); /* More opaque background */
--graph-border: rgba(30, 86, 177, 0.35); /* Stronger border */
--graph-node: #ffffff; /* Pure white for maximum contrast */
--graph-node-border: rgba(2, 119, 182, 0.8); /* Much more visible node borders */
--graph-line: rgba(113, 50, 219, 0.5); /* Higher opacity for clear connections */
--graph-highlight: rgba(2, 119, 182, 1); /* Full opacity for highlighted elements */
}
/* Ensure transitions for smooth theme changes */
@ -67,53 +88,119 @@
transition: background-color 0.3s ease, color 0.3s ease, border-color 0.3s ease, box-shadow 0.3s ease;
}
/* Knowledge Graph specific theme adjustments - More transparent in light mode */
/* Knowledge Graph specific theme enhancements - Glassmorphism with better definition */
:root.light-mode .graph-container {
background: rgba(248, 250, 252, 0.08); /* Much more transparent - lighter gray */
backdrop-filter: blur(2px);
border: 1px solid rgba(37, 99, 235, 0.15);
box-shadow: 0 4px 20px rgba(37, 99, 235, 0.05);
background: linear-gradient(135deg, rgba(240, 244, 248, 0.85), rgba(230, 237, 245, 0.9)); /* Gradient background instead of flat */
backdrop-filter: blur(15px);
border: 1px solid var(--graph-border);
box-shadow: 0 10px 30px rgba(30, 86, 177, 0.15), inset 0 1px 1px rgba(255, 255, 255, 0.7);
border-radius: 16px;
overflow: hidden;
}
/* Enhanced node styling for much better visibility */
:root.light-mode .node {
background: var(--graph-node);
border: 2px solid var(--graph-node-border);
box-shadow: 0 4px 15px rgba(2, 119, 182, 0.25);
transform: translateY(0);
}
:root.light-mode .node:hover {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(2, 119, 182, 0.35);
border-color: var(--accent-primary);
}
/* Connection lines with much better contrast */
:root.light-mode path.edge {
stroke: var(--graph-line) !important;
stroke-width: 1.5px !important;
}
:root.light-mode path.edge.highlighted {
stroke: var(--graph-highlight) !important;
stroke-width: 2.5px !important;
}
/* Node text labels with improved readability */
:root.light-mode .node-label {
color: var(--text-primary) !important;
background-color: rgba(255, 255, 255, 0.9) !important;
text-shadow: 0 1px 1px rgba(255, 255, 255, 0.8);
font-weight: 500 !important;
padding: 3px 8px !important;
border-radius: 4px !important;
border: 1px solid rgba(30, 86, 177, 0.15) !important;
}
:root.light-mode .node-details {
background: rgba(255, 255, 255, 0.8); /* More opaque for readability */
backdrop-filter: blur(5px);
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.05);
border: 1px solid rgba(37, 99, 235, 0.1);
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(15px);
box-shadow: 0 15px 35px rgba(2, 119, 182, 0.2);
border: 1px solid rgba(30, 86, 177, 0.25);
border-radius: 12px;
padding: 1.5rem;
}
:root.light-mode .graph-filters {
background: rgba(248, 250, 252, 0.6); /* Slightly more opaque */
backdrop-filter: blur(3px);
border: 1px solid rgba(37, 99, 235, 0.1);
background: rgba(240, 244, 248, 0.9);
backdrop-filter: blur(10px);
border: 1px solid rgba(30, 86, 177, 0.25);
border-radius: 12px;
padding: 1rem;
box-shadow: 0 5px 15px rgba(30, 86, 177, 0.1);
}
:root.light-mode .graph-filter {
color: var(--text-secondary);
border-color: var(--border-primary);
background: rgba(255, 255, 255, 0.5);
border: 1px solid var(--border-primary);
background: rgba(255, 255, 255, 0.9);
border-radius: 8px;
padding: 0.5rem 1rem;
transition: all 0.2s ease;
font-weight: 500;
}
:root.light-mode .graph-filter:hover {
background: rgba(255, 255, 255, 0.7);
background: #ffffff;
transform: translateY(-1px);
box-shadow: 0 4px 8px rgba(30, 86, 177, 0.15);
}
:root.light-mode .graph-filter.active {
background: linear-gradient(135deg, rgba(8, 145, 178, 0.1), rgba(37, 99, 235, 0.1));
border-color: rgba(37, 99, 235, 0.3);
background: linear-gradient(135deg, rgba(2, 119, 182, 0.15), rgba(30, 86, 177, 0.15));
border-color: rgba(30, 86, 177, 0.5);
box-shadow: 0 2px 10px rgba(30, 86, 177, 0.15);
color: var(--accent-primary);
font-weight: 600;
}
:root.light-mode .connections-list a {
color: var(--accent-secondary);
transition: color 0.2s ease, transform 0.2s ease;
font-weight: 500;
}
:root.light-mode .connections-list a:hover {
color: var(--accent-primary);
transform: translateX(2px);
}
:root.light-mode .node-link {
box-shadow: 0 4px 10px rgba(8, 145, 178, 0.1);
background: linear-gradient(135deg, rgba(8, 145, 178, 0.1), rgba(37, 99, 235, 0.1));
box-shadow: 0 4px 15px rgba(2, 119, 182, 0.2);
background: linear-gradient(135deg, rgba(2, 119, 182, 0.1), rgba(30, 86, 177, 0.1));
border: 1px solid rgba(30, 86, 177, 0.25);
border-radius: 8px;
transition: all 0.2s ease;
color: var(--accent-primary);
font-weight: 600;
}
:root.light-mode .node-link:hover {
background: linear-gradient(135deg, rgba(8, 145, 178, 0.2), rgba(37, 99, 235, 0.2));
background: linear-gradient(135deg, rgba(2, 119, 182, 0.15), rgba(30, 86, 177, 0.15));
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(2, 119, 182, 0.25);
}
/* Fix for code blocks in light mode */
@ -121,106 +208,233 @@
:root.light-mode code {
background-color: var(--bg-code);
color: var(--text-secondary);
border: 1px solid rgba(37, 99, 235, 0.1);
border: 1px solid rgba(30, 86, 177, 0.15);
border-radius: 6px;
}
/* Services and Newsletter sections - More transparent in light mode */
:root.light-mode code {
color: var(--accent-secondary);
padding: 0.2em 0.4em;
}
/* Services and Newsletter sections - More defined cards with depth */
:root.light-mode .service-card {
background: rgba(255, 255, 255, 0.5);
backdrop-filter: blur(5px);
border: 1px solid rgba(37, 99, 235, 0.1);
background: rgba(255, 255, 255, 0.9);
backdrop-filter: blur(10px);
border: 1px solid rgba(30, 86, 177, 0.2);
border-radius: 12px;
box-shadow: 0 8px 25px rgba(30, 86, 177, 0.1);
transform: translateY(0);
transition: all 0.3s ease;
}
:root.light-mode .service-card:hover {
transform: translateY(-5px);
box-shadow: 0 12px 30px rgba(30, 86, 177, 0.15);
border-color: rgba(30, 86, 177, 0.3);
}
:root.light-mode .newsletter-container,
:root.light-mode .cta-container {
background: rgba(255, 255, 255, 0.5);
backdrop-filter: blur(5px);
border: 1px solid rgba(37, 99, 235, 0.15);
background: rgba(255, 255, 255, 0.9);
backdrop-filter: blur(15px);
border: 1px solid rgba(30, 86, 177, 0.25);
border-radius: 16px;
box-shadow: 0 15px 40px rgba(30, 86, 177, 0.15);
}
:root.light-mode .newsletter-input {
background: rgba(255, 255, 255, 0.7);
border: 1px solid rgba(37, 99, 235, 0.2);
}
/* Enhanced light mode body background with more pronounced grid pattern */
:root.light-mode body {
background-color: var(--bg-primary);
background-image:
radial-gradient(circle at 20% 35%, rgba(8, 145, 178, 0.08) 0%, transparent 50%),
radial-gradient(circle at 75% 15%, rgba(37, 99, 235, 0.08) 0%, transparent 45%),
radial-gradient(circle at 85% 70%, rgba(124, 58, 237, 0.08) 0%, transparent 40%),
linear-gradient(rgba(37, 99, 235, 0.05) 1px, transparent 1px),
linear-gradient(90deg, rgba(37, 99, 235, 0.05) 1px, transparent 1px);
background-size: auto, auto, auto, 16px 16px, 16px 16px;
background-position: 0 0, 0 0, 0 0, center center, center center;
}
/* Apply base styles using variables */
body {
background-color: var(--bg-primary);
background: rgba(255, 255, 255, 1);
border: 1px solid rgba(30, 86, 177, 0.3);
border-radius: 8px;
padding: 0.75rem 1rem;
box-shadow: 0 2px 10px rgba(30, 86, 177, 0.08);
color: var(--text-primary);
}
a {
color: var(--accent-primary);
:root.light-mode .newsletter-input:focus {
border-color: var(--accent-primary);
box-shadow: 0 2px 15px rgba(2, 119, 182, 0.2);
outline: none;
}
/* Fix for inputs/selects */
input, select, textarea {
background-color: var(--bg-secondary);
color: var(--text-primary);
border-color: var(--border-primary);
}
/* Ensure header and footer adapt to theme */
.site-header, .site-footer {
background-color: var(--bg-secondary);
border-color: var(--border-primary);
}
/* Fix card styles */
.post-card, .sidebar-block {
background-color: var(--card-bg);
border-color: var(--card-border);
}
/* Light mode buttons are more attractive */
/* Buttons with sophisticated gradient effects */
:root.light-mode button {
background: linear-gradient(135deg, rgba(8, 145, 178, 0.05), rgba(37, 99, 235, 0.05));
border: 1px solid rgba(37, 99, 235, 0.1);
background: linear-gradient(135deg, rgba(240, 244, 248, 0.8), rgba(255, 255, 255, 1));
border: 1px solid rgba(30, 86, 177, 0.25);
border-radius: 8px;
transition: all 0.2s ease;
color: var(--text-secondary);
font-weight: 500;
}
:root.light-mode button:hover {
background: linear-gradient(135deg, rgba(8, 145, 178, 0.1), rgba(37, 99, 235, 0.1));
border-color: rgba(37, 99, 235, 0.2);
background: linear-gradient(135deg, rgba(255, 255, 255, 1), rgba(240, 244, 248, 0.8));
border-color: rgba(30, 86, 177, 0.4);
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(30, 86, 177, 0.15);
color: var(--accent-primary);
}
/* Other light mode improvements */
:root.light-mode .primary-button,
:root.light-mode .cta-primary-button {
background: linear-gradient(90deg, var(--accent-primary), var(--accent-secondary));
box-shadow: 0 4px 12px rgba(37, 99, 235, 0.2);
color: white;
border: none;
box-shadow: 0 4px 15px rgba(30, 86, 177, 0.3);
border-radius: 8px;
font-weight: 600;
}
:root.light-mode .primary-button:hover,
:root.light-mode .cta-primary-button:hover {
box-shadow: 0 6px 20px rgba(30, 86, 177, 0.4);
transform: translateY(-2px);
filter: brightness(1.05);
}
:root.light-mode .secondary-button,
:root.light-mode .cta-secondary-button {
background: rgba(255, 255, 255, 0.5);
border: 1px solid rgba(37, 99, 235, 0.2);
background: rgba(255, 255, 255, 0.9);
backdrop-filter: blur(5px);
border: 1px solid rgba(30, 86, 177, 0.3);
color: var(--accent-secondary);
border-radius: 8px;
}
:root.light-mode .secondary-button:hover,
:root.light-mode .cta-secondary-button:hover {
background: rgba(255, 255, 255, 1);
border-color: var(--accent-primary);
color: var(--accent-primary);
transform: translateY(-2px);
box-shadow: 0 4px 15px rgba(2, 119, 182, 0.2);
}
/* Hero section with enhanced gradient background */
:root.light-mode .hero-section {
background: linear-gradient(to bottom, var(--bg-secondary), var(--bg-primary));
background: linear-gradient(160deg, var(--bg-secondary), var(--bg-primary));
border-bottom: 1px solid rgba(30, 86, 177, 0.15);
}
:root.light-mode .hero-bg {
background-image:
radial-gradient(circle at 20% 35%, rgba(8, 145, 178, 0.1) 0%, transparent 50%),
radial-gradient(circle at 75% 15%, rgba(37, 99, 235, 0.1) 0%, transparent 45%),
radial-gradient(circle at 85% 70%, rgba(124, 58, 237, 0.1) 0%, transparent 40%);
radial-gradient(circle at 20% 35%, rgba(2, 119, 182, 0.08) 0%, transparent 50%),
radial-gradient(circle at 75% 15%, rgba(30, 86, 177, 0.08) 0%, transparent 45%),
radial-gradient(circle at 85% 70%, rgba(113, 50, 219, 0.08) 0%, transparent 40%);
}
/* Fix for knowledge graph in both themes */
.graph-container {
backdrop-filter: blur(2px);
/* Enhanced light mode body background with subtle animated grid pattern */
:root.light-mode body {
background-color: var(--bg-primary);
background-image:
radial-gradient(circle at 20% 35%, rgba(2, 119, 182, 0.07) 0%, transparent 50%),
radial-gradient(circle at 75% 15%, rgba(30, 86, 177, 0.07) 0%, transparent 45%),
radial-gradient(circle at 85% 70%, rgba(113, 50, 219, 0.07) 0%, transparent 40%),
linear-gradient(rgba(30, 86, 177, 0.05) 1px, transparent 1px),
linear-gradient(90deg, rgba(30, 86, 177, 0.05) 1px, transparent 1px);
background-size: auto, auto, auto, 20px 20px, 20px 20px;
background-position: 0 0, 0 0, 0 0, center center, center center;
}
/* Blog specific enhancements */
:root.light-mode .blog-post-card {
background: rgba(255, 255, 255, 0.9);
backdrop-filter: blur(10px);
border: 1px solid rgba(30, 86, 177, 0.2);
border-radius: 12px;
box-shadow: 0 10px 30px rgba(30, 86, 177, 0.1);
overflow: hidden;
transform: translateY(0);
transition: all 0.3s ease;
}
:root.light-mode .blog-post-card:hover {
transform: translateY(-5px);
box-shadow: 0 15px 40px rgba(30, 86, 177, 0.15);
border-color: rgba(30, 86, 177, 0.3);
}
:root.light-mode .blog-post-category {
background: linear-gradient(135deg, rgba(2, 119, 182, 0.15), rgba(30, 86, 177, 0.15));
color: var(--accent-primary);
border-radius: 4px;
padding: 0.25rem 0.75rem;
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
}
:root.light-mode .blog-post-title {
color: var(--text-primary);
font-weight: 700;
line-height: 1.3;
margin: 0.75rem 0;
transition: color 0.2s ease;
}
:root.light-mode .blog-post-card:hover .blog-post-title {
color: var(--accent-primary);
}
/* Blog post content styling */
:root.light-mode .blog-content {
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(15px);
border: 1px solid rgba(30, 86, 177, 0.2);
border-radius: 16px;
box-shadow: 0 20px 60px rgba(30, 86, 177, 0.12);
padding: 2.5rem;
}
/* Navigation and header for light mode */
:root.light-mode .site-header {
background: rgba(240, 244, 248, 0.9);
backdrop-filter: blur(15px);
border-bottom: 1px solid rgba(30, 86, 177, 0.15);
box-shadow: 0 4px 20px rgba(30, 86, 177, 0.08);
}
:root.light-mode .nav-link {
color: var(--text-secondary);
font-weight: 500;
transition: all 0.2s ease;
}
:root.light-mode .nav-link:hover {
color: var(--accent-primary);
transform: translateY(-1px);
}
:root.light-mode .nav-link.active {
color: var(--accent-primary);
font-weight: 600;
}
:root.light-mode .theme-toggle {
background: rgba(230, 237, 245, 0.9);
border: 1px solid rgba(30, 86, 177, 0.2);
border-radius: 50%;
width: 2.5rem;
height: 2.5rem;
display: flex;
align-items: center;
justify-content: center;
color: var(--text-secondary);
transition: all 0.2s ease;
}
:root.light-mode .theme-toggle:hover {
background: rgba(230, 237, 245, 1);
color: var(--accent-primary);
transform: rotate(15deg);
box-shadow: 0 2px 10px rgba(30, 86, 177, 0.15);
}
/* Footer styling */
:root.light-mode .site-footer {
background: linear-gradient(160deg, var(--bg-secondary), var(--bg-primary));
border-top: 1px solid rgba(30, 86, 177, 0.15);
padding: 3rem 0;
}