2 Commits

Author SHA1 Message Date
Zoe
71fcbbd6f4 make workflow more generic
Some checks failed
Build and Push Docker Image to GHCR / build-and-push (push) Failing after 8s
2025-09-24 16:07:17 +00:00
Zoe
b75337f450 cleaned up templates a bit, and some style fixes 2025-09-24 15:39:16 +00:00
5 changed files with 156 additions and 169 deletions

View File

@@ -1,5 +1,8 @@
name: Build and Push Docker Image to GHCR name: Build and Push Docker Image to GHCR
env:
OCI_REGISTRY: ghcr.io
on: on:
push: push:
branches: ["main"] branches: ["main"]
@@ -20,7 +23,7 @@ jobs:
- name: Log in to GHCR - name: Log in to GHCR
uses: docker/login-action@v3 uses: docker/login-action@v3
with: with:
registry: ghcr.io registry: ${{ OCI_REGISTRY }}
username: ${{ github.actor }} username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }} password: ${{ secrets.GITHUB_TOKEN }}
@@ -28,7 +31,7 @@ jobs:
id: meta id: meta
uses: docker/metadata-action@v5 uses: docker/metadata-action@v5
with: with:
images: ghcr.io/${{ github.repository }} images: ${{ OCI_REGISTRY }}/${{ github.repository }}
- name: Build and push Docker image - name: Build and push Docker image
uses: docker/build-push-action@v6 uses: docker/build-push-action@v6

View File

