Files
Nathanwoodburn.github.io/templates/tools.html
Nathan Woodburn c2803e372a
All checks were successful
Check Code Quality / RuffCheck (push) Successful in 58s
Build Docker / BuildImage (push) Successful in 1m0s
feat: Dynamically load tool demos and start tracking BSdesign
2025-10-30 21:29:27 +11:00

215 lines
11 KiB
HTML

<!DOCTYPE html>
<html data-bs-theme="light" lang="en-au">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no">
<title>Tools | Nathan.Woodburn/</title>
<meta name="theme-color" content="#000000">
<link rel="canonical" href="https://nathan.woodburn.au/tools">
<meta property="og:url" content="https://nathan.woodburn.au/tools">
<meta name="fediverse:creator" content="@nathanwoodburn@mastodon.woodburn.au">
<meta name="twitter:description" content="G'day, this is my personal website. You can find out about me or check out some of my projects.">
<meta property="og:title" content="Nathan.Woodburn/">
<meta name="twitter:card" content="summary">
<meta name="twitter:image" content="https://nathan.woodburn.au/assets/img/profile.jpg">
<meta property="og:type" content="website">
<meta name="twitter:title" content="Nathan.Woodburn/">
<meta property="og:description" content="G'day, this is my personal website. You can find out about me or check out some of my projects.">
<meta property="og:image" content="https://nathan.woodburn.au/assets/img/profile.jpg">
<meta name="description" content="Check out some tools I use">
<link rel="apple-touch-icon" type="image/png" sizes="180x180" href="/assets/img/favicon/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="16x16" href="/assets/img/favicon/favicon-16x16.png">
<link rel="icon" type="image/png" sizes="32x32" href="/assets/img/favicon/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="180x180" href="/assets/img/favicon/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="192x192" href="/assets/img/favicon/android-chrome-192x192.png">
<link rel="icon" type="image/png" sizes="512x512" href="/assets/img/favicon/android-chrome-512x512.png">
<link rel="stylesheet" href="/assets/bootstrap/css/bootstrap.min.css">
<link rel="manifest" href="/manifest.json" crossorigin="use-credentials">
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Lora:400,700,400italic,700italic&amp;display=swap">
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Cabin:700&amp;display=swap">
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Anonymous+Pro&amp;display=swap">
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&amp;display=swap">
<link rel="stylesheet" href="/assets/fonts/font-awesome.min.css">
<link rel="stylesheet" href="/assets/css/styles.min.css">
<link rel="stylesheet" href="/assets/css/brand-reveal.min.css">
<link rel="stylesheet" href="/assets/css/profile.min.css">
<link rel="stylesheet" href="/assets/css/Social-Icons.min.css">
<link rel="stylesheet" href="/assets/css/tools.min.css">
<link rel="me" href="https://mastodon.woodburn.au/@nathanwoodburn" />
<script async src="https://umami.woodburn.au/script.js" data-website-id="6a55028e-aad3-481c-9a37-3e096ff75589"></script>
</head>
<body id="page-top" data-bs-spy="scroll" data-bs-target="#mainNav" data-bs-offset="77">
<nav class="navbar navbar-expand-md fixed-top navbar-light" id="mainNav" style="background: var(--bs-navbar-hover-color);">
<div class="container-fluid"><a class="navbar-brand" href="/#">
<div style="padding-right: 1em;display: inline-flex;">
<div class="slider"><span>/</span></div><span class="brand">Nathan.Woodburn</span>
</div>
</a><button data-bs-toggle="collapse" class="navbar-toggler navbar-toggler-right" data-bs-target="#navbarResponsive" type="button" aria-controls="navbarResponsive" aria-expanded="false" aria-label="Toggle navigation" value="Menu"><i class="fa fa-bars"></i></button>
<div class="collapse navbar-collapse" id="navbarResponsive">
<ul class="navbar-nav ms-auto">
<li class="nav-item nav-link"><a class="nav-link" href="/">Home</a></li>
<li class="nav-item nav-link"><a class="nav-link" href="/hosting">Hosting</a></li>
<li class="nav-item nav-link"><a class="nav-link" href="/projects">Projects</a></li>
<li class="nav-item nav-link"><a class="nav-link" href="/tools">Tools</a></li>
<li class="nav-item nav-link"><a class="nav-link" href="/blog">Blog</a></li>
<li class="nav-item nav-link"><a class="nav-link" href="/now">Now</a></li>
</ul>
</div>
</div>
</nav>
<header class="masthead" style="background: url(&quot;/assets/img/bg/projects.webp&quot;) bottom / cover no-repeat;height: auto;padding-top: 20px;">
<div style="margin-top: 150px;margin-bottom: 100px;">
<div class="container">
<div class="row">
<div class="col-lg-8 mx-auto">
<h1 class="brand-heading">Tools</h1>
<p>Here is a list of applications, tools and services I use regularly.</p>
</div>
</div>
</div>
</div>
</header>
<section class="text-center content-section" id="tools" style="padding-bottom: 100px;">
<div class="container">{% for type, tools_in_type in tools | groupby('type') %}
<h2 class="mt-4 mb-3 sticky-top bg-primary py-2 section-header" id="{{type}}">{{ type }}</h2>
<div class="row">
{% for tool in tools_in_type %}
<div class="col-md-6 col-lg-4 mb-4">
<div class="card h-100 shadow-sm transition-all" style="transition: transform 0.2s, box-shadow 0.2s;">
<div class="card-body d-flex flex-column">
<h4 class="card-title">{{tool.name}}</h4>
<p class="card-text">{{ tool.description }}</p>
<div class="btn-group gap-3 mt-auto" role="group">{% if tool.demo %}<button class="btn btn-primary"
type="button" data-bs-target="#modal-{{tool.name}}" data-bs-toggle="modal"
style="transition: transform 0.2s, background-color 0.2s;">View Demo</button>{% endif %}<a
class="btn btn-primary" role="button" href="{{tool.url}}" target="_blank"
style="transition: transform 0.2s, background-color 0.2s;">{{tool.name}} Website</a></div>
</div>
</div>
</div>
{% endfor %}
</div>
<!-- Modals for this type -->
{% for tool in tools_in_type %}
{% if tool.demo %}
<div id="modal-{{tool.name}}" class="modal fade" role="dialog" tabindex="-1" style="z-index: 1055;"
data-demo-url="{{ tool.demo_url | e }}">
<div class="modal-dialog modal-xl" role="document">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title">{{tool.name}}</h4><button class="btn-close" type="button" aria-label="Close"
data-bs-dismiss="modal"></button>
</div>
<div class="modal-body" data-demo-loaded="false"></div>
</div>
<div class="modal-footer"><button class="btn btn-light" type="button" data-bs-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
{% endif %}
{% endfor %}
{% endfor %}
<script>
document.addEventListener('DOMContentLoaded', function () {
const navbar = document.getElementById('mainNav');
const headers = document.querySelectorAll('.section-header');
if (navbar) {
const navbarHeight = navbar.offsetHeight;
headers.forEach(header => {
header.style.top = navbarHeight + 'px';
header.style.zIndex = '100';
header.style.scrollMarginTop = navbarHeight + 'px';
});
// Handle hash navigation on page load
if (window.location.hash) {
setTimeout(() => {
const target = document.querySelector(window.location.hash);
if (target) {
window.scrollTo({
top: target.offsetTop - navbarHeight,
behavior: 'smooth'
});
}
}, 0);
}
}
// Load demo in modal
document.querySelectorAll('.modal').forEach(modal => {
modal.addEventListener('show.bs.modal', () => {
const body = modal.querySelector('.modal-body');
if (body.dataset.demoLoaded === 'false') {
const demoUrl = modal.dataset.demoUrl;
const iframeId = 'iframe-' + modal.id;
// Add a div on top of all content to show loading message
const loadingDiv = document.createElement('div');
loadingDiv.style.position = 'absolute';
loadingDiv.style.top = '0';
loadingDiv.style.left = '0';
loadingDiv.style.width = '100%';
loadingDiv.style.height = '100%';
loadingDiv.style.backgroundColor = 'rgb(0, 0, 0)';
loadingDiv.style.display = 'flex';
loadingDiv.style.justifyContent = 'center';
loadingDiv.style.alignItems = 'center';
loadingDiv.style.zIndex = '10';
const loadingMsg = document.createElement('p');
loadingMsg.className = 'text-center';
loadingMsg.textContent = 'Loading demo...';
loadingDiv.appendChild(loadingMsg);
body.style.position = 'relative';
body.appendChild(loadingDiv);
// Create iframe
const iframe = document.createElement('iframe');
iframe.src = demoUrl + '/iframe';
iframe.id = iframeId;
iframe.style.width = '100%';
iframe.style.height = '400px'; // temporary height
iframe.style.border = '0';
iframe.setAttribute('scrolling', 'no');
iframe.setAttribute('allowfullscreen', 'true');
body.appendChild(iframe);
body.dataset.demoLoaded = 'true';
// Listen for bodySize message from asciinema iframe
const origin = new URL(demoUrl).origin;
function onMessage(event) {
if (event.origin !== origin || event.source !== iframe.contentWindow) return;
if (event.data.type === 'bodySize' && event.data.payload.height) {
iframe.style.height = event.data.payload.height + 'px';
// Remove loading message
body.removeChild(loadingDiv);
// Optional: limit modal max height
modal.querySelector('.modal-dialog').style.maxHeight = '90vh';
}
}
window.addEventListener('message', onMessage, false);
}
});
});
});
</script></div>
</section>
<footer>
<div class="container text-center">
<p class="copyright">Copyright ©&nbsp;Nathan.Woodburn/ 2025</p>
</div>
</footer>{{handshake_scripts | safe}}
<script src="/assets/bootstrap/js/bootstrap.min.js"></script>
<script src="/assets/js/script.min.js"></script>
<script src="/assets/js/grayscale.min.js"></script>
<script src="/assets/js/hacker.min.js"></script>
</body>
</html>