V0.3.3: Even more optimization
Some checks failed
Build and Push Docker Image to GHCR / build-and-push (push) Has been cancelled
Some checks failed
Build and Push Docker Image to GHCR / build-and-push (push) Has been cancelled
In this realease, I have further optimized Passport. The css that Passport now uses is entirely handrolled and build via postcss (sadly). Several bugs have also been fixed in this release, as well as a few performance improvements relating to the admin UI.
This commit is contained in:
Binary file not shown.
30
src/main.go
30
src/main.go
@@ -1,4 +1,4 @@
|
||||
//go:generate tailwindcss -i styles/main.scss -o assets/tailwind.css --minify
|
||||
//go:generate bun run build
|
||||
|
||||
package main
|
||||
|
||||
@@ -44,7 +44,7 @@ import (
|
||||
_ "modernc.org/sqlite"
|
||||
)
|
||||
|
||||
//go:embed assets/** templates/** schema.sql scripts/**.js styles/**.css
|
||||
//go:embed assets/** templates/** schema.sql scripts/**.js
|
||||
var embeddedAssets embed.FS
|
||||
|
||||
var devContent = `<script>
|
||||
@@ -285,7 +285,12 @@ func UploadFile(file *multipart.FileHeader, contentType string, c fiber.Ctx) (st
|
||||
return "", err
|
||||
}
|
||||
|
||||
fileName := fmt.Sprintf("%s.%s", fileId.String(), filepath.Ext(file.Filename))
|
||||
var fileName string
|
||||
if filepath.Ext(file.Filename) != ".svg" {
|
||||
fileName = fmt.Sprintf("%s.webp", fileId.String())
|
||||
} else {
|
||||
fileName = fmt.Sprintf("%s.svg", fileId.String())
|
||||
}
|
||||
|
||||
srcFile, err := file.Open()
|
||||
if err != nil {
|
||||
@@ -307,6 +312,10 @@ func UploadFile(file *multipart.FileHeader, contentType string, c fiber.Ctx) (st
|
||||
return "", errors.New("unsupported file type")
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if contentType != "image/svg+xml" {
|
||||
off, err := srcFile.Seek(0, io.SeekStart)
|
||||
if err != nil {
|
||||
@@ -708,6 +717,10 @@ func main() {
|
||||
|
||||
engine := handlebars.NewFileSystem(http.FS(templatesDir), ".hbs")
|
||||
|
||||
engine.AddFunc("eq", func(a, b any) bool {
|
||||
return a == b
|
||||
})
|
||||
|
||||
engine.AddFunc("embedFile", func(fileToEmbed string) string {
|
||||
content, err := fs.ReadFile(embeddedAssets, fileToEmbed)
|
||||
if err != nil {
|
||||
@@ -715,6 +728,7 @@ func main() {
|
||||
}
|
||||
|
||||
fileExtension := filepath.Ext(fileToEmbed)
|
||||
|
||||
switch fileExtension {
|
||||
case ".js":
|
||||
return fmt.Sprintf("<script>%s</script>", content)
|
||||
@@ -761,10 +775,13 @@ func main() {
|
||||
|
||||
router.Use("/assets", static.New("", static.Config{
|
||||
FS: assetsDir,
|
||||
Browse: false,
|
||||
MaxAge: 31536000,
|
||||
}))
|
||||
|
||||
router.Get("/", func(c fiber.Ctx) error {
|
||||
c.Response().Header.Set("Link", "</assets/fonts/InstrumentSans-VariableFont_wdth,wght.woff2>; rel=preload; as=font; type=font/woff2; crossorigin")
|
||||
|
||||
renderData := fiber.Map{
|
||||
"SearchProviderURL": app.Config.SearchProvider.URL,
|
||||
"SearchParam": app.Config.SearchProvider.Query,
|
||||
@@ -785,7 +802,7 @@ func main() {
|
||||
renderData["UptimeData"] = app.UptimeManager.GetUptime()
|
||||
}
|
||||
|
||||
return c.Render("views/index", renderData, "layouts/main")
|
||||
return c.Render("views/index", renderData)
|
||||
})
|
||||
|
||||
router.Use(middleware.AdminMiddleware(app.db))
|
||||
@@ -795,7 +812,7 @@ func main() {
|
||||
return c.Redirect().To("/admin")
|
||||
}
|
||||
|
||||
return c.Render("views/admin/login", fiber.Map{}, "layouts/main")
|
||||
return c.Render("views/admin/login", fiber.Map{})
|
||||
})
|
||||
|
||||
router.Post("/admin/login", func(c fiber.Ctx) error {
|
||||
@@ -844,7 +861,8 @@ func main() {
|
||||
|
||||
return c.Render("views/admin/index", fiber.Map{
|
||||
"Categories": app.CategoryManager.GetCategories(),
|
||||
}, "layouts/admin")
|
||||
"IsAdmin": true,
|
||||
})
|
||||
})
|
||||
|
||||
api := router.Group("/api")
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
"use strict";
|
||||
|
||||
// idfk what this variable capitalization is, it's a mess
|
||||
let modalContainer = document.getElementById("modal-container");
|
||||
let modal = modalContainer.querySelector("div");
|
||||
@@ -54,7 +56,7 @@ function addErrorListener(form) {
|
||||
event.target.parentElement
|
||||
.querySelectorAll("[required]")
|
||||
.forEach((el) => {
|
||||
el.classList.add("invalid:border-[#861024]!");
|
||||
el.classList.add("invalid");
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -83,7 +85,7 @@ function cloneEditActions(primaryActions) {
|
||||
let actionButtonObj = primaryActions[i];
|
||||
|
||||
let actionButton = editActions.querySelector(
|
||||
`div[data-primary-actions] button:nth-child(${i + 1})`
|
||||
`div:first-child button:nth-child(${i + 1})`
|
||||
);
|
||||
actionButton.setAttribute("onclick", actionButtonObj.clickAction);
|
||||
actionButton.setAttribute("aria-label", actionButtonObj.label);
|
||||
@@ -120,7 +122,7 @@ document
|
||||
newLinkCard.classList.add("link-card", "admin", "relative");
|
||||
|
||||
let newLinkImgElement = newLinkCard.querySelector(
|
||||
"div[data-img-container] img"
|
||||
"div:first-child img"
|
||||
);
|
||||
|
||||
newLinkImgElement.src = await processFile(data.get("icon"));
|
||||
@@ -200,7 +202,7 @@ document
|
||||
|
||||
categoryHeader.appendChild(editActions);
|
||||
|
||||
let categoryImg = categoryHeader.querySelector(".category-img");
|
||||
let categoryImg = categoryHeader.querySelector("div:first-child");
|
||||
|
||||
categoryImg.querySelector("img").src = await processFile(
|
||||
data.get("icon")
|
||||
@@ -336,11 +338,11 @@ function closeModal() {
|
||||
.getElementById(activeModal + "-form")
|
||||
.querySelectorAll("[required]")
|
||||
.forEach((el) => {
|
||||
el.classList.remove("invalid:border-[#861024]!");
|
||||
el.classList.remove("invalid");
|
||||
});
|
||||
}
|
||||
|
||||
targetCategoryID = null;
|
||||
currentlyEditing = {};
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -417,22 +419,23 @@ function cancelEdit() {
|
||||
* @param {HTMLElement} target The target element that was clicked
|
||||
*/
|
||||
function editLink(target) {
|
||||
let startTime = performance.now();
|
||||
|
||||
// we do it in this dynamic way so that if we add a new link without refreshing the page, it still works
|
||||
let linkEl = target.closest(".link-card");
|
||||
let linkEl = target.closest("[data-card]");
|
||||
let linkID = parseInt(linkEl.id);
|
||||
let categoryID = parseInt(linkEl.parentElement.previousElementSibling.id);
|
||||
|
||||
if (currentlyEditing.linkID !== undefined) {
|
||||
if (
|
||||
currentlyEditing.linkID !== undefined ||
|
||||
currentlyEditing.categoryID !== undefined
|
||||
) {
|
||||
// cancel the edit if it's already in progress
|
||||
cancelEdit();
|
||||
}
|
||||
|
||||
let linkImg = linkEl.querySelector("div[data-img-container] img");
|
||||
let linkName = linkEl.querySelector("div[data-text-container] h3");
|
||||
let linkDesc = linkEl.querySelector("div[data-text-container] p");
|
||||
let editActions = linkEl.querySelector("[data-edit-actions]");
|
||||
let linkImg = linkEl.querySelector("div:first-child img");
|
||||
let linkName = linkEl.querySelector("div:nth-child(2) h3");
|
||||
let linkDesc = linkEl.querySelector("div:nth-child(2) p");
|
||||
let editActions = linkEl.querySelector("div:nth-child(3)");
|
||||
|
||||
currentlyEditing = {
|
||||
type: "link",
|
||||
@@ -453,8 +456,7 @@ function editLink(target) {
|
||||
teleportElement(selectIconButton, linkImg.parentElement);
|
||||
teleportElement(confirmActions, editActions);
|
||||
|
||||
editActions.querySelector("div[data-primary-actions]").style.display =
|
||||
"none";
|
||||
editActions.querySelector("div:first-child").style.display = "none";
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
currentlyEditing.cleanup = replaceWithResizableTextarea([
|
||||
@@ -498,6 +500,7 @@ async function confirmLinkEdit() {
|
||||
formData.get("description") === null &&
|
||||
formData.get("icon") === null
|
||||
) {
|
||||
cancelEdit();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -527,14 +530,14 @@ function cancelLinkEdit(
|
||||
let linkEl = document.getElementById(`${currentlyEditing.linkID}_link`);
|
||||
let linkInput = linkEl.querySelector("textarea");
|
||||
let linkTextarea = linkInput.nextElementSibling;
|
||||
let linkImg = linkEl.querySelector("div[data-img-container] img");
|
||||
let editActions = linkEl.querySelector("[data-edit-actions]");
|
||||
let linkImg = linkEl.querySelector("div:first-child img");
|
||||
let editActions = linkEl.querySelector("div:nth-child(3)");
|
||||
|
||||
if (currentlyEditing.icon !== undefined) {
|
||||
linkImg.src = currentlyEditing.icon;
|
||||
}
|
||||
|
||||
editActions.querySelector("div[data-primary-actions]").style.display = "";
|
||||
editActions.querySelector("div:first-child").style.display = "";
|
||||
|
||||
// teleport the teleported elements back to the body for literally safe keeping
|
||||
unteleportElement(selectIconButton);
|
||||
@@ -557,7 +560,10 @@ function deleteLink(target) {
|
||||
let linkID = parseInt(linkEl.id);
|
||||
let categoryID = parseInt(linkEl.parentElement.previousElementSibling.id);
|
||||
|
||||
if (currentlyEditing.linkID !== undefined) {
|
||||
if (
|
||||
currentlyEditing.linkID !== undefined ||
|
||||
currentlyEditing.categoryID !== undefined
|
||||
) {
|
||||
// cancel the edit if it's already in progress
|
||||
cancelEdit();
|
||||
}
|
||||
@@ -596,14 +602,17 @@ function editCategory(target) {
|
||||
let categoryEl = target.closest(".category-header");
|
||||
let categoryID = parseInt(categoryEl.id);
|
||||
|
||||
if (currentlyEditing.linkID !== undefined) {
|
||||
if (
|
||||
currentlyEditing.linkID !== undefined ||
|
||||
currentlyEditing.categoryID !== undefined
|
||||
) {
|
||||
// cancel the edit if it's already in progress
|
||||
cancelEdit();
|
||||
}
|
||||
|
||||
let categoryName = categoryEl.querySelector("h2");
|
||||
let categoryIcon = categoryEl.querySelector("div[data-img-container] img");
|
||||
let editActions = categoryEl.querySelector("[data-edit-actions]");
|
||||
let categoryIcon = categoryEl.querySelector("div:first-child img");
|
||||
let editActions = categoryEl.querySelector("div:nth-child(3)");
|
||||
|
||||
currentlyEditing = {
|
||||
type: "category",
|
||||
@@ -622,8 +631,7 @@ function editCategory(target) {
|
||||
teleportElement(selectIconButton, categoryIcon.parentElement);
|
||||
teleportElement(confirmActions, editActions);
|
||||
|
||||
editActions.querySelector("div[data-primary-actions]").style.display =
|
||||
"none";
|
||||
editActions.querySelector("div:first-child").style.display = "none";
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
currentlyEditing.cleanup = replaceWithResizableTextarea([
|
||||
@@ -659,6 +667,7 @@ async function confirmCategoryEdit() {
|
||||
|
||||
// nothing to update
|
||||
if (formData.get("name") === null && formData.get("icon") === null) {
|
||||
cancelEdit();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -686,8 +695,8 @@ function cancelCategoryEdit(text = currentlyEditing.originalText) {
|
||||
);
|
||||
|
||||
let categoryInput = categoryEl.querySelector("textarea");
|
||||
let categoryIcon = categoryEl.querySelector(".category-img img");
|
||||
let editActions = categoryEl.querySelector("[data-edit-actions]");
|
||||
let categoryIcon = categoryEl.querySelector("div:first-child img");
|
||||
let editActions = categoryEl.querySelector("div:nth-child(3)");
|
||||
|
||||
if (currentlyEditing.icon !== undefined) {
|
||||
categoryIcon.src = currentlyEditing.icon;
|
||||
@@ -696,7 +705,7 @@ function cancelCategoryEdit(text = currentlyEditing.originalText) {
|
||||
unteleportElement(selectIconButton);
|
||||
unteleportElement(confirmActions);
|
||||
|
||||
editActions.querySelector("div[data-primary-actions]").style.display = "";
|
||||
editActions.querySelector("div:first-child").style.display = "";
|
||||
|
||||
restoreElementFromInput(categoryInput, text);
|
||||
|
||||
@@ -711,7 +720,10 @@ function cancelCategoryEdit(text = currentlyEditing.originalText) {
|
||||
function deleteCategory(target) {
|
||||
let categoryEl = target.closest(".category-header");
|
||||
|
||||
if (currentlyEditing.categoryID !== undefined) {
|
||||
if (
|
||||
currentlyEditing.categoryID !== undefined ||
|
||||
currentlyEditing.linkID !== undefined
|
||||
) {
|
||||
// cancel the edit if it's already in progress
|
||||
cancelEdit();
|
||||
}
|
||||
@@ -815,8 +827,14 @@ function replaceWithResizableTextarea(targetEls) {
|
||||
let maxWidth = parentBoundingRect.width - borderWidth;
|
||||
// take care of category headers specifically because the parent bounding box contains two other elements
|
||||
if (targetEl.tagName === "H2") {
|
||||
let imageComputedStyle = window.getComputedStyle(
|
||||
targetEl.previousElementSibling
|
||||
);
|
||||
|
||||
let imageWidth =
|
||||
targetEl.previousElementSibling.getBoundingClientRect().width;
|
||||
targetEl.previousElementSibling.getBoundingClientRect().width +
|
||||
parseFloat(imageComputedStyle.marginLeft) +
|
||||
parseFloat(imageComputedStyle.marginRight);
|
||||
let actionButtonWidth =
|
||||
targetEl.nextElementSibling.getBoundingClientRect().width;
|
||||
|
||||
@@ -898,11 +916,12 @@ function replaceWithResizableTextarea(targetEls) {
|
||||
// step 3: batch writes
|
||||
let inputElements = [];
|
||||
|
||||
elsInitialStyles.forEach((elInfo) => {
|
||||
elsInitialStyles.forEach((elInfo, i) => {
|
||||
const inputElement = document.createElement("textarea");
|
||||
inputElement.value = elInfo.originalText;
|
||||
inputElement.className = "resizable-input";
|
||||
inputElement.placeholder = elInfo.targetEl.dataset.placeholder;
|
||||
inputElement.placeholder =
|
||||
i == 0 ? "Enter title..." : "Enter description...";
|
||||
inputElement.dataset.originalElementType = elInfo.targetEl.tagName;
|
||||
inputElement.dataset.originalClassName = elInfo.targetEl.className;
|
||||
|
||||
@@ -951,9 +970,15 @@ function replaceWithResizableTextarea(targetEls) {
|
||||
|
||||
// is it maybe a bit of some math that doesnt entirely make sense to me? you bet. But does it work? Hell yeah it does
|
||||
if (inputElement.dataset.originalElementType === "H2") {
|
||||
let imageComputedStyle = window.getComputedStyle(
|
||||
inputElement.previousElementSibling
|
||||
);
|
||||
|
||||
let imageWidth =
|
||||
inputElement.previousElementSibling.getBoundingClientRect()
|
||||
.width;
|
||||
.width +
|
||||
imageComputedStyle.marginLeft +
|
||||
imageComputedStyle.marginRight;
|
||||
let actionButtonWidth =
|
||||
inputElement.nextElementSibling.getBoundingClientRect().width;
|
||||
|
||||
|
||||
@@ -1,70 +1,238 @@
|
||||
.modal-bg {
|
||||
visibility: hidden;
|
||||
opacity: 0;
|
||||
}
|
||||
@import "./base.css";
|
||||
@import "./card.css";
|
||||
|
||||
.modal-bg.is-visible {
|
||||
visibility: visible;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.modal {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.modal.is-visible {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
@layer components {
|
||||
.modal-bg {
|
||||
position: fixed;
|
||||
visibility: hidden;
|
||||
opacity: 0;
|
||||
|
||||
transition: opacity 0.3s ease, visibility 0s 0.3s;
|
||||
transition-timing-function: cubic-bezier(0.45, 0, 0.55, 1);
|
||||
inset: 0;
|
||||
background-color: color-mix(in srgb, #000 45%, #0000);
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.modal-bg.is-visible {
|
||||
visibility: visible;
|
||||
opacity: 1;
|
||||
transition-delay: 0s;
|
||||
}
|
||||
|
||||
.modal {
|
||||
opacity: 0;
|
||||
transform: translateY(20px) scale(0.95);
|
||||
|
||||
transition: opacity 0.3s ease, transform 0.3s ease;
|
||||
transition-timing-function: cubic-bezier(0.45, 0, 0.55, 1);
|
||||
background-color: var(--color-overlay);
|
||||
border-radius: calc(var(--spacing) * 3);
|
||||
overflow: hidden;
|
||||
padding: calc(var(--spacing) * 4);
|
||||
width: 100%;
|
||||
max-width: 24rem;
|
||||
}
|
||||
|
||||
.modal.is-visible {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
transform: translateY(0) scale(1);
|
||||
transition-delay: 0s;
|
||||
}
|
||||
}
|
||||
|
||||
.action-button {
|
||||
display: flex;
|
||||
width: fit-content;
|
||||
height: fit-content;
|
||||
padding: 0.25rem;
|
||||
background-color: var(--color-highlight-sm);
|
||||
border: 1px solid color-mix(in srgb, var(--color-highlight) 70%, #0000);
|
||||
border-radius: 9999px;
|
||||
box-shadow: var(--shadow-sm);
|
||||
cursor: pointer;
|
||||
transition: filter 0.15s cubic-bezier(0.45, 0, 0.55, 1);
|
||||
contain: layout style paint;
|
||||
|
||||
&:hover {
|
||||
filter: brightness(125%);
|
||||
}
|
||||
|
||||
&:active {
|
||||
filter: brightness(95%);
|
||||
}
|
||||
|
||||
#blur-target {
|
||||
transition: filter 300ms cubic-bezier(0.45, 0, 0.55, 1);
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
.modal-bg {
|
||||
visibility: hidden;
|
||||
opacity: 0;
|
||||
|
||||
transition: opacity 0.3s ease, visibility 0s 0.3s;
|
||||
transition-timing-function: cubic-bezier(0.45, 0, 0.55, 1);
|
||||
}
|
||||
|
||||
.modal-bg.is-visible {
|
||||
visibility: visible;
|
||||
opacity: 1;
|
||||
transition-delay: 0s;
|
||||
}
|
||||
|
||||
.modal {
|
||||
opacity: 0;
|
||||
transform: translateY(20px) scale(0.95);
|
||||
|
||||
transition: opacity 0.3s ease, transform 0.3s ease;
|
||||
transition-timing-function: cubic-bezier(0.45, 0, 0.55, 1);
|
||||
}
|
||||
|
||||
.modal.is-visible {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
transform: translateY(0) scale(1);
|
||||
transition-delay: 0s;
|
||||
}
|
||||
|
||||
#blur-target {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
|
||||
.modal-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: calc(var(--spacing) * 2);
|
||||
|
||||
& > div {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: calc(var(--spacing) * 1);
|
||||
}
|
||||
|
||||
& > button {
|
||||
background-color: var(--color-accent);
|
||||
color: #fff;
|
||||
border-radius: calc(var(--spacing) * 1.5);
|
||||
padding-inline: calc(var(--spacing) * 4);
|
||||
padding-block: calc(var(--spacing) * 2);
|
||||
}
|
||||
}
|
||||
|
||||
.delete-modal {
|
||||
text-align: center;
|
||||
|
||||
& > p {
|
||||
margin-bottom: calc(var(--spacing) * 3);
|
||||
}
|
||||
|
||||
& > div {
|
||||
display: flex;
|
||||
justify-content: end;
|
||||
flex-direction: column;
|
||||
row-gap: calc(var(--spacing) * 2);
|
||||
|
||||
& > button {
|
||||
padding-inline: calc(var(--spacing) * 4);
|
||||
padding-block: calc(var(--spacing) * 2);
|
||||
border-radius: calc(var(--spacing) * 1.5);
|
||||
width: 100%;
|
||||
color: #fff;
|
||||
transition: filter 300ms cubic-bezier(0.45, 0, 0.55, 1);
|
||||
|
||||
&:nth-child(1) {
|
||||
background-color: var(--color-error);
|
||||
}
|
||||
|
||||
&:nth-child(2) {
|
||||
border: 1px solid var(--color-highlight);
|
||||
background-color: #0000;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
filter: brightness(125%);
|
||||
}
|
||||
|
||||
&:active {
|
||||
filter: brightness(95%);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
input:invalid.invalid {
|
||||
border: 1px solid var(--color-error);
|
||||
}
|
||||
|
||||
.new-link-card {
|
||||
border: calc(var(--spacing) * 0.5) dashed var(--color-subtle);
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
.action-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: calc(var(--spacing) * 2);
|
||||
}
|
||||
|
||||
.link-grid > div > div:nth-child(3) {
|
||||
position: absolute;
|
||||
right: var(--spacing);
|
||||
top: var(--spacing);
|
||||
}
|
||||
|
||||
.category-header > div:nth-child(2) {
|
||||
padding-left: calc(var(--spacing) * 2);
|
||||
}
|
||||
|
||||
.add-category-button {
|
||||
width: fit-content;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: var(--color-subtle);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.add-category-button > h2 {
|
||||
text-decoration: underline;
|
||||
text-decoration-style: dashed;
|
||||
text-decoration-thickness: calc(var(--spacing) * 0.5);
|
||||
|
||||
&:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
.action-button {
|
||||
display: flex;
|
||||
width: fit-content;
|
||||
height: fit-content;
|
||||
padding: 0.25rem;
|
||||
background-color: var(--color-highlight-sm);
|
||||
border: 1px solid color-mix(in srgb, var(--color-highlight) 70%, #0000);
|
||||
border-radius: 9999px;
|
||||
box-shadow: var(--shadow-sm);
|
||||
cursor: pointer;
|
||||
transition: filter 0.15s cubic-bezier(0.45, 0, 0.55, 1);
|
||||
contain: layout style paint;
|
||||
|
||||
&:hover {
|
||||
filter: brightness(125%);
|
||||
}
|
||||
|
||||
&:active {
|
||||
filter: brightness(95%);
|
||||
}
|
||||
}
|
||||
|
||||
.select-icon-button {
|
||||
display: flex;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: color-mix(in srgb, var(--color-highlight) 70%, #0000);
|
||||
color: var(--color-base);
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
header {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
padding: calc(var(--spacing) * 3);
|
||||
|
||||
& > a {
|
||||
text-decoration: none;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: calc(var(--spacing) * 2);
|
||||
color: var(--color-text);
|
||||
border-bottom: 1px solid var(--color-text);
|
||||
justify-content: center;
|
||||
line-height: var(--leading-condensed);
|
||||
|
||||
&:hover {
|
||||
border-bottom-color: transparent;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
199
src/styles/base.css
Normal file
199
src/styles/base.css
Normal file
@@ -0,0 +1,199 @@
|
||||
@layer reset, base, components, utilities;
|
||||
@font-face {
|
||||
font-family: "Instrument Sans";
|
||||
src: url("/assets/fonts/InstrumentSans-VariableFont_wdth,wght.woff2")
|
||||
format("woff2");
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@layer reset {
|
||||
/*
|
||||
Josh's Custom CSS Reset slightly Modified
|
||||
https://www.joshwcomeau.com/css/custom-css-reset/
|
||||
*/
|
||||
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
* {
|
||||
border: 0 solid;
|
||||
line-height: calc(1em + 0.5rem);
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
img,
|
||||
picture,
|
||||
video,
|
||||
canvas,
|
||||
svg {
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
input,
|
||||
button,
|
||||
textarea,
|
||||
select {
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
button {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
p,
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
p {
|
||||
text-wrap: pretty;
|
||||
hyphens: auto;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
text-wrap: balance;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
--family-sans: "Instrument Sans", ui-sans-serif, system-ui, sans-serif,
|
||||
"Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol",
|
||||
"Noto Color Emoji";
|
||||
--color-accent: oklch(57.93% 0.258 294.12);
|
||||
--color-success: oklch(70.19% 0.158 160.44);
|
||||
--color-error: oklch(53% 0.251 28.48);
|
||||
|
||||
--color-base: oklch(11% 0.007 285);
|
||||
--color-surface: oklch(19% 0.007 285.66);
|
||||
--color-overlay: oklch(26% 0.008 285.66);
|
||||
|
||||
--color-muted: oklch(63% 0.015 286);
|
||||
--color-subtle: oklch(72% 0.015 286);
|
||||
--color-text: oklch(87% 0.015 286);
|
||||
|
||||
--color-highlight-sm: oklch(30.67% 0.007 286);
|
||||
--color-highlight: oklch(39.26% 0.01 286);
|
||||
--color-highlight-lg: oklch(47.72% 0.011 286);
|
||||
|
||||
--spacing: 0.25rem;
|
||||
|
||||
--leading-condensed: normal;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
font-family: var(--family-sans);
|
||||
color-scheme: dark;
|
||||
color: var(--color-text);
|
||||
background-color: var(--color-surface);
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: clamp(42px, 10vw, 64px);
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: clamp(30px, 6vw, 36px);
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
input:not(.search) {
|
||||
color: var(--color-text);
|
||||
padding-inline: calc(var(--spacing) * 4);
|
||||
padding-block: calc(var(--spacing) * 2);
|
||||
|
||||
border-radius: calc(var(--spacing) * 1.5);
|
||||
|
||||
width: 100%;
|
||||
background-color: var(--color-surface);
|
||||
border: 1px solid color-mix(in srgb, var(--color-highlight) 70%, #0000);
|
||||
|
||||
transition-property: color, border, background-color;
|
||||
transition-duration: 300ms;
|
||||
transition-timing-function: cubic-bezier(0.45, 0, 0.55, 1);
|
||||
|
||||
overflow: hidden;
|
||||
|
||||
&::placeholder {
|
||||
font-style: italic;
|
||||
color: var(--color-highlight);
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
&[type="file"] {
|
||||
padding: 0;
|
||||
cursor: pointer;
|
||||
|
||||
&::file-selector-button {
|
||||
border: 0px;
|
||||
padding: calc(var(--spacing) * 2);
|
||||
margin-right: var(--spacing);
|
||||
background-color: var(--color-highlight);
|
||||
color: var(--color-subtle);
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@layer utilities {
|
||||
.flex {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.leading-condensed {
|
||||
line-height: var(--leading-condensed);
|
||||
}
|
||||
|
||||
.text-error {
|
||||
color: var(--color-error);
|
||||
}
|
||||
|
||||
.text-success {
|
||||
color: var(--color-success);
|
||||
}
|
||||
}
|
||||
148
src/styles/card.css
Normal file
148
src/styles/card.css
Normal file
@@ -0,0 +1,148 @@
|
||||
/* All css related to the card and category stuff */
|
||||
|
||||
@layer components {
|
||||
.link-grid > :is(a, div) {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
border-radius: calc(var(--spacing) * 4);
|
||||
padding: calc(var(--spacing) * 2.5);
|
||||
align-items: center;
|
||||
transition-property: box-shadow, transform, translate;
|
||||
transition-duration: 150ms;
|
||||
transition-timing-function: cubic-bezier(0.45, 0, 0.55, 1);
|
||||
box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1),
|
||||
0 2px 4px -2px rgb(0 0 0 / 0.1);
|
||||
contain: layout style paint;
|
||||
|
||||
&:not(:is(div)) {
|
||||
&:hover {
|
||||
box-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1),
|
||||
0 4px 6px -4px rgb(0 0 0 / 0.1);
|
||||
transform: translateY(-4px);
|
||||
}
|
||||
|
||||
&:active {
|
||||
box-shadow: 0 2px 4px -2px rgb(0 0 0 / 0.1);
|
||||
transform: translateY(2px);
|
||||
}
|
||||
}
|
||||
|
||||
&:not(.new-link-card) {
|
||||
text-decoration: none;
|
||||
background: var(--color-overlay);
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.link-grid > :is(a, div) {
|
||||
transition: none;
|
||||
|
||||
&:hover {
|
||||
transform: none;
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Div that holds the image */
|
||||
.link-grid > :is(a, div) > div:first-child {
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
margin-right: calc(var(--spacing) * 2);
|
||||
border-radius: calc(var(--spacing) * 2.5);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.link-grid > :is(a, div) > div:first-child img {
|
||||
user-select: none;
|
||||
aspect-ratio: 1/1;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
/* Div that holds the text */
|
||||
.link-grid > :is(a, div) > div:nth-child(2) {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
flex-direction: column;
|
||||
row-gap: 1px;
|
||||
overflow: hidden;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.link-grid > :is(a, div) > div:nth-child(2) h3 {
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
|
||||
.link-grid > :is(a, div) > div:nth-child(2) p {
|
||||
color: var(--color-subtle);
|
||||
white-space: pre-wrap;
|
||||
border: 1px solid #0000;
|
||||
min-height: calc(1em + 0.5rem);
|
||||
border: 1px solid transparent;
|
||||
min-height: 26px;
|
||||
}
|
||||
|
||||
.category-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.category-header > div:first-child {
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
position: relative;
|
||||
margin-right: calc(var(--spacing) * 2);
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: calc(var(--spacing) * 8);
|
||||
height: calc(var(--spacing) * 8);
|
||||
border-radius: calc(var(--spacing) * 1.5);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.categoy-header > div:first-child img {
|
||||
user-select: none;
|
||||
object-fit: cover;
|
||||
aspect-ratio: 1/1;
|
||||
}
|
||||
|
||||
.category-header h2 {
|
||||
text-transform: capitalize;
|
||||
word-break: break-all;
|
||||
border-width: 1px;
|
||||
border-color: #0000;
|
||||
}
|
||||
|
||||
.link-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(min(330px, 100%), 1fr));
|
||||
gap: calc(var(--spacing) * 2);
|
||||
padding: calc(var(--spacing) * 2.5);
|
||||
contain: layout style paint;
|
||||
}
|
||||
|
||||
/* Empty state */
|
||||
.link-grid > p {
|
||||
color: var(--color-subtle);
|
||||
}
|
||||
|
||||
.card-section {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.card-section > div {
|
||||
width: 100%;
|
||||
padding: calc(var(--spacing) * 2.5);
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
.card-section > div {
|
||||
width: 80%;
|
||||
}
|
||||
}
|
||||
}
|
||||
58
src/styles/login.css
Normal file
58
src/styles/login.css
Normal file
@@ -0,0 +1,58 @@
|
||||
@import "./base.css";
|
||||
|
||||
@layer base {
|
||||
html,
|
||||
body {
|
||||
background-color: var(--color-base);
|
||||
}
|
||||
|
||||
body {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
@layer components {
|
||||
.login-container {
|
||||
width: fit-content;
|
||||
display: flex;
|
||||
position: relative;
|
||||
background-color: var(--color-surface);
|
||||
border-radius: calc(var(--spacing) * 3);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.login-container > img {
|
||||
height: calc(var(--spacing) * 96);
|
||||
width: calc(var(--spacing) * 64);
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.login-container > div {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: calc(var(--spacing) * 4);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.login-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
row-gap: calc(var(--spacing) * 3);
|
||||
margin-top: calc(var(--spacing) * 2);
|
||||
margin-bottom: calc(var(--spacing) * 2);
|
||||
}
|
||||
|
||||
.login-form button {
|
||||
padding-left: calc(var(--spacing) * 4);
|
||||
padding-right: calc(var(--spacing) * 4);
|
||||
padding-top: calc(var(--spacing) * 2);
|
||||
padding-bottom: calc(var(--spacing) * 2);
|
||||
|
||||
border-radius: calc(var(--spacing) * 2.5);
|
||||
|
||||
background-color: var(--color-accent);
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
133
src/styles/main.css
Normal file
133
src/styles/main.css
Normal file
@@ -0,0 +1,133 @@
|
||||
@import "./base.css";
|
||||
@import "./card.css";
|
||||
|
||||
@layer components {
|
||||
.hero {
|
||||
/* grid grid-rows-3 grid-cols-[1fr] justify-center items-center h-screen bg-base */
|
||||
display: grid;
|
||||
grid-template: repeat(3, minmax(0, 1fr)) / 1fr;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100vh;
|
||||
background-color: var(--color-base);
|
||||
}
|
||||
|
||||
.glance-container {
|
||||
display: flex;
|
||||
color: var(--color-subtle);
|
||||
height: 100%;
|
||||
padding: calc(var(--spacing) * 2.5);
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.primary-hero-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
grid-row-start: 2;
|
||||
padding-left: calc(var(--spacing) * 3);
|
||||
padding-right: calc(var(--spacing) * 3);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.primary-hero-container > div {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: calc(var(--spacing) * 2.5);
|
||||
|
||||
& > svg {
|
||||
margin-right: calc(var(--spacing) * 3);
|
||||
aspect-ratio: 1/1;
|
||||
width: clamp(42px, 10vw, 60px);
|
||||
}
|
||||
}
|
||||
|
||||
.primary-hero-container > form {
|
||||
width: 100%;
|
||||
max-width: 48rem;
|
||||
|
||||
& > input {
|
||||
color: #fff;
|
||||
width: 100%;
|
||||
background-color: var(--color-surface);
|
||||
border: 1px solid
|
||||
color-mix(in srgb, var(--color-highlight-sm) 70%, #0000);
|
||||
padding-left: calc(var(--spacing) * 3);
|
||||
padding-right: calc(var(--spacing) * 3);
|
||||
padding-top: var(--spacing);
|
||||
padding-bottom: var(--spacing);
|
||||
height: calc(var(--spacing) * 7);
|
||||
border-radius: 9999px;
|
||||
|
||||
&::placeholder {
|
||||
font-style: italic;
|
||||
color: var(--color-highlight);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.weather-data {
|
||||
display: flex;
|
||||
height: fit-content;
|
||||
align-items: center;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.weather-data span {
|
||||
margin-right: calc(var(--spacing) * 2);
|
||||
}
|
||||
|
||||
.uptime-data {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: end;
|
||||
|
||||
& > div {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
& > span {
|
||||
margin-right: calc(var(--spacing) * 2);
|
||||
line-height: var(--leading-condensed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.uptime-status {
|
||||
position: relative;
|
||||
display: flex;
|
||||
margin-top: auto;
|
||||
margin-bottom: auto;
|
||||
width: calc(var(--spacing) * 2);
|
||||
height: calc(var(--spacing) * 2);
|
||||
}
|
||||
|
||||
.uptime-status > svg {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
fill: currentColor;
|
||||
}
|
||||
|
||||
.uptime-status > svg:nth-child(1) {
|
||||
position: absolute;
|
||||
animation: ping 1s linear infinite;
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
|
||||
@layer utilities {
|
||||
.animate-ping {
|
||||
animation: ping 1s cubic-bezier(0, 0, 0.2, 1) infinite;
|
||||
}
|
||||
|
||||
@keyframes ping {
|
||||
75%,
|
||||
100% {
|
||||
transform: scale(2);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,195 +0,0 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
@theme {
|
||||
--color-accent: oklch(57.93% 0.258 294.12);
|
||||
--color-success: oklch(70.19% 0.158 160.44);
|
||||
--color-error: oklch(53% 0.251 28.48);
|
||||
|
||||
--color-base: oklch(11% 0.007 285);
|
||||
--color-surface: oklch(19% 0.007 285.66);
|
||||
--color-overlay: oklch(26% 0.008 285.66);
|
||||
|
||||
--color-muted: oklch(63% 0.015 286);
|
||||
--color-subtle: oklch(72% 0.015 286);
|
||||
--color-text: oklch(87% 0.015 286);
|
||||
|
||||
--color-highlight-sm: oklch(30.67% 0.007 286);
|
||||
--color-highlight: oklch(39.26% 0.01 286);
|
||||
--color-highlight-lg: oklch(47.72% 0.011 286);
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Instrument Sans";
|
||||
src: url("/assets/fonts/InstrumentSans-VariableFont_wdth,wght.woff2")
|
||||
format("woff2");
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
:root {
|
||||
--default-font-family: "Instrument Sans", ui-sans-serif, system-ui,
|
||||
sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol",
|
||||
"Noto Color Emoji";
|
||||
}
|
||||
|
||||
html {
|
||||
line-height: normal;
|
||||
color-scheme: dark;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
@layer base {
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: clamp(42px, 10vw, 64px);
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: clamp(30px, 6vw, 36px);
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
button {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
input:not(.search) {
|
||||
@apply px-4 py-2 rounded-md w-full bg-surface border border-highlight/70 placeholder:text-highlight text-text 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-highlight text-subtle cursor-pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.link-card {
|
||||
background: var(--color-overlay);
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
text-decoration: none;
|
||||
border-radius: 1rem;
|
||||
padding: 0.625rem;
|
||||
align-items: center;
|
||||
transition-property: box-shadow, transform, translate;
|
||||
transition-duration: 150ms;
|
||||
transition-timing-function: cubic-bezier(0.45, 0, 0.55, 1);
|
||||
box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
|
||||
contain: layout style paint;
|
||||
|
||||
&:not(.admin) {
|
||||
&:hover {
|
||||
box-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1),
|
||||
0 4px 6px -4px rgb(0 0 0 / 0.1);
|
||||
transform: translateY(-4px);
|
||||
}
|
||||
|
||||
&:active {
|
||||
box-shadow: 0 2px 4px -2px rgb(0 0 0 / 0.1);
|
||||
transform: translateY(2px);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.link-card {
|
||||
transition: none;
|
||||
|
||||
&:hover {
|
||||
transform: none;
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Div that holds the image */
|
||||
.link-card div[data-img-container] {
|
||||
flex-shrink: 0;
|
||||
padding-right: 0.5rem;
|
||||
}
|
||||
|
||||
.link-card div[data-img-container] img {
|
||||
user-select: none;
|
||||
border-radius: 0.375rem;
|
||||
aspect-ratio: 1/1;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
/* Div that holds the text */
|
||||
.link-card div[data-text-container] {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
flex-direction: column;
|
||||
row-gap: 1px;
|
||||
overflow: hidden;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.link-card div[data-text-container] p {
|
||||
color: var(--color-subtle);
|
||||
white-space: pre-wrap;
|
||||
border: 1px solid #0000;
|
||||
min-height: 22px;
|
||||
}
|
||||
|
||||
.new-link-card {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding: 0.625rem;
|
||||
border: 0.125rem dashed var(--color-subtle);
|
||||
border-radius: 1rem;
|
||||
transition: box-shadow, transofrm 150ms cubic-bezier(0.45, 0, 0.55, 1);
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
.categoy-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.category-header div[data-img-container] {
|
||||
@apply shrink-0 relative mr-2 h-full flex items-center justify-center size-8;
|
||||
}
|
||||
|
||||
.categoy-header div[data-img-container] img {
|
||||
user-select: none;
|
||||
object-fit: cover;
|
||||
aspect-ratio: 1/1;
|
||||
}
|
||||
|
||||
.category-header h2 {
|
||||
text-transform: capitalize;
|
||||
word-break: break-all;
|
||||
border-width: 1px;
|
||||
border-color: #0000;
|
||||
}
|
||||
|
||||
.link-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(min(330px, 100%), 1fr));
|
||||
gap: 0.5rem;
|
||||
padding: 0.625rem;
|
||||
contain: layout style paint;
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<title>Passport</title>
|
||||
<link rel="favicon" href="/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link rel="preload" as="font" type="font/woff2" crossorigin="anonymous"
|
||||
href="/assets/fonts/InstrumentSans-VariableFont_wdth,wght.woff2" />
|
||||
{{{embedFile "assets/tailwind.css"}}}
|
||||
{{{embedFile "styles/adminUi.css"}}}
|
||||
</head>
|
||||
|
||||
<body class="bg-surface text-text">
|
||||
{{embed}}
|
||||
</body>
|
||||
|
||||
{{{devContent}}}
|
||||
|
||||
</html>
|
||||
@@ -1,19 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<title>Passport</title>
|
||||
<link rel="favicon" href="/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link rel="preload" as="font" type="font/woff2" crossorigin="anonymous"
|
||||
href="/assets/fonts/InstrumentSans-VariableFont_wdth,wght.woff2" />
|
||||
{{{embedFile "assets/tailwind.css"}}}
|
||||
</head>
|
||||
|
||||
<body class="bg-surface text-text">
|
||||
{{embed}}
|
||||
</body>
|
||||
|
||||
{{{devContent}}}
|
||||
|
||||
</html>
|
||||
111
src/templates/partials/category-grid.hbs
Normal file
111
src/templates/partials/category-grid.hbs
Normal file
@@ -0,0 +1,111 @@
|
||||
<section class="card-section">
|
||||
<div>
|
||||
{{#each Categories}}
|
||||
<div class="category-header" id="{{this.ID}}_category">
|
||||
<div>
|
||||
<img width="32" height="32" draggable="false" alt="{{this.Name}}" src="{{this.Icon}}" />
|
||||
</div>
|
||||
<h2 class="capitalize break-all">{{this.Name}}</h2>
|
||||
{{#if IsAdmin}}
|
||||
<div>
|
||||
<div class="action-container">
|
||||
<button aria-label="Edit category" onclick="editCategory(this)" class="action-button">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
|
||||
<use href="#edit-icon" />
|
||||
</svg>
|
||||
</button>
|
||||
<button aria-label="Delete category" onclick="deleteCategory(this)"
|
||||
class="text-error action-button">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
|
||||
<use href="#trash-icon" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
<div class="link-grid">
|
||||
{{#each this.Links}}
|
||||
|
||||
{{#if IsAdmin}}<div data-card id="{{this.ID}}_link" {{else}} <a href="{{this.URL}}" draggable="false"
|
||||
target="_blank" rel="noreferrer" {{/if}}>
|
||||
|
||||
<div>
|
||||
<img width="64" height="64" draggable="false" src="{{this.Icon}}" alt="{{this.Name}}" />
|
||||
</div>
|
||||
<div>
|
||||
<h3>{{this.Name}}</h3>
|
||||
<p>{{this.Description}}</p>
|
||||
</div>
|
||||
{{#if IsAdmin}}
|
||||
<div>
|
||||
<div class="action-container">
|
||||
<button aria-label="Edit link" onclick="editLink(this)" class="action-button">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
|
||||
<use href="#edit-icon" />
|
||||
</svg>
|
||||
</button>
|
||||
<button aria-label="Delete link" onclick="deleteLink(this)" class="text-error action-button">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
|
||||
<use href="#trash-icon" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{#if IsAdmin}}
|
||||
</div {{else}} </a {{/if}}>
|
||||
|
||||
{{else}}
|
||||
{{#unless IsAdmin}}
|
||||
<p class="text-subtle">No links here, add one!</p>
|
||||
{{/unless}}
|
||||
{{/each}}
|
||||
|
||||
{{#if IsAdmin}}
|
||||
<div onclick="openModal('link', {{this.ID}})" class="new-link-card link-card admin">
|
||||
<svg 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"
|
||||
stroke-width="2" d="M12 5v14m-7-7h14" />
|
||||
</svg>
|
||||
<div>
|
||||
<h3>Add a link</h3>
|
||||
</div>
|
||||
</div>
|
||||
{{/if }}
|
||||
</div>
|
||||
{{/each}}
|
||||
|
||||
{{#if IsAdmin}}
|
||||
<div class="add-category-button" id="add-category-button">
|
||||
<svg 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"
|
||||
d="M12 5v14m-7-7h14" />
|
||||
</svg>
|
||||
<h2 onclick="openModal('category')">
|
||||
Add a new category
|
||||
</h2>
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- store the svg icons here so that they can be reused -->
|
||||
{{#if IsAdmin}}
|
||||
<div class="hidden">
|
||||
<svg id="edit-icon" xmlns="http://www.w3.org/2000/svg" width="24" height="24"
|
||||
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">
|
||||
<path d="M7 7H6a2 2 0 0 0-2 2v9a2 2 0 0 0 2 2h9a2 2 0 0 0 2-2v-1" />
|
||||
<path d="M20.385 6.585a2.1 2.1 0 0 0-2.97-2.97L9 12v3h3zM16 5l3 3" />
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
<svg id="trash-icon" xmlns="http://www.w3.org/2000/svg" width="24" height="24"
|
||||
viewBox="0 0 24 24"><!-- Icon from Tabler Icons by Paweł Kuna - https://github.com/tabler/tabler-icons/blob/master/LICENSE -->
|
||||
<path fill="none" stroke="currentColor" 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" />
|
||||
</svg>
|
||||
</div>
|
||||
{{/if}}
|
||||
@@ -1,7 +1,6 @@
|
||||
<div id="category-contents" class="hidden">
|
||||
<h3>Create A category</h3>
|
||||
<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">
|
||||
<form id="category-form" action="/api/categories" method="post" class="modal-form">
|
||||
<div>
|
||||
<label for="categoryName">Name</label>
|
||||
<input required type="text" name="name" id="categoryName" maxlength="50" />
|
||||
@@ -10,7 +9,7 @@
|
||||
<label for="linkIcon">Icon</label>
|
||||
<input type="file" name="icon" id="linkIcon" accept=".svg" required />
|
||||
</div>
|
||||
<button class="px-4 py-2 rounded-md w-full bg-accent text-white border-0" type="submit">Create
|
||||
<button type="submit">Create
|
||||
category</button>
|
||||
</form>
|
||||
<span id="category-message"></span>
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
<div id="category-delete-contents" class="hidden text-center">
|
||||
<div id="category-delete-contents" class="hidden delete-modal">
|
||||
<h3>Are you sure you want to delete this category?</h3>
|
||||
<p class="mb-3">You are about to delete the category <strong id="category-name"></strong>. This action cannot be
|
||||
<p>You are about to delete the category <strong id="category-name"></strong>. This action cannot be
|
||||
undone.
|
||||
All links associated with this category will also be deleted. Are you sure you want to continue?</p>
|
||||
<div class="flex justify-end flex-col gap-y-2">
|
||||
<button class="px-4 py-2 rounded-md w-full bg-error text-white border-0"
|
||||
onclick="confirmDeleteCategory()">Delete
|
||||
<div>
|
||||
<button onclick="confirmDeleteCategory()">Delete
|
||||
category</button>
|
||||
<button class="px-4 py-2 rounded-md w-full bg-overlay border border-highlight text-white"
|
||||
onclick="closeModal()">Cancel</button>
|
||||
<button onclick="closeModal()">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,12 +1,11 @@
|
||||
<div id="link-delete-contents" class="hidden text-center">
|
||||
<div id="link-delete-contents" class="hidden delete-modal">
|
||||
<h3>Are you sure you want to delete this link?</h3>
|
||||
<p class="mb-3">You are about to delete the link <strong id="link-name"></strong>. This action cannot be undone. Are
|
||||
<p>You are about to delete the link <strong id="link-name"></strong>. This action cannot be undone. Are
|
||||
you sure you
|
||||
want to continue?</p>
|
||||
<div class="flex justify-end flex-col gap-y-2">
|
||||
<button class="px-4 py-2 rounded-md w-full bg-error text-white border-0" onclick="confirmDeleteLink()">Delete
|
||||
<div>
|
||||
<button onclick="confirmDeleteLink()">Delete
|
||||
link</button>
|
||||
<button class="px-4 py-2 rounded-md w-full bg-overlay border border-highlight text-white"
|
||||
onclick="closeModal()">Cancel</button>
|
||||
<button onclick="closeModal()">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,7 +1,6 @@
|
||||
<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">
|
||||
<form id="link-form" action="/api/links" method="post" class="modal-form">
|
||||
<div>
|
||||
<label for="linkName">Name</label>
|
||||
<input required type="text" name="name" id="linkName" maxlength="50" />
|
||||
@@ -18,7 +17,7 @@
|
||||
<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-accent text-white border-0" type="submit">Add
|
||||
<button type="submit">Add
|
||||
link</button>
|
||||
</form>
|
||||
<span id="link-message"></span>
|
||||
|
||||
@@ -1,208 +1,132 @@
|
||||
<div id="blur-target"
|
||||
class="transition-[filter] motion-reduce:transition-none ease-[cubic-bezier(0.45,0,0.55,1)] duration-300">
|
||||
<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"
|
||||
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">
|
||||
<path d="m9 14l-4-4l4-4" />
|
||||
<path d="M5 10h11a4 4 0 1 1 0 8h-1" />
|
||||
</g>
|
||||
</svg>
|
||||
Return to home
|
||||
</a>
|
||||
</header>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<section class="flex justify-center w-full">
|
||||
<div class="w-full sm:w-4/5 p-2.5">
|
||||
{{#each Categories}}
|
||||
<div class="flex items-center category-header" id="{{this.ID}}_category">
|
||||
<div class="category-img" data-img-container>
|
||||
<img width="32" height="32" draggable="false" alt="{{this.Name}}" src="{{this.Icon}}" />
|
||||
</div>
|
||||
<h2 data-placeholder="Enter title...">{{~ this.Name ~}}</h2>
|
||||
<div class="pl-2" data-edit-actions>
|
||||
<div class="flex flex-row gap-2" data-primary-actions>
|
||||
<button aria-label="Edit category" onclick="editCategory(this)" class="action-button">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24"
|
||||
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">
|
||||
<path d="M7 7H6a2 2 0 0 0-2 2v9a2 2 0 0 0 2 2h9a2 2 0 0 0 2-2v-1" />
|
||||
<path d="M20.385 6.585a2.1 2.1 0 0 0-2.97-2.97L9 12v3h3zM16 5l3 3" />
|
||||
</g>
|
||||
</svg>
|
||||
</button>
|
||||
<button aria-label="Delete category" onclick="deleteCategory(this)"
|
||||
class="text-error action-button">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24"
|
||||
viewBox="0 0 24 24"><!-- Icon from Tabler Icons by Paweł Kuna - https://github.com/tabler/tabler-icons/blob/master/LICENSE -->
|
||||
<path fill="none" stroke="currentColor" 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" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<head>
|
||||
<title>Passport</title>
|
||||
<link rel="favicon" href="/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link rel="preload" as="font" type="font/woff2" crossorigin="anonymous"
|
||||
href="/assets/fonts/InstrumentSans-VariableFont_wdth,wght.woff2" />
|
||||
{{{embedFile "assets/styles/adminUi.css"}}}
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="blur-target">
|
||||
<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"
|
||||
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">
|
||||
<path d="m9 14l-4-4l4-4" />
|
||||
<path d="M5 10h11a4 4 0 1 1 0 8h-1" />
|
||||
</g>
|
||||
</svg>
|
||||
Return to home
|
||||
</a>
|
||||
</header>
|
||||
|
||||
{{> 'partials/category-grid' }}
|
||||
</div>
|
||||
|
||||
<input type="file" id="icon-upload" accept="image/*" style="display: none;" />
|
||||
<div id="modal-container" role="dialog" aria-modal="true" class="modal-bg">
|
||||
<div class="modal">
|
||||
{{> 'partials/modals/category-form' }}
|
||||
{{> 'partials/modals/link-form' }}
|
||||
{{> 'partials/modals/delete-link' }}
|
||||
{{> 'partials/modals/delete-category' }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- store a blank link card so that if we add a new link we can clone it to make the editing experience easier -->
|
||||
<div id="template-link-card" class="hidden">
|
||||
<div>
|
||||
<img width="64" height="64" draggable="false" />
|
||||
</div>
|
||||
<div>
|
||||
<h3></h3>
|
||||
<!-- add 2 to the height to account for the border -->
|
||||
<p></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="template-category" class="hidden">
|
||||
<div class="category-header">
|
||||
<div>
|
||||
<img width="32" height="32" draggable="false" />
|
||||
</div>
|
||||
<div class="link-grid">
|
||||
{{#each this.Links}}
|
||||
<div id="{{this.ID}}_link" class="link-card relative admin">
|
||||
<div class="relative" data-img-container>
|
||||
<img width="64" height="64" draggable="false" src="{{this.Icon}}" alt="{{this.Name}}" />
|
||||
</div>
|
||||
<div data-text-container>
|
||||
<h3 class="border border-transparent" data-placeholder="Enter title...">
|
||||
{{~ this.Name ~}}
|
||||
</h3>
|
||||
<!-- add 2 to the height to account for the border -->
|
||||
<p data-placeholder="Enter description...">
|
||||
{{~ this.Description ~}}
|
||||
</p>
|
||||
</div>
|
||||
<div class="absolute right-1 top-1" data-edit-actions>
|
||||
<div class="flex flex-row gap-2" data-primary-actions>
|
||||
<button aria-label="Edit link" onclick="editLink(this)" class="action-button">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24"
|
||||
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">
|
||||
<path d="M7 7H6a2 2 0 0 0-2 2v9a2 2 0 0 0 2 2h9a2 2 0 0 0 2-2v-1" />
|
||||
<path d="M20.385 6.585a2.1 2.1 0 0 0-2.97-2.97L9 12v3h3zM16 5l3 3" />
|
||||
</g>
|
||||
</svg>
|
||||
</button>
|
||||
<button aria-label="Delete link" onclick="deleteLink(this)"
|
||||
class="text-error action-button">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24"
|
||||
viewBox="0 0 24 24"><!-- Icon from Tabler Icons by Paweł Kuna - https://github.com/tabler/tabler-icons/blob/master/LICENSE -->
|
||||
<path fill="none" stroke="currentColor" 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" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{/each}}
|
||||
<div onclick="openModal('link', {{this.ID}})" class="new-link-card">
|
||||
<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"
|
||||
stroke-width="2" d="M12 5v14m-7-7h14" />
|
||||
</svg>
|
||||
<div>
|
||||
<h3>Add a link</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{/each}}
|
||||
<div class="flex items-center" id="add-category-button">
|
||||
<svg class="mr-2" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24">
|
||||
<h2></h2>
|
||||
</div>
|
||||
<div class="link-grid">
|
||||
<div class="new-link-card link-card admin">
|
||||
<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"
|
||||
stroke-width="2" d="M12 5v14m-7-7h14" />
|
||||
</svg>
|
||||
<h2 onclick="openModal('category')" class="text-subtle underline decoration-dashed cursor-pointer">
|
||||
Add a new category
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<input type="file" id="icon-upload" accept="image/*" style="display: none;" />
|
||||
<div id="modal-container" role="dialog" aria-modal="true"
|
||||
class="modal-bg fixed top-0 left-0 bottom-0 right-0 bg-black/45 justify-center items-center hidden">
|
||||
<div class="bg-overlay rounded-xl overflow-hidden w-full p-4 modal max-w-sm">
|
||||
{{> 'partials/modals/category-form' }}
|
||||
{{> 'partials/modals/link-form' }}
|
||||
{{> 'partials/modals/delete-link' }}
|
||||
{{> 'partials/modals/delete-category' }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- store a blank link card so that if we add a new link we can clone it to make the editing experience easier -->
|
||||
<div id="template-link-card" class="hidden">
|
||||
<div class="relative" data-img-container>
|
||||
<img width="64" height="64" draggable="false" />
|
||||
</div>
|
||||
<div class="flex-grow flex flex-col gap-y-px overflow-hidden" data-text-container>
|
||||
<h3 class="border border-transparent"></h3>
|
||||
<!-- add 2 to the height to account for the border -->
|
||||
<p class="min-h-[22px] border border-transparent"></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="template-category" class="hidden">
|
||||
<div class="flex items-center category-header">
|
||||
<div class="category-img" data-img-container>
|
||||
<img width="32" height="32" draggable="false" />
|
||||
</div>
|
||||
<h2></h2>
|
||||
</div>
|
||||
<div class="link-grid">
|
||||
<div class="new-link-card">
|
||||
<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" stroke-width="2"
|
||||
d="M12 5v14m-7-7h14" />
|
||||
</svg>
|
||||
<div>
|
||||
<h3>Add a link</h3>
|
||||
<div>
|
||||
<h3>Add a link</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="template-edit-actions" class="hidden" data-edit-actions>
|
||||
<div class="flex flex-row gap-2" data-primary-actions>
|
||||
<button class="action-button">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24"
|
||||
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">
|
||||
<path d="M7 7H6a2 2 0 0 0-2 2v9a2 2 0 0 0 2 2h9a2 2 0 0 0 2-2v-1" />
|
||||
<path d="M20.385 6.585a2.1 2.1 0 0 0-2.97-2.97L9 12v3h3zM16 5l3 3" />
|
||||
</g>
|
||||
</svg>
|
||||
</button>
|
||||
<button class="text-error action-button">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24"
|
||||
viewBox="0 0 24 24"><!-- Icon from Tabler Icons by Paweł Kuna - https://github.com/tabler/tabler-icons/blob/master/LICENSE -->
|
||||
<path fill="none" stroke="currentColor" 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" />
|
||||
</svg>
|
||||
</button>
|
||||
<div id="template-edit-actions" class="hidden">
|
||||
<div class="flex flex-row gap-2">
|
||||
<button class="action-button">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24"
|
||||
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">
|
||||
<path d="M7 7H6a2 2 0 0 0-2 2v9a2 2 0 0 0 2 2h9a2 2 0 0 0 2-2v-1" />
|
||||
<path d="M20.385 6.585a2.1 2.1 0 0 0-2.97-2.97L9 12v3h3zM16 5l3 3" />
|
||||
</g>
|
||||
</svg>
|
||||
</button>
|
||||
<button class="text-error action-button">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24"
|
||||
viewBox="0 0 24 24"><!-- Icon from Tabler Icons by Paweł Kuna - https://github.com/tabler/tabler-icons/blob/master/LICENSE -->
|
||||
<path fill="none" stroke="currentColor" 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" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="teleport-storage" class="absolute -top-full -left-full hidden">
|
||||
<!-- These are the elements that appear when the user enters edit mode, they allow for the cancelation/confirmation of the edit -->
|
||||
<div class="flex flex-row gap-2" data-confirm-actions id="confirm-actions">
|
||||
<button class="action-button text-success" onclick="confirmEdit()">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24"
|
||||
<div id="teleport-storage" class="absolute -top-full -left-full hidden">
|
||||
<!-- These are the elements that appear when the user enters edit mode, they allow for the cancelation/confirmation of the edit -->
|
||||
<div class="action-container" id="confirm-actions">
|
||||
<button class="action-button text-success" onclick="confirmEdit()">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24"
|
||||
viewBox="0 0 24 24"><!-- Icon from Tabler Icons by Paweł Kuna - https://github.com/tabler/tabler-icons/blob/master/LICENSE -->
|
||||
<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
|
||||
stroke-width="2" d="m5 12l5 5L20 7" />
|
||||
</svg>
|
||||
</button>
|
||||
<button class="action-button text-error" onclick="cancelEdit()">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24"
|
||||
viewBox="0 0 24 24"><!-- Icon from Tabler Icons by Paweł Kuna - https://github.com/tabler/tabler-icons/blob/master/LICENSE -->
|
||||
<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
|
||||
stroke-width="2" d="M3 12a9 9 0 1 0 18 0a9 9 0 1 0-18 0m15.364-6.364L5.636 18.364" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- This is the element that appears on top of the icon when the user is editing it that allows for changing the icon -->
|
||||
<button id="select-icon-button" onclick="selectIcon()" class="select-icon-button" draggable="false">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="28" height="28"
|
||||
viewBox="0 0 24 24"><!-- Icon from Tabler Icons by Paweł Kuna - https://github.com/tabler/tabler-icons/blob/master/LICENSE -->
|
||||
<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="m5 12l5 5L20 7" />
|
||||
</svg>
|
||||
</button>
|
||||
<button class="action-button text-error" onclick="cancelEdit()">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24"
|
||||
viewBox="0 0 24 24"><!-- Icon from Tabler Icons by Paweł Kuna - https://github.com/tabler/tabler-icons/blob/master/LICENSE -->
|
||||
<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M3 12a9 9 0 1 0 18 0a9 9 0 1 0-18 0m15.364-6.364L5.636 18.364" />
|
||||
d="M4 17v2a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-2M7 9l5-5l5 5m-5-5v12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- This is the element that appears on top of the icon when the user is editing it that allows for changing the icon -->
|
||||
<button id="select-icon-button" onclick="selectIcon()"
|
||||
class="flex absolute inset-0 bg-highlight/80 rounded-md text-base items-center justify-center"
|
||||
draggable="false">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="28" height="28"
|
||||
viewBox="0 0 24 24"><!-- Icon from Tabler Icons by Paweł Kuna - https://github.com/tabler/tabler-icons/blob/master/LICENSE -->
|
||||
<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M4 17v2a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-2M7 9l5-5l5 5m-5-5v12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
{{{embedFile "scripts/admin.js"}}}
|
||||
</body>
|
||||
|
||||
{{{embedFile "scripts/admin.js"}}}
|
||||
{{{devContent}}}
|
||||
|
||||
</html>
|
||||
@@ -1,20 +1,31 @@
|
||||
<main class="flex justify-center items-center h-screen relative bg-base">
|
||||
<div class="flex bg-surface rounded-xl overflow-hidden">
|
||||
<img src="/assets/leaves.webp" class="h-96 w-64 object-cover" />
|
||||
<div class="flex flex-col p-4 text-center">
|
||||
<h2 class="text-2xl">
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<title>Passport</title>
|
||||
<link rel="favicon" href="/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link rel="preload" as="font" type="font/woff2" crossorigin="anonymous"
|
||||
href="/assets/fonts/InstrumentSans-VariableFont_wdth,wght.woff2" />
|
||||
{{{embedFile "assets/styles/login.css"}}}
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<main class="login-container">
|
||||
<img src="/assets/leaves.webp" />
|
||||
<div>
|
||||
<h2>
|
||||
Login
|
||||
</h2>
|
||||
<form action="/admin/login" method="post" class="flex flex-col gap-y-3 my-2">
|
||||
<form action="/admin/login" method="post" class="login-form">
|
||||
<input type="text" name="username" placeholder="Username" />
|
||||
<input type="password" name="password" placeholder="Password" />
|
||||
<button class="px-4 py-2 rounded-md w-full bg-accent text-white border-0" type="submit">Login</button>
|
||||
</form>
|
||||
<span id="message"></span>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
</main>
|
||||
</body>
|
||||
<script>
|
||||
let message = document.getElementById("message");
|
||||
let form = document.querySelector("form");
|
||||
@@ -42,4 +53,7 @@
|
||||
|
||||
message.innerText = (await res.json()).message;
|
||||
});
|
||||
</script>
|
||||
</script>
|
||||
{{{devContent}}}
|
||||
|
||||
</html>
|
||||
@@ -1,100 +1,88 @@
|
||||
<main class="grid grid-rows-3 grid-cols-[1fr] justify-center items-center h-screen bg-base">
|
||||
<div class="flex h-full p-2.5 justify-between">
|
||||
<div>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<title>Passport</title>
|
||||
<link rel="favicon" href="/favicon.ico" />
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link rel="preload" as="font" type="font/woff2" crossorigin="anonymous"
|
||||
href="/assets/fonts/InstrumentSans-VariableFont_wdth,wght.woff2" />
|
||||
{{{embedFile "assets/styles/main.css"}}}
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<main class="hero">
|
||||
<div class="glance-container">
|
||||
{{#if WeatherData}}
|
||||
<div class="text-subtle flex items-center">
|
||||
<span class="mr-2 flex items-center">
|
||||
<div class="weather-data">
|
||||
<span>
|
||||
{{{WeatherData.Icon}}}
|
||||
</span>
|
||||
<div class="font-semibold">
|
||||
<p>{{WeatherData.Temp}}°C</p>
|
||||
<p>{{WeatherData.Desc}}</p>
|
||||
<p class="leading-condensed">{{WeatherData.Temp}}°C</p>
|
||||
<p class="leading-condensed">{{WeatherData.Desc}}</p>
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
<div>
|
||||
{{#if UptimeData}}
|
||||
<div class="text-subtle flex items-end flex-col">
|
||||
<div class="uptime-data">
|
||||
<svg width="0" height="0" style="display:none">
|
||||
<defs>
|
||||
<circle id="status-dot" cx="5" cy="5" r="5" />
|
||||
</defs>
|
||||
</svg>
|
||||
{{#each UptimeData}}
|
||||
<div class="flex items-center">
|
||||
<span class="mr-2 flex items-center">
|
||||
{{{this.FriendlyName}}}
|
||||
<div>
|
||||
<span>
|
||||
{{this.FriendlyName}}
|
||||
</span>
|
||||
<div class="relative my-auto size-2">
|
||||
<div class="relative my-auto size-2 flex-shrink-0 flex-grow-0">
|
||||
<svg class="absolute w-full h-full animate-ping" viewBox="0 0 10 10">
|
||||
<circle cx="5" cy="5" r="5"
|
||||
class="fill-current {{#if (eq this.Status 2)}} text-success {{else}} text-error {{/if}}">
|
||||
</circle>
|
||||
</svg>
|
||||
<svg class="relative w-full h-full" viewBox="0 0 10 10">
|
||||
<circle cx="5" cy="5" r="5"
|
||||
class="fill-current {{#if (eq this.Status 2)}} text-success {{else}} text-error {{/if}}">
|
||||
</circle>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="uptime-status">
|
||||
<svg viewBox="0 0 10 10">
|
||||
<use href="#status-dot"
|
||||
class="{{#if (eq this.Status 2)}}text-success{{else}}text-error{{/if}}">
|
||||
</use>
|
||||
</svg>
|
||||
<svg viewBox="0 0 10 10">
|
||||
<use href="#status-dot"
|
||||
class="{{#if (eq this.Status 2)}}text-success{{else}}text-error{{/if}}">
|
||||
</use>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
{{/each}}
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row-start-2 flex flex-col items-center w-full px-6">
|
||||
<div class="flex items-center pb-2.5">
|
||||
<svg class="mr-3 aspect-square w-[clamp(42px,10vw,60px)]" viewBox="0 0 100 100" fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="12.1483" y="24.7693" width="70" height="47" rx="12" transform="rotate(14.63 12.1483 24.7693)"
|
||||
fill="url(#paint0_linear_20_10)" />
|
||||
<path fill-rule="evenodd" clip-rule="evenodd"
|
||||
d="M52.7386 13.4812C46.8869 10.3698 39.6209 12.5913 36.5096 18.4429L32.7819 25.4537L68.4322 34.7599C77.5166 37.1313 82.9586 46.418 80.5872 55.5025L74.7779 77.7567C74.7752 77.7674 74.7724 77.778 74.7696 77.7886C79.7728 78.7022 85.0029 76.3441 87.518 71.6138L98.3159 51.306C101.427 45.4543 99.2058 38.1883 93.3542 35.0769L52.7386 13.4812Z"
|
||||
fill="url(#paint1_linear_20_10)" />
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_20_10" x1="12.359" y1="44.8681" x2="82.491" y2="48.2607"
|
||||
gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#F0389B" />
|
||||
<stop offset="1" stop-color="#EEE740" />
|
||||
</linearGradient>
|
||||
<linearGradient id="paint1_linear_20_10" x1="33.8935" y1="25.6926" x2="94.2236" y2="61.6131"
|
||||
gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#AA38F0" />
|
||||
<stop offset="1" stop-color="#EE406A" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
<h1>Passport</h1>
|
||||
<div class="primary-hero-container">
|
||||
<div>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 100 100">
|
||||
<rect width="70" height="47" x="12.1" y="24.8" fill="url(#a)" rx="12"
|
||||
transform="rotate(14.6 12.1 24.8)" />
|
||||
<path fill="url(#b)" fill-rule="evenodd"
|
||||
d="M52.7 13.5a12 12 0 0 0-16.2 5l-3.7 7 35.6 9.3a17 17 0 0 1 12.2 20.7l-5.8 22.3a12 12 0 0 0 12.7-6.2l10.8-20.3a12 12 0 0 0-5-16.2z"
|
||||
clip-rule="evenodd" />
|
||||
<defs>
|
||||
<linearGradient id="a" x1="12.4" x2="82.5" y1="44.9" y2="48.3" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#f0389b" />
|
||||
<stop offset="1" stop-color="#eee740" />
|
||||
</linearGradient>
|
||||
<linearGradient id="b" x1="33.9" x2="94.2" y1="25.7" y2="61.6" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#aa38f0" />
|
||||
<stop offset="1" stop-color="#ee406a" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
<h1>Passport</h1>
|
||||
</div>
|
||||
<form action="{{ SearchProviderURL }}" method="GET">
|
||||
<input name="{{ SearchParam }}" aria-label="Search bar" placeholder="Search..." />
|
||||
</form>
|
||||
</div>
|
||||
<form class="w-full max-w-3xl" action="{{ SearchProviderURL }}" method="GET">
|
||||
<input name="{{ SearchParam }}" aria-label="Search bar"
|
||||
class="w-full bg-surface border border-highlight-sm/70 rounded-full px-3 py-1 text-white h-7 focus-visible:outline-none placeholder:italic placeholder:text-highlight search"
|
||||
placeholder="Search..." />
|
||||
</form>
|
||||
</div>
|
||||
</main>
|
||||
<section class="flex justify-center w-full bg-surface">
|
||||
<div class="w-full sm:w-4/5 p-2.5">
|
||||
{{#each Categories}}
|
||||
<div class="flex items-center mt-2 first:mt-0">
|
||||
<img class="object-contain mr-2 select-none size-8" width="32" height="32" draggable="false"
|
||||
alt="{{this.Name}}" src="{{this.Icon}}" />
|
||||
<h2 class="capitalize break-all">{{this.Name}}</h2>
|
||||
</div>
|
||||
<div class="link-grid">
|
||||
{{#each this.Links}}
|
||||
<a href="{{this.URL}}" class="link-card" draggable="false" target="_blank" rel="noopener noreferrer">
|
||||
<div data-img-container>
|
||||
<img width="64" height="64" draggable="false" src="{{this.Icon}}" alt="{{this.Name}}" />
|
||||
</div>
|
||||
<div data-text-container>
|
||||
<h3>{{this.Name}}</h3>
|
||||
<p class="min-h-5">{{this.Description}}</p>
|
||||
</div>
|
||||
</a>
|
||||
{{else}}
|
||||
<p class="text-subtle">No links here, add one!</p>
|
||||
{{/each}}
|
||||
</div>
|
||||
{{/each}}
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
{{> 'partials/category-grid' }}
|
||||
</body>
|
||||
|
||||
{{{devContent}}}
|
||||
|
||||
</html>
|
||||
Reference in New Issue
Block a user