@@ -35,3 +35,15 @@ h2 {
h3 { h3 {
font-size: 1.25rem; font-size: 1.25rem;
} }
input:not(.search) {
@apply px-4 py-2 rounded-md w-full bg-[#1C1C21] border border-[#2E2E32] placeholder:text-[#8A8E90] text-white focus-visible:outline-none transition-colors duration-300 ease-out overflow-hidden;
&[type="file"] {
@apply p-0 cursor-pointer;
&::file-selector-button {
@apply px-2 py-2 mr-1 bg-[hsl(240,6%,18%)] text-white cursor-pointer;
}
}
}

View File

@@ -1,5 +1,7 @@
<header class="flex w-full p-3"> <div id="blur-target" class="transition-[filter] ease-[cubic-bezier(0.45,0,0.55,1)] duration-300">
<a href="/" class="flex items-center flex-row gap-2 text-white border-b hover:border-transparent justify-center"> <header class="flex w-full p-3">
<a href="/"
class="flex items-center flex-row gap-2 text-white border-b hover:border-transparent justify-center">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20"
viewBox="0 0 24 24"><!-- Icon from Tabler Icons by Paweł Kuna - https://github.com/tabler/tabler-icons/blob/master/LICENSE --> viewBox="0 0 24 24"><!-- Icon from Tabler Icons by Paweł Kuna - https://github.com/tabler/tabler-icons/blob/master/LICENSE -->
<g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"> <g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2">
@@ -9,19 +11,20 @@
</svg> </svg>
Return to home Return to home
</a> </a>
</header> </header>
<section class="flex justify-center w-full"> <section class="flex justify-center w-full">
<div class="w-full sm:w-4/5 p-2.5"> <div class="w-full sm:w-4/5 p-2.5">
{{#each Categories}} {{#each Categories}}
<div class="flex items-center" key="category-{{this.ID}}"> <div class="flex items-center" key="category-{{this.ID}}">
<img class="object-contain mr-2 select-none" width="32" height="32" draggable="false" alt="{{this.Name}}" <img class="object-contain mr-2 select-none" width="32" height="32" draggable="false"
src="{{this.Icon}}" /> alt="{{this.Name}}" src="{{this.Icon}}" />
<h2 class="capitalize break-all">{{this.Name}}</h2> <h2 class="capitalize break-all">{{this.Name}}</h2>
<button onclick="deleteCategory({{this.ID}})" <button onclick="deleteCategory({{this.ID}})"
class="w-fit h-fit flex p-0.5 bg-[#1C1C21] border-solid border-[#211F23] rounded-md hover:bg-[#29292e] cursor-pointer"><svg class="w-fit h-fit flex p-0.5 bg-[#1C1C21] border-solid border-[#211F23] rounded-md hover:bg-[#29292e] cursor-pointer"><svg
xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24"> xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24">
<path fill="none" stroke="#ff1919" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" <path fill="none" stroke="#ff1919" stroke-linecap="round" stroke-linejoin="round"
stroke-width="2"
d="M4 7h16m-10 4v6m4-6v6M5 7l1 12a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2l1-12M9 7V4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v3" /> d="M4 7h16m-10 4v6m4-6v6M5 7l1 12a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2l1-12M9 7V4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v3" />
</svg></button> </svg></button>
</div> </div>
@@ -44,7 +47,7 @@
</svg></button> </svg></button>
</div> </div>
{{/each}} {{/each}}
<div onclick="openLinkModal({{this.ID}})" <div onclick="openModal('link', {{this.ID}})"
class="rounded-2xl border border-dashed border-[#656565] p-2.5 flex flex-row items-center shadow-md hover:shadow-xl transition-[shadow,transform] ease-[cubic-bezier(0.16,1,0.3,1)] pointer-cursor select-none cursor-pointer"> class="rounded-2xl border border-dashed border-[#656565] p-2.5 flex flex-row items-center shadow-md hover:shadow-xl transition-[shadow,transform] ease-[cubic-bezier(0.16,1,0.3,1)] pointer-cursor select-none cursor-pointer">
<svg class="mr-2" xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 24 24"> <svg class="mr-2" xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 24 24">
<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" <path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
@@ -58,123 +61,110 @@
{{/each}} {{/each}}
<div class="flex items-center"> <div class="flex items-center">
<svg class="mr-2" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"> <svg class="mr-2" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24">
<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" <path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
d="M12 5v14m-7-7h14" /> stroke-width="2" d="M12 5v14m-7-7h14" />
</svg> </svg>
<h2 onclick="openCategoryModal()" class="text-[#656565] underline decoration-dashed cursor-pointer"> <h2 onclick="openModal('category')" class="text-[#656565] underline decoration-dashed cursor-pointer">
Add a new category Add a new category
</h2> </h2>
</div> </div>
</div> </div>
</section> </section>
<div id="linkModal"
class="flex modal-bg fixed top-0 left-0 bottom-0 right-0 bg-[#00000070] justify-center items-center">
<div class="bg-[#151316] rounded-xl overflow-hidden w-fit p-4 modal">
<h3>Add A link</h3>
<form id="link-form" action="/api/links" method="post"
class="flex flex-col gap-y-3 my-2 [&>div]:flex [&>div]:flex-col [&>div]:gap-1">
<div>
<label for="linkName">Name</label>
<input required
class="px-4 py-2 rounded-md w-full bg-[#1C1C21] border border-[#56565b]/30 text-white focus-visible:outline-none transition-colors duration-300 ease-out"
type="text" name="name" placeholder="Name" id="linkName" />
</div>
<div>
<label for="linkDesc">Description (optional)</label>
<input
class="px-4 py-2 rounded-md w-full bg-[#1C1C21] border border-[#56565b]/30 text-white focus-visible:outline-none transition-colors duration-300 ease-out"
type="text" name="description" placeholder="Description" id="linkDesc" />
</div>
<div>
<label for="linkURL">URL</label>
<input required
class="px-4 py-2 rounded-md w-full bg-[#1C1C21] border border-[#56565b]/30 text-white focus-visible:outline-none transition-colors duration-300 ease-out"
type="url" name="url" placeholder="URL" id="linkURL" />
</div>
<div>
<label for="linkIcon">Icon</label>
<input required
class="w-full text-white py-2 px-4 rounded bg-[#1C1C21] border border-[#56565b]/30 transition-colors duration-300 ease-out"
type="file" name="icon" id="linkIcon" accept="image/*" />
</div>
<button class="px-4 py-2 rounded-md w-full bg-[#8A42FF] text-white border-0" type="submit">Add link</button>
</form>
<span id="link-message"></span>
</div>
</div> </div>
<div id="categoryModal"
<div id="modal-container"
class="flex modal-bg fixed top-0 left-0 bottom-0 right-0 bg-[#00000070] justify-center items-center"> class="flex modal-bg fixed top-0 left-0 bottom-0 right-0 bg-[#00000070] justify-center items-center">
<div class="bg-[#151316] rounded-xl overflow-hidden w-fit p-4 modal"> <div class="bg-[#151316] rounded-xl overflow-hidden w-fit p-4 modal">
<div id="category-contents" class="hidden">
<h3>Create A category</h3> <h3>Create A category</h3>
<form id="category-form" action="/api/categories" method="post" <form id="category-form" action="/api/categories" method="post"
class="flex flex-col gap-y-3 my-2 [&>div]:flex [&>div]:flex-col [&>div]:gap-1"> class="flex flex-col gap-y-3 my-2 [&>div]:flex [&>div]:flex-col [&>div]:gap-1">
<div> <div>
<label for="categoryName">Name</label> <label for="categoryName">Name</label>
<input <input required type="text" name="name" id="categoryName" />
class="px-4 py-2 rounded-md w-full bg-[#1C1C21] border border-[#56565b]/30 text-white focus-visible:outline-none"
type="text" name="name" placeholder="Name" id="categoryName" />
</div> </div>
<div> <div>
<label for="linkIcon">Icon</label> <label for="linkIcon">Icon</label>
<input class="w-full text-white py-2 px-4 rounded bg-[#1C1C21] border border-[#56565b]/30" type="file" <input type="file" name="icon" id="linkIcon" accept=".svg" required />
name="icon" id="linkIcon" accept=".svg" />
</div> </div>
<button class="px-4 py-2 rounded-md w-full bg-[#8A42FF] text-white border-0" type="submit">Create <button class="px-4 py-2 rounded-md w-full bg-[#8A42FF] text-white border-0" type="submit">Create
category</button> category</button>
</form> </form>
<span id="category-message"></span> <span id="category-message"></span>
</div> </div>
<div id="link-contents" class="hidden">
<h3>Add A link</h3>
<form id="link-form" action="/api/links" method="post"
class="flex flex-col gap-y-3 my-2 [&>div]:flex [&>div]:flex-col [&>div]:gap-1">
<div>
<label for="linkName">Name</label>
<input required type="text" name="name" id="linkName" />
</div>
<div>
<label for="linkDesc">Description (optional)</label>
<input type="text" name="description" id="linkDesc" />
</div>
<div>
<label for="linkURL">URL</label>
<input required type="url" name="url" id="linkURL" />
</div>
<div>
<label for="linkIcon">Icon</label>
<input required type="file" name="icon" id="linkIcon" accept="image/*" />
</div>
<button class="px-4 py-2 rounded-md w-full bg-[#8A42FF] text-white border-0" type="submit">Add
link</button>
</form>
<span id="link-message"></span>
</div>
</div>
</div> </div>
<script> <script>
// idfk what this variable capitalization is, it's a mess // idfk what this variable capitalization is, it's a mess
let linkModalBg = document.getElementById("linkModal"); let modalContainer = document.getElementById("modal-container");
let linkModal = linkModalBg.querySelector("div"); let modal = modalContainer.querySelector("div");
let categoryModalBg = document.getElementById("categoryModal"); let pageElement = document.getElementById("blur-target");
let categoryModal = categoryModalBg.querySelector("div");
let pageElement = document.querySelector("section");
let targetCategoryID = null; let targetCategoryID = null;
let activeModal = null;
function openCategoryModal() { function openModal(modalKind, categoryID) {
pageElement.style.filter = "blur(20px)"; activeModal = modalKind;
document.getElementById("category-form").reset();
categoryModalBg.classList.add("is-visible");
categoryModal.classList.add("is-visible");
}
function closeCategoryModal() {
pageElement.style.filter = "";
categoryModalBg.classList.remove("is-visible");
categoryModal.classList.remove("is-visible");
document.getElementById("category-form").querySelectorAll("[required]").forEach((el) => {
el.classList.remove("invalid:border-[#861024]");
});
}
function openLinkModal(categoryID) {
targetCategoryID = categoryID; targetCategoryID = categoryID;
pageElement.style.filter = "blur(20px)"; pageElement.style.filter = "blur(20px)";
document.getElementById("link-form").reset(); document.getElementById(modalKind + "-contents").classList.remove("hidden");
linkModalBg.classList.add("is-visible"); modalContainer.classList.add("is-visible");
linkModal.classList.add("is-visible"); modal.classList.add("is-visible");
document.getElementById(modalKind + "-form").reset();
} }
function closeLinkModal() { function closeModal() {
pageElement.style.filter = ""; pageElement.style.filter = "";
linkModalBg.classList.remove("is-visible"); modalContainer.classList.remove("is-visible");
linkModal.classList.remove("is-visible"); modal.classList.remove("is-visible");
document.getElementById("link-form").querySelectorAll("[required]").forEach((el) => { setTimeout(() => {
el.classList.remove("invalid:border-[#861024]"); document.getElementById(activeModal + "-contents").classList.add("hidden");
activeModal = null;
}, 300)
document.getElementById(activeModal + "-form").querySelectorAll("[required]").forEach((el) => {
el.classList.remove("invalid:border-[#861024]!");
}); });
targetCategoryID = null;
} }
modalContainer.addEventListener("click", (event) => {
if (event.target === modalContainer) {
closeModal();
}
});
async function deleteLink(linkID, categoryID) { async function deleteLink(linkID, categoryID) {
let res = await fetch(`/api/category/${categoryID}/link/${linkID}`, { let res = await fetch(`/api/category/${categoryID}/link/${linkID}`, {
method: "DELETE" method: "DELETE"
@@ -202,7 +192,7 @@
document.getElementById("link-form").querySelector("button").addEventListener("click", (event) => { document.getElementById("link-form").querySelector("button").addEventListener("click", (event) => {
document.getElementById("link-form").querySelectorAll("[required]").forEach((el) => { document.getElementById("link-form").querySelectorAll("[required]").forEach((el) => {
el.classList.add("invalid:border-[#861024]"); el.classList.add("invalid:border-[#861024]!");
}); });
}); });
@@ -216,7 +206,7 @@
}); });
if (res.status === 201) { if (res.status === 201) {
closeLinkModal(); closeModal('link');
document.getElementById("link-form").reset(); document.getElementById("link-form").reset();
location.reload(); location.reload();
} else { } else {
@@ -227,7 +217,7 @@
document.getElementById("category-form").querySelector("button").addEventListener("click", (event) => { document.getElementById("category-form").querySelector("button").addEventListener("click", (event) => {
document.getElementById("category-form").querySelectorAll("[required]").forEach((el) => { document.getElementById("category-form").querySelectorAll("[required]").forEach((el) => {
el.classList.add("invalid:border-[#861024]"); el.classList.add("invalid:border-[#861024]!");
}); });
}); });
@@ -241,7 +231,7 @@
}); });
if (res.status === 201) { if (res.status === 201) {
closeCategoryModal() closeModal('category');
document.getElementById("category-form").reset(); document.getElementById("category-form").reset();
location.reload(); location.reload();
} else { } else {
@@ -249,20 +239,6 @@
document.getElementById("link-message").innerText = json.message; document.getElementById("link-message").innerText = json.message;
} }
}); });
linkModalBg.addEventListener("click", (event) => {
if (event.target === linkModalBg) {
targetCategoryID = null;
closeLinkModal();
}
});
categoryModalBg.addEventListener("click", (event) => {
if (event.target === categoryModalBg) {
targetCategoryID = null;
closeCategoryModal();
}
});
</script> </script>
<style> <style>

