From 3de69f02f24e7c2de103b30b7d83d4eb267556d4 Mon Sep 17 00:00:00 2001
From: Zoe <62722391+juls0730@users.noreply.github.com>
Date: Tue, 30 Sep 2025 01:06:52 -0500
Subject: [PATCH] Vastly overhaul admin UI
Admin UI now has the ability to edit links that exist. Deleting items is
more accessible and asks for a confirmation before deleting. Link and
Category names as well as link descriptions now have a length limit
(todo: make it configurable?). Small bug fixes related to image saving
are also included in this commit.
---
README.md | 3 +
go.mod | 2 +
go.sum | 6 +
src/main.go | 370 +++++++-
src/styles/{main.css => main.scss} | 36 +-
.../partials/modals/category-form.hbs | 17 +
.../partials/modals/delete-category.hbs | 13 +
src/templates/partials/modals/delete-link.hbs | 12 +
src/templates/partials/modals/link-form.hbs | 25 +
src/templates/views/admin/index.hbs | 809 +++++++++++++++---
src/templates/views/index.hbs | 6 +-
zqdgr.config.json | 4 +-
12 files changed, 1140 insertions(+), 163 deletions(-)
rename src/styles/{main.css => main.scss} (73%)
create mode 100644 src/templates/partials/modals/category-form.hbs
create mode 100644 src/templates/partials/modals/delete-category.hbs
create mode 100644 src/templates/partials/modals/delete-link.hbs
create mode 100644 src/templates/partials/modals/link-form.hbs
diff --git a/README.md b/README.md
index f82dcd1..128c949 100644
--- a/README.md
+++ b/README.md
@@ -63,6 +63,9 @@ You can then run the binary.
| `PASSPORT_SEARCH_PROVIDER` | The search provider to use for the search bar, without any query parameters | true |
| `PASSPORT_SEARCH_PROVIDER_QUERY_PARAM` | The query parameter to use for the search provider, e.g. `q` for most providers | false | q |
+> [!NOTE]
+> Currently passport only supports search using a GET request.
+
#### Weather configuration
The weather integration is optional, and will be enabled automatically if you provide an API key. The following only applies if you are using the OpenWeatherMap integration.
diff --git a/go.mod b/go.mod
index 4ca8dd8..6f62166 100644
--- a/go.mod
+++ b/go.mod
@@ -5,6 +5,8 @@ go 1.25.0
require (
github.com/HugoSmits86/nativewebp v1.2.0
github.com/caarlos0/env/v11 v11.3.1
+ github.com/disintegration/imaging v1.6.2
+ github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd
golang.org/x/image v0.24.0
modernc.org/sqlite v1.39.0
)
diff --git a/go.sum b/go.sum
index f7c827e..941ae82 100644
--- a/go.sum
+++ b/go.sum
@@ -7,6 +7,8 @@ github.com/caarlos0/env/v11 v11.3.1/go.mod h1:qupehSf/Y0TUTsxKywqRt/vJjN5nz6vaui
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c=
+github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM=
@@ -51,6 +53,8 @@ github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
+github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd h1:CmH9+J6ZSsIjUK3dcGsnCnO41eRBOnY12zwkn5qVwgc=
+github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd/go.mod h1:hPqNNc0+uJM6H+SuU8sEs5K5IQeKccPqeSjfgcKGgPk=
github.com/shamaton/msgpack/v2 v2.3.0 h1:eawIa7lQmwRv0V6rdmL/5Ev9KdJHk07eQH3ceJi3BUw=
github.com/shamaton/msgpack/v2 v2.3.0/go.mod h1:6khjYnkx73f7VQU7wjcFS9DFjs+59naVWJv1TB7qdOI=
github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE=
@@ -74,6 +78,7 @@ golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI=
golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8=
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o=
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8=
+golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.24.0 h1:AN7zRgVsbvmTfNyqIbbOraYL8mSwcKncEj8ofjgzcMQ=
golang.org/x/image v0.24.0/go.mod h1:4b/ITuLfqYq1hqZcjofwctIhi7sZh2WaCjvsBNjjya8=
golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ=
@@ -86,6 +91,7 @@ golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg=
diff --git a/src/main.go b/src/main.go
index 10de50f..ee6c008 100644
--- a/src/main.go
+++ b/src/main.go
@@ -1,4 +1,4 @@
-//go:generate tailwindcss -i styles/main.css -o assets/tailwind.css --minify
+//go:generate tailwindcss -i styles/main.scss -o assets/tailwind.css --minify
package main
@@ -27,6 +27,7 @@ import (
"github.com/HugoSmits86/nativewebp"
"github.com/caarlos0/env/v11"
+ "github.com/disintegration/imaging"
"github.com/gofiber/fiber/v3"
"github.com/gofiber/fiber/v3/middleware/helmet"
"github.com/gofiber/fiber/v3/middleware/static"
@@ -35,6 +36,8 @@ import (
"github.com/joho/godotenv"
"github.com/juls0730/passport/src/middleware"
"github.com/juls0730/passport/src/services"
+ "github.com/rwcarlsen/goexif/exif"
+ "github.com/rwcarlsen/goexif/tiff"
"golang.org/x/image/draw"
_ "modernc.org/sqlite"
)
@@ -44,23 +47,36 @@ var embeddedAssets embed.FS
var devContent = ``
@@ -282,8 +298,47 @@ func UploadFile(file *multipart.FileHeader, fileName, contentType string, c fibe
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 {
+ return "", fmt.Errorf("failed to seek to start of file: %v", err)
+ }
+
+ if off != 0 {
+ return "", fmt.Errorf("failed to seek to start of file: %v", err)
+ }
+
+ x, err := exif.Decode(srcFile)
+ // if there *is* exif, parse it
+ if err == nil {
+ tag, err := x.Get(exif.Orientation)
+ if err != nil {
+ return "", fmt.Errorf("failed to get orientation: %v", err)
+ }
+
+ if tag.Count == 1 && tag.Format() == tiff.IntVal {
+ orientation, err := tag.Int(0)
+ if err != nil {
+ return "", fmt.Errorf("failed to get orientation: %v", err)
+ }
+
+ slog.Debug("Orientation tag found", "orientation", orientation)
+
+ switch orientation {
+ case 3:
+ img = imaging.Rotate180(img)
+ case 6:
+ img = imaging.Rotate270(img)
+ case 8:
+ img = imaging.Rotate90(img)
+ }
+ }
+ }
+
+ img, err = CropToCenter(img, 96)
+ if err != nil {
+ return "", err
+ }
}
assetsDir := "public/uploads"
@@ -291,7 +346,21 @@ func UploadFile(file *multipart.FileHeader, fileName, contentType string, c fibe
iconPath := filepath.Join(assetsDir, fileName)
if contentType == "image/svg+xml" {
- if err = c.SaveFile(file, iconPath); err != nil {
+ // replace currentColor with a text color
+ outFile, err := os.Create(iconPath)
+ if err != nil {
+ return "", err
+ }
+ defer outFile.Close()
+
+ svgText, err := io.ReadAll(srcFile)
+ if err != nil {
+ return "", err
+ }
+
+ svgText = bytes.ReplaceAll(svgText, []byte("currentColor"), []byte(`oklch(87% 0.015 286)`))
+ _, err = outFile.Write(svgText)
+ if err != nil {
return "", err
}
} else {
@@ -301,16 +370,9 @@ func UploadFile(file *multipart.FileHeader, fileName, contentType string, c fibe
}
defer outFile.Close()
- // crop slightly larger than 64px to vastly increase the quality of the image, but not increase the file size
- // *too* much and so that we dont have a ton of extra file data that will never be seen by the user
- resizedImg, err := CropToCenter(img, 96)
- if err != nil {
- return "", err
- }
-
var buf bytes.Buffer
options := &nativewebp.Options{}
- if err := nativewebp.Encode(&buf, resizedImg, options); err != nil {
+ if err := nativewebp.Encode(&buf, img, options); err != nil {
return "", err
}
@@ -790,6 +852,14 @@ func main() {
})
}
+ req.Name = strings.TrimSpace(req.Name)
+
+ if len(req.Name) > 50 {
+ return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
+ "message": "Name is too long. Maximum length is 50 characters",
+ })
+ }
+
file, err := c.FormFile("icon")
if err != nil || file == nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
@@ -810,7 +880,7 @@ func main() {
})
}
- filename := fmt.Sprintf("%d_%s.svg", time.Now().Unix(), strings.ReplaceAll(req.Name, " ", "_"))
+ filename := fmt.Sprintf("%d_%s.svg", time.Now().Unix(), strings.ReplaceAll(req.Name[:min(10, len(req.Name))], " ", "_"))
iconPath, err := UploadFile(file, filename, contentType, c)
if err != nil {
@@ -819,8 +889,6 @@ func main() {
})
}
- UploadFile(file, iconPath, contentType, c)
-
category, err := app.CategoryManager.CreateCategory(Category{
Name: req.Name,
Icon: iconPath,
@@ -857,6 +925,23 @@ func main() {
})
}
+ req.Name = strings.TrimSpace(req.Name)
+ if req.Description != "" {
+ req.Description = strings.TrimSpace(req.Description)
+ }
+
+ if len(req.Name) > 50 {
+ return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
+ "message": "Name is too long. Maximum length is 50 characters",
+ })
+ }
+
+ if len(req.Description) > 150 {
+ return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
+ "message": "Description is too long. Maximum length is 150 characters",
+ })
+ }
+
categoryID, err := strconv.ParseInt(c.Params("id"), 10, 64)
if err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
@@ -890,7 +975,7 @@ func main() {
})
}
- filename := fmt.Sprintf("%d_%s.webp", time.Now().Unix(), strings.ReplaceAll(req.Name, " ", "_"))
+ filename := fmt.Sprintf("%d_%s.webp", time.Now().Unix(), strings.ReplaceAll(req.Name[:min(10, len(req.Name))], " ", "_"))
iconPath, err := UploadFile(file, filename, contentType, c)
if err != nil {
@@ -900,8 +985,6 @@ func main() {
})
}
- UploadFile(file, iconPath, contentType, c)
-
link, err := app.CategoryManager.CreateLink(app.CategoryManager.db, Link{
CategoryID: categoryID,
Name: req.Name,
@@ -922,6 +1005,245 @@ func main() {
})
})
+ api.Patch("/category/:id", func(c fiber.Ctx) error {
+ var req struct {
+ Name string `form:"name"`
+ }
+
+ if c.Params("id") == "" {
+ return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
+ "message": "ID is required",
+ })
+ }
+
+ id, err := strconv.ParseInt(c.Params("id"), 10, 64)
+ if err != nil {
+ return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
+ "message": fmt.Sprintf("Failed to parse category ID: %v", err),
+ })
+ }
+
+ if err := c.Bind().Form(&req); err != nil {
+ return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
+ "message": "Failed to parse request",
+ })
+ }
+
+ if req.Name != "" {
+ if len(req.Name) > 50 {
+ return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
+ "message": "Name is too long. Maximum length is 50 characters",
+ })
+ }
+ }
+
+ category := app.CategoryManager.GetCategory(id)
+ if category == nil {
+ return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
+ "message": "Category not found",
+ })
+ }
+
+ tx, err := app.db.Begin()
+ if err != nil {
+ return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
+ "message": "Failed to start transaction",
+ })
+ }
+ defer tx.Rollback()
+
+ file, err := c.FormFile("icon")
+ if err == nil {
+ if file.Size > 5*1024*1024 {
+ return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
+ "message": "File size too large. Maximum size is 5MB",
+ })
+ }
+
+ contentType := file.Header.Get("Content-Type")
+ if contentType != "image/svg+xml" {
+ return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
+ "message": "Only svg files are allowed",
+ })
+ }
+
+ oldIconPath := category.Icon
+
+ filename := fmt.Sprintf("%d_%s.svg", time.Now().Unix(), strings.ReplaceAll(req.Name[:min(10, len(req.Name))], " ", "_"))
+
+ iconPath, err := UploadFile(file, filename, contentType, c)
+ if err != nil {
+ slog.Error("Failed to upload file", "error", err)
+ return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
+ "message": "Failed to upload file, please try again!",
+ })
+ }
+
+ _, err = tx.Exec("UPDATE categories SET icon = ? WHERE id = ?", iconPath, id)
+ if err != nil {
+ return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
+ "message": "Failed to update category",
+ })
+ }
+
+ err = os.Remove(filepath.Join("public/", oldIconPath))
+ if err != nil {
+ slog.Error("Failed to delete icon", "error", err)
+ }
+ }
+
+ if req.Name != "" {
+ _, err = tx.Exec("UPDATE categories SET name = ? WHERE id = ?", req.Name, category.ID)
+ if err != nil {
+ return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
+ "message": "Failed to update category",
+ })
+ }
+ }
+
+ err = tx.Commit()
+ if err != nil {
+ return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
+ "message": "Failed to commit transaction",
+ })
+ }
+
+ return c.Status(fiber.StatusOK).JSON(fiber.Map{
+ "message": "Category updated successfully",
+ })
+ })
+
+ api.Patch("/category/:categoryID/link/:linkID", func(c fiber.Ctx) error {
+ var req struct {
+ Name string `form:"name"`
+ Description string `form:"description"`
+ Icon string `form:"icon"`
+ }
+ if err := c.Bind().Form(&req); err != nil {
+ return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
+ "message": "Failed to parse request",
+ })
+ }
+
+ if len(req.Name) > 50 {
+ return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
+ "message": "Name is too long. Maximum length is 50 characters",
+ })
+ }
+
+ if len(req.Description) > 150 {
+ return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
+ "message": "Description is too long. Maximum length is 150 characters",
+ })
+ }
+
+ linkID, err := strconv.ParseInt(c.Params("linkID"), 10, 64)
+ if err != nil {
+ return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
+ "message": fmt.Sprintf("Failed to parse link ID: %v", err),
+ })
+ }
+
+ categoryID, err := strconv.ParseInt(c.Params("categoryID"), 10, 64)
+ if err != nil {
+ return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
+ "message": fmt.Sprintf("Failed to parse category ID: %v", err),
+ })
+ }
+
+ if app.CategoryManager.GetCategory(categoryID) == nil {
+ return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
+ "message": "Category not found",
+ })
+ }
+
+ link := app.CategoryManager.GetLink(linkID)
+ if link == nil {
+ return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
+ "message": "Link not found",
+ })
+ }
+
+ tx, err := app.db.Begin()
+ if err != nil {
+ return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
+ "message": "Failed to start transaction",
+ })
+ }
+ defer tx.Rollback()
+
+ file, err := c.FormFile("icon")
+ if err == nil {
+ if file.Size > 5*1024*1024 {
+ return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
+ "message": "File size too large. Maximum size is 5MB",
+ })
+ }
+
+ contentType := file.Header.Get("Content-Type")
+ if !strings.HasPrefix(contentType, "image/") {
+ return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
+ "message": "Only image files are allowed",
+ })
+ }
+
+ oldIconPath := link.Icon
+
+ filename := fmt.Sprintf("%d_%s.webp", time.Now().Unix(), strings.ReplaceAll(req.Name[:min(10, len(req.Name))], " ", "_"))
+
+ iconPath, err := UploadFile(file, filename, contentType, c)
+ if err != nil {
+ slog.Error("Failed to upload file", "error", err)
+ return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
+ "message": "Failed to upload file, please try again!",
+ })
+ }
+
+ _, err = tx.Exec("UPDATE links SET icon = ? WHERE id = ?", iconPath, linkID)
+ if err != nil {
+ return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
+ "message": "Failed to update link",
+ })
+ }
+
+ err = os.Remove(filepath.Join("public/", oldIconPath))
+ if err != nil {
+ slog.Error("Failed to delete icon", "error", err)
+ }
+ }
+
+ if req.Name != "" {
+ _, err = tx.Exec("UPDATE links SET name = ? WHERE id = ?", req.Name, linkID)
+ if err != nil {
+ return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
+ "message": "Failed to update link",
+ })
+ }
+ }
+
+ if req.Description != "" {
+ _, err = tx.Exec("UPDATE links SET description = ? WHERE id = ?", req.Description, linkID)
+ if err != nil {
+ return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
+ "message": "Failed to update link",
+ })
+ }
+ }
+
+ err = tx.Commit()
+ if err != nil {
+ return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
+ "message": "Failed to commit transaction",
+ })
+ }
+
+ slog.Info("Link updated successfully", "id", linkID, "name", req.Name)
+
+ return c.Status(fiber.StatusOK).JSON(fiber.Map{
+ "message": "Link updated successfully",
+ })
+ })
+
api.Delete("/category/:categoryID/link/:linkID", func(c fiber.Ctx) error {
linkID, err := strconv.ParseInt(c.Params("linkID"), 10, 64)
if err != nil {
diff --git a/src/styles/main.css b/src/styles/main.scss
similarity index 73%
rename from src/styles/main.css
rename to src/styles/main.scss
index 4b65f2a..1828323 100644
--- a/src/styles/main.css
+++ b/src/styles/main.scss
@@ -4,11 +4,11 @@
@theme {
--color-accent: oklch(57.93% 0.258 294.12);
--color-success: oklch(70.19% 0.158 160.44);
- --color-error: oklch(63.43% 0.251 28.48);
+ --color-error: oklch(53% 0.251 28.48);
--color-base: oklch(11% .007 285);
- --color-surface: oklch(19% 0.007 314.66);
- --color-overlay: oklch(26% 0.008 314.66);
+ --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);
@@ -61,7 +61,7 @@ button {
}
input:not(.search) {
- @apply px-4 py-2 rounded-md w-full bg-surface border border-highlight-sm/70 placeholder:text-highlight text-text focus-visible:outline-none transition-colors duration-300 ease-out overflow-hidden;
+ @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;
@@ -85,14 +85,16 @@ input:not(.search) {
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);
- &: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);
- }
+ &: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);
+ &:active {
+ box-shadow: 0 2px 4px -2px rgb(0 0 0 / 0.1);
+ transform: translateY(2px);
+ }
}
}
@@ -110,18 +112,24 @@ input:not(.search) {
}
}
-.link-card img {
+/* Div that holds the image */
+.link-card div:has(img):first-child {
+ flex-shrink: 0;
margin-right: 0.5rem;
+}
+
+.link-card div:first-child img {
user-select: none;
border-radius: 0.375rem;
aspect-ratio: 1/1;
object-fit: cover;
}
-.link-card div {
+/* Div that holds the text */
+.link-card div:nth-child(2) {
word-break: break-all;
}
-.link-card div p {
+.link-card div:nth-child(2) p {
color: var(--color-subtle);
}
\ No newline at end of file
diff --git a/src/templates/partials/modals/category-form.hbs b/src/templates/partials/modals/category-form.hbs
new file mode 100644
index 0000000..746f3dd
--- /dev/null
+++ b/src/templates/partials/modals/category-form.hbs
@@ -0,0 +1,17 @@
+
+
Create A category
+
+
+
\ No newline at end of file
diff --git a/src/templates/partials/modals/delete-category.hbs b/src/templates/partials/modals/delete-category.hbs
new file mode 100644
index 0000000..9f9257c
--- /dev/null
+++ b/src/templates/partials/modals/delete-category.hbs
@@ -0,0 +1,13 @@
+
+
Are you sure you want to delete this category?
+
You are about to delete the category . This action cannot be
+ undone.
+ All links associated with this category will also be deleted. Are you sure you want to continue?
+
+
+
+
+
\ No newline at end of file
diff --git a/src/templates/partials/modals/delete-link.hbs b/src/templates/partials/modals/delete-link.hbs
new file mode 100644
index 0000000..e9b6897
--- /dev/null
+++ b/src/templates/partials/modals/delete-link.hbs
@@ -0,0 +1,12 @@
+
+
Are you sure you want to delete this link?
+
You are about to delete the link . This action cannot be undone. Are
+ you sure you
+ want to continue?
+
+
+
+
+
\ No newline at end of file
diff --git a/src/templates/partials/modals/link-form.hbs b/src/templates/partials/modals/link-form.hbs
new file mode 100644
index 0000000..0bdf923
--- /dev/null
+++ b/src/templates/partials/modals/link-form.hbs
@@ -0,0 +1,25 @@
+
+
Add A link
+
+
+
\ No newline at end of file
diff --git a/src/templates/views/admin/index.hbs b/src/templates/views/admin/index.hbs
index 4ed6080..c4b3e74 100644
--- a/src/templates/views/admin/index.hbs
+++ b/src/templates/views/admin/index.hbs
@@ -18,32 +18,129 @@