feat: Added tools page
All checks were successful
Build Docker / BuildImage (push) Successful in 2m9s
All checks were successful
Build Docker / BuildImage (push) Successful in 2m9s
This commit is contained in:
@@ -10,6 +10,10 @@ blog_bp = Blueprint('blog', __name__)
|
|||||||
|
|
||||||
def list_page_files():
|
def list_page_files():
|
||||||
blog_pages = os.listdir("data/blog")
|
blog_pages = os.listdir("data/blog")
|
||||||
|
# Sort pages by modified time, newest first
|
||||||
|
blog_pages.sort(
|
||||||
|
key=lambda x: os.path.getmtime(os.path.join("data/blog", x)), reverse=True)
|
||||||
|
|
||||||
# Remove .md extension
|
# Remove .md extension
|
||||||
blog_pages = [page.removesuffix(".md")
|
blog_pages = [page.removesuffix(".md")
|
||||||
for page in blog_pages if page.endswith(".md")]
|
for page in blog_pages if page.endswith(".md")]
|
||||||
|
|||||||
9
blueprints/template.py
Normal file
9
blueprints/template.py
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
from flask import Blueprint, request
|
||||||
|
from tools import json_response
|
||||||
|
|
||||||
|
template_bp = Blueprint('template', __name__)
|
||||||
|
|
||||||
|
|
||||||
|
@template_bp.route("/")
|
||||||
|
def index():
|
||||||
|
return json_response(request, "Success", 200)
|
||||||
@@ -1,7 +1,9 @@
|
|||||||
G'day,
|
G'day,
|
||||||
Just thought it might be useful to write down some of the software I use regularly. I've no clue if you'll find any useful :)
|
Just thought it might be useful to write down some of the software I use regularly. I've no clue if you'll find any useful :)
|
||||||
|
|
||||||
|
For a more complete list, check out [/tools](/tools)
|
||||||
|
|
||||||
|
<br>
|
||||||
## Overview
|
## Overview
|
||||||
OS: Arch Linux | Because it is quick to update and has all the latest tools I can play with
|
OS: Arch Linux | Because it is quick to update and has all the latest tools I can play with
|
||||||
DE: Hyprland | Feel free to check out my dotfiles if you're interested
|
DE: Hyprland | Feel free to check out my dotfiles if you're interested
|
||||||
|
|||||||
164
data/tools.json
Normal file
164
data/tools.json
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"name":"Obsidian",
|
||||||
|
"type":"Desktop Applications",
|
||||||
|
"url":"https://obsidian.md/",
|
||||||
|
"description":"Note taking app that stores everything in Markdown files"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Alacritty",
|
||||||
|
"type": "Desktop Applications",
|
||||||
|
"url": "https://alacritty.org/",
|
||||||
|
"description": "A cross-platform, GPU-accelerated terminal emulator"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Brave",
|
||||||
|
"type": "Desktop Applications",
|
||||||
|
"url": "https://brave.com/",
|
||||||
|
"description": "Privacy-focused web browser"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "VSCode",
|
||||||
|
"type": "Desktop Applications",
|
||||||
|
"url": "https://code.visualstudio.com/",
|
||||||
|
"description": "Source-code editor developed by Microsoft"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Zellij",
|
||||||
|
"type": "terminal",
|
||||||
|
"url": "https://zellij.dev/",
|
||||||
|
"description": "A terminal workspace and multiplexer"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Fx",
|
||||||
|
"type": "terminal",
|
||||||
|
"url": "https://fx.wtf/",
|
||||||
|
"description": "A command-line JSON viewer and processor",
|
||||||
|
"demo": "<script src=\"https://asciinema.c.woodburn.au/a/cx8sr8adsP6Uoi9zpg8MYnVTl.js\" id=\"asciicast-cx8sr8adsP6Uoi9zpg8MYnVTl\" async=\"true\"></script>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Zoxide",
|
||||||
|
"type": "terminal",
|
||||||
|
"url": "https://github.com/ajeetdsouza/zoxide",
|
||||||
|
"description": "cd but with fuzzy matching and other cool features",
|
||||||
|
"demo": "<script src=\"https://asciinema.c.woodburn.au/a/5.js\" id=\"asciicast-5\" async=\"true\"></script>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Atuin",
|
||||||
|
"type": "terminal",
|
||||||
|
"url": "https://atuin.sh/",
|
||||||
|
"description": "A next-generation shell history manager",
|
||||||
|
"demo": "<script src=\"https://asciinema.c.woodburn.au/a/6.js\" id=\"asciicast-6\" async=\"true\"></script>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Tmate",
|
||||||
|
"type": "terminal",
|
||||||
|
"url": "https://tmate.io/",
|
||||||
|
"description": "Instant terminal sharing",
|
||||||
|
"demo": "<script src=\"https://asciinema.c.woodburn.au/a/7.js\" id=\"asciicast-7\" async=\"true\"></script>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Eza",
|
||||||
|
"type": "terminal",
|
||||||
|
"url": "https://eza.rocks/",
|
||||||
|
"description": "A modern replacement for 'ls'",
|
||||||
|
"demo": "<script src=\"https://asciinema.c.woodburn.au/a/8.js\" id=\"asciicast-8\" async=\"true\"></script>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Bat",
|
||||||
|
"type": "terminal",
|
||||||
|
"url": "https://github.com/sharkdp/bat",
|
||||||
|
"description": "A cat clone with syntax highlighting and Git integration",
|
||||||
|
"demo": "<script src=\"https://asciinema.c.woodburn.au/a/9.js\" id=\"asciicast-9\" async=\"true\"></script>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Oh My Zsh",
|
||||||
|
"type": "terminal",
|
||||||
|
"url": "https://ohmyz.sh/",
|
||||||
|
"description": "A delightful community-driven framework for managing your Zsh configuration"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Proxmox",
|
||||||
|
"type": "Server Management",
|
||||||
|
"url": "https://www.proxmox.com/en",
|
||||||
|
"description": "Open-source server virtualization management solution"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Portainer",
|
||||||
|
"type": "Server Management",
|
||||||
|
"url": "https://www.portainer.io/",
|
||||||
|
"description": "Lightweight management UI which allows you to easily manage your Docker containers"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Coolify",
|
||||||
|
"type": "Server Management",
|
||||||
|
"url": "https://coolify.io/",
|
||||||
|
"description": "An open-source self-hosted Heroku alternative"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "OpnSense",
|
||||||
|
"type": "Server Management",
|
||||||
|
"url": "https://opnsense.org/",
|
||||||
|
"description": "Open source, easy-to-use and easy-to-build FreeBSD based firewall and routing platform"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Nginx Proxy Manager",
|
||||||
|
"type": "Server Management",
|
||||||
|
"url": "https://nginxproxymanager.com/",
|
||||||
|
"description": "A powerful yet easy to use web interface for managing Nginx proxy hosts"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Tailscale",
|
||||||
|
"type": "Server Management",
|
||||||
|
"url": "https://tailscale.com/",
|
||||||
|
"description": "A zero-config VPN that just works"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Authentik",
|
||||||
|
"type": "Self-Hosting Services",
|
||||||
|
"url": "https://goauthentik.io/",
|
||||||
|
"description": "An open-source identity provider focused on flexibility and ease of use"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Uptime Kuma",
|
||||||
|
"type": "Self-Hosting Services",
|
||||||
|
"url": "https://uptime.kuma.pet/",
|
||||||
|
"description": "A fancy self-hosted monitoring tool"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Gitea",
|
||||||
|
"type": "Self-Hosting Services",
|
||||||
|
"url": "https://about.gitea.com/",
|
||||||
|
"description": "A painless self-hosted Git service"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Nextcloud",
|
||||||
|
"type": "Self-Hosting Services",
|
||||||
|
"url": "https://nextcloud.com/",
|
||||||
|
"description": "A suite of client-server software for creating and using file hosting services"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Umami",
|
||||||
|
"type": "Self-Hosting Services",
|
||||||
|
"url": "https://umami.is/",
|
||||||
|
"description": "A simple, fast, privacy-focused alternative to Google Analytics"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "PhotoPrism",
|
||||||
|
"type": "Self-Hosting Services",
|
||||||
|
"url": "https://photoprism.app/",
|
||||||
|
"description": "AI-powered app for browsing, organizing & sharing your photo collection"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "FreeScout",
|
||||||
|
"type": "Self-Hosting Services",
|
||||||
|
"url": "https://freescout.net/",
|
||||||
|
"description": "Self hosted email dashboard"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Vaultwarden",
|
||||||
|
"type": "Miscellaneous",
|
||||||
|
"url": "https://github.com/dani-garcia/vaultwarden",
|
||||||
|
"description": "Password manager server implementation compatible with Bitwarden clients"
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -25,7 +25,7 @@ from blueprints.wellknown import wk_bp
|
|||||||
from blueprints.api import api_bp
|
from blueprints.api import api_bp
|
||||||
from blueprints.podcast import podcast_bp
|
from blueprints.podcast import podcast_bp
|
||||||
from blueprints.acme import acme_bp
|
from blueprints.acme import acme_bp
|
||||||
from tools import isCurl, isCrawler, getAddress, getFilePath, error_response, getClientIP, json_response, getGitCommit, isDev, getHandshakeScript
|
from tools import isCurl, isCrawler, getAddress, getFilePath, error_response, getClientIP, json_response, getGitCommit, isDev, getHandshakeScript, get_tools_data
|
||||||
|
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
CORS(app)
|
CORS(app)
|
||||||
@@ -399,8 +399,6 @@ def index():
|
|||||||
return resp
|
return resp
|
||||||
|
|
||||||
# region Donate
|
# region Donate
|
||||||
|
|
||||||
|
|
||||||
@app.route("/donate")
|
@app.route("/donate")
|
||||||
def donate():
|
def donate():
|
||||||
coinList = os.listdir(".well-known/wallets")
|
coinList = os.listdir(".well-known/wallets")
|
||||||
@@ -563,7 +561,6 @@ def qrcodee(data):
|
|||||||
|
|
||||||
# endregion
|
# endregion
|
||||||
|
|
||||||
|
|
||||||
@app.route("/supersecretpath")
|
@app.route("/supersecretpath")
|
||||||
def supersecretpath():
|
def supersecretpath():
|
||||||
ascii_art = ""
|
ascii_art = ""
|
||||||
@@ -689,6 +686,10 @@ def resume_pdf():
|
|||||||
return send_file("data/resume.pdf")
|
return send_file("data/resume.pdf")
|
||||||
return error_response(request, message="Resume not found")
|
return error_response(request, message="Resume not found")
|
||||||
|
|
||||||
|
@app.route("/tools")
|
||||||
|
def tools():
|
||||||
|
return render_template("tools.html", tools=get_tools_data())
|
||||||
|
|
||||||
# endregion
|
# endregion
|
||||||
# region Error Catching
|
# region Error Catching
|
||||||
|
|
||||||
|
|||||||
2
templates/assets/css/styles.min.css
vendored
2
templates/assets/css/styles.min.css
vendored
@@ -1 +1 @@
|
|||||||
:root,[data-bs-theme=light]{--bs-primary:#6E0E9C;--bs-primary-rgb:110,14,156;--bs-primary-text-emphasis:#2C063E;--bs-primary-bg-subtle:#E2CFEB;--bs-primary-border-subtle:#C59FD7;--bs-link-color:#6E0E9C;--bs-link-color-rgb:110,14,156;--bs-link-hover-color:#a41685;--bs-link-hover-color-rgb:164,22,133}.btn-primary{--bs-btn-color:#fff;--bs-btn-bg:#6E0E9C;--bs-btn-border-color:#6E0E9C;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#5E0C85;--bs-btn-hover-border-color:#580B7D;--bs-btn-focus-shadow-rgb:233,219,240;--bs-btn-active-color:#fff;--bs-btn-active-bg:#580B7D;--bs-btn-active-border-color:#530B75;--bs-btn-disabled-color:#fff;--bs-btn-disabled-bg:#6E0E9C;--bs-btn-disabled-border-color:#6E0E9C}.btn-outline-primary{--bs-btn-color:#6E0E9C;--bs-btn-border-color:#6E0E9C;--bs-btn-focus-shadow-rgb:110,14,156;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#6E0E9C;--bs-btn-hover-border-color:#6E0E9C;--bs-btn-active-color:#fff;--bs-btn-active-bg:#6E0E9C;--bs-btn-active-border-color:#6E0E9C;--bs-btn-disabled-color:#6E0E9C;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#6E0E9C}.py-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}@media (min-width:992px){.py-lg-5{padding-top:3rem!important;padding-bottom:3rem!important}}
|
:root,[data-bs-theme=light]{--bs-primary:#6E0E9C;--bs-primary-rgb:110,14,156;--bs-primary-text-emphasis:#2C063E;--bs-primary-bg-subtle:#E2CFEB;--bs-primary-border-subtle:#C59FD7;--bs-link-color:#6E0E9C;--bs-link-color-rgb:110,14,156;--bs-link-hover-color:#a41685;--bs-link-hover-color-rgb:164,22,133}.btn-primary{--bs-btn-color:#fff;--bs-btn-bg:#6E0E9C;--bs-btn-border-color:#6E0E9C;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#5E0C85;--bs-btn-hover-border-color:#580B7D;--bs-btn-focus-shadow-rgb:233,219,240;--bs-btn-active-color:#fff;--bs-btn-active-bg:#580B7D;--bs-btn-active-border-color:#530B75;--bs-btn-disabled-color:#fff;--bs-btn-disabled-bg:#6E0E9C;--bs-btn-disabled-border-color:#6E0E9C}.btn-outline-primary{--bs-btn-color:#6E0E9C;--bs-btn-border-color:#6E0E9C;--bs-btn-focus-shadow-rgb:110,14,156;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#6E0E9C;--bs-btn-hover-border-color:#6E0E9C;--bs-btn-active-color:#fff;--bs-btn-active-bg:#6E0E9C;--bs-btn-active-border-color:#6E0E9C;--bs-btn-disabled-color:#6E0E9C;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#6E0E9C}.py-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}@media (min-width:992px){.py-lg-5{padding-top:3rem!important;padding-bottom:3rem!important}}
|
||||||
@@ -96,4 +96,7 @@
|
|||||||
<url>
|
<url>
|
||||||
<loc>https://nathan.woodburn.au/resume</loc>
|
<loc>https://nathan.woodburn.au/resume</loc>
|
||||||
</url>
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://nathan.woodburn.au/tools</loc>
|
||||||
|
</url>
|
||||||
</urlset>
|
</urlset>
|
||||||
148
templates/tools.html
Normal file
148
templates/tools.html
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
<!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&display=swap">
|
||||||
|
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Cabin:700&display=swap">
|
||||||
|
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Anonymous+Pro&display=swap">
|
||||||
|
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&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="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="/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("/assets/img/bg/projects.webp") 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;" onmouseover="this.style.transform='translateY(-5px)'; this.style.boxShadow='0 0.5rem 1rem rgba(0,0,0,0.15)';" onmouseout="this.style.transform='translateY(0)'; this.style.boxShadow='';">
|
||||||
|
<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;" onmouseover="this.style.transform='scale(1.05)'" onmouseout="this.style.transform='scale(1)'">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;" onmouseover="this.style.transform='scale(1.05)'" onmouseout="this.style.transform='scale(1)'">{{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;">
|
||||||
|
<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">
|
||||||
|
{{ tool.demo | safe }}
|
||||||
|
</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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script></div>
|
||||||
|
</section>
|
||||||
|
<footer>
|
||||||
|
<div class="container text-center">
|
||||||
|
<p class="copyright">Copyright © 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>
|
||||||
5
tools.py
5
tools.py
@@ -5,6 +5,7 @@ import datetime
|
|||||||
from typing import Optional, Dict, Union, Tuple
|
from typing import Optional, Dict, Union, Tuple
|
||||||
import re
|
import re
|
||||||
from dateutil.parser import parse
|
from dateutil.parser import parse
|
||||||
|
import json
|
||||||
|
|
||||||
# HTTP status codes
|
# HTTP status codes
|
||||||
HTTP_OK = 200
|
HTTP_OK = 200
|
||||||
@@ -250,3 +251,7 @@ def parse_date(date_groups: list[str]) -> str | None:
|
|||||||
|
|
||||||
except (ValueError, TypeError):
|
except (ValueError, TypeError):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def get_tools_data():
|
||||||
|
with open("data/tools.json", "r") as f:
|
||||||
|
return json.load(f)
|
||||||
Reference in New Issue
Block a user