View File

@@ -6,12 +6,8 @@
Login Login
</h2> </h2>
<form action="/admin/login" method="post" class="flex flex-col gap-y-3 my-2"> <form action="/admin/login" method="post" class="flex flex-col gap-y-3 my-2">
<input <input type="text" name="username" placeholder="Username" />
class="px-4 py-2 rounded-md w-full bg-[#1C1C21] border border-[#56565b]/30 text-white focus-visible:outline-none" <input type="password" name="password" placeholder="Password" />
type="text" name="username" placeholder="Username" />
<input
class="px-4 py-2 rounded-md w-full bg-[#1C1C21] border border-[#56565b]/30 text-white focus-visible:outline-none"
type="password" name="password" placeholder="Password" />
<button class="px-4 py-2 rounded-md w-full bg-[#8A42FF] text-white border-0" <button class="px-4 py-2 rounded-md w-full bg-[#8A42FF] text-white border-0"
type="submit">Login</button> type="submit">Login</button>
</form> </form>

View File

@@ -62,7 +62,7 @@
</div> </div>
<form class="w-full max-w-3xl" action="{{ SearchProviderURL }}" method="GET"> <form class="w-full max-w-3xl" action="{{ SearchProviderURL }}" method="GET">
<input name="{{ SearchParam }}" aria-label="Search bar" <input name="{{ SearchParam }}" aria-label="Search bar"
class="w-full bg-[#1C1C21] border border-[#56565b]/30 rounded-full px-3 py-1 text-white h-7 focus-visible:outline-none placeholder:italic placeholder:text-[#434343]" class="w-full bg-[#1C1C21] border border-[#2E2E32] rounded-full px-3 py-1 text-white h-7 focus-visible:outline-none placeholder:italic placeholder:text-[#434343] search"
placeholder="Search..." /> placeholder="Search..." />
</form> </form>
</div> </div>