From ca79e57141a03cbabb1fe7569a320c0e4af111ab Mon Sep 17 00:00:00 2001 From: Bobby <30593201+luciferreeves@users.noreply.github.com> Date: Tue, 10 Mar 2026 22:49:41 +0530 Subject: feat(districts): add district management functionality - Introduced new types and interfaces for districts, sites, and citizen summaries. - Implemented district image and icon utilities. - Created controllers for listing districts, submitting sites, and managing site requests. - Added enums for district slugs and site statuses. - Developed services for district site management, including submission, review, and editing. - Implemented thumbnail generation for district sites. - Established repository methods for district site CRUD operations. - Created router paths for district-related endpoints. - Added messages for error handling in district operations. - Enhanced models to support district site features. - Implemented pagination and search functionalities for district site listings. --- garden/public/images/background.webp | Bin 940 -> 0 bytes garden/public/images/backgrounds/background.webp | Bin 0 -> 940 bytes garden/public/images/districts/arcadia.png | Bin 0 -> 799 bytes garden/public/images/districts/arles.png | Bin 0 -> 649 bytes garden/public/images/districts/hollywood.png | Bin 0 -> 742 bytes garden/public/images/districts/oxford.png | Bin 0 -> 766 bytes garden/public/images/districts/petsburg.png | Bin 0 -> 1031 bytes garden/public/images/districts/purgatory.png | Bin 0 -> 1832 bytes garden/public/images/districts/silicon.png | Bin 0 -> 806 bytes garden/public/images/districts/silver.png | Bin 0 -> 720 bytes garden/public/images/districts/stratford.png | Bin 0 -> 662 bytes garden/public/images/districts/tokyo.png | Bin 0 -> 1413 bytes garden/src/index.css | 3 +- garden/src/pages/council/districts.tsx | 350 +++++++++++++++++++ garden/src/pages/districts/district.tsx | 145 ++++++++ garden/src/pages/districts/index.tsx | 57 ++++ garden/src/pages/districts/submit.tsx | 171 ++++++++++ garden/src/routes.ts | 4 + garden/src/styles/districts.css | 416 +++++++++++++++++++++++ garden/src/styles/layout.css | 5 +- garden/src/types/admin.ts | 3 + garden/src/types/district.ts | 40 +++ garden/src/utils/districts.ts | 30 ++ shrine/controllers/district.go | 98 ++++++ shrine/database/migrate.go | 2 + shrine/enums/district.go | 25 ++ shrine/go.mod | 11 +- shrine/go.sum | 20 ++ shrine/jobs/scheduler.go | 26 ++ shrine/messages/district.go | 21 ++ shrine/models/district.go | 99 ++++++ shrine/repositories/district.go | 133 ++++++++ shrine/router/council.go | 6 + shrine/router/district.go | 16 + shrine/services/district.go | 249 ++++++++++++++ shrine/services/thumbnail.go | 151 ++++++++ shrine/shrine/main.go | 3 + shrine/types/district/request.go | 20 ++ shrine/types/district/response.go | 48 +++ shrine/utils/districts/registry.go | 44 +++ 40 files changed, 2191 insertions(+), 5 deletions(-) delete mode 100644 garden/public/images/background.webp create mode 100644 garden/public/images/backgrounds/background.webp create mode 100644 garden/public/images/districts/arcadia.png create mode 100644 garden/public/images/districts/arles.png create mode 100644 garden/public/images/districts/hollywood.png create mode 100644 garden/public/images/districts/oxford.png create mode 100644 garden/public/images/districts/petsburg.png create mode 100644 garden/public/images/districts/purgatory.png create mode 100644 garden/public/images/districts/silicon.png create mode 100644 garden/public/images/districts/silver.png create mode 100644 garden/public/images/districts/stratford.png create mode 100644 garden/public/images/districts/tokyo.png create mode 100644 garden/src/pages/council/districts.tsx create mode 100644 garden/src/pages/districts/district.tsx create mode 100644 garden/src/pages/districts/index.tsx create mode 100644 garden/src/pages/districts/submit.tsx create mode 100644 garden/src/styles/districts.css create mode 100644 garden/src/types/district.ts create mode 100644 garden/src/utils/districts.ts create mode 100644 shrine/controllers/district.go create mode 100644 shrine/enums/district.go create mode 100644 shrine/jobs/scheduler.go create mode 100644 shrine/messages/district.go create mode 100644 shrine/models/district.go create mode 100644 shrine/repositories/district.go create mode 100644 shrine/router/district.go create mode 100644 shrine/services/district.go create mode 100644 shrine/services/thumbnail.go create mode 100644 shrine/types/district/request.go create mode 100644 shrine/types/district/response.go create mode 100644 shrine/utils/districts/registry.go diff --git a/garden/public/images/background.webp b/garden/public/images/background.webp deleted file mode 100644 index 9e37b7f..0000000 Binary files a/garden/public/images/background.webp and /dev/null differ diff --git a/garden/public/images/backgrounds/background.webp b/garden/public/images/backgrounds/background.webp new file mode 100644 index 0000000..9e37b7f Binary files /dev/null and b/garden/public/images/backgrounds/background.webp differ diff --git a/garden/public/images/districts/arcadia.png b/garden/public/images/districts/arcadia.png new file mode 100644 index 0000000..821955e Binary files /dev/null and b/garden/public/images/districts/arcadia.png differ diff --git a/garden/public/images/districts/arles.png b/garden/public/images/districts/arles.png new file mode 100644 index 0000000..43322e2 Binary files /dev/null and b/garden/public/images/districts/arles.png differ diff --git a/garden/public/images/districts/hollywood.png b/garden/public/images/districts/hollywood.png new file mode 100644 index 0000000..56e52a2 Binary files /dev/null and b/garden/public/images/districts/hollywood.png differ diff --git a/garden/public/images/districts/oxford.png b/garden/public/images/districts/oxford.png new file mode 100644 index 0000000..d1016d2 Binary files /dev/null and b/garden/public/images/districts/oxford.png differ diff --git a/garden/public/images/districts/petsburg.png b/garden/public/images/districts/petsburg.png new file mode 100644 index 0000000..a8bb491 Binary files /dev/null and b/garden/public/images/districts/petsburg.png differ diff --git a/garden/public/images/districts/purgatory.png b/garden/public/images/districts/purgatory.png new file mode 100644 index 0000000..85aac4f Binary files /dev/null and b/garden/public/images/districts/purgatory.png differ diff --git a/garden/public/images/districts/silicon.png b/garden/public/images/districts/silicon.png new file mode 100644 index 0000000..bf630f9 Binary files /dev/null and b/garden/public/images/districts/silicon.png differ diff --git a/garden/public/images/districts/silver.png b/garden/public/images/districts/silver.png new file mode 100644 index 0000000..250613a Binary files /dev/null and b/garden/public/images/districts/silver.png differ diff --git a/garden/public/images/districts/stratford.png b/garden/public/images/districts/stratford.png new file mode 100644 index 0000000..ba18bc5 Binary files /dev/null and b/garden/public/images/districts/stratford.png differ diff --git a/garden/public/images/districts/tokyo.png b/garden/public/images/districts/tokyo.png new file mode 100644 index 0000000..56d1843 Binary files /dev/null and b/garden/public/images/districts/tokyo.png differ diff --git a/garden/src/index.css b/garden/src/index.css index fe53bac..d1b4530 100644 --- a/garden/src/index.css +++ b/garden/src/index.css @@ -3,4 +3,5 @@ @import "./styles/council.css"; @import "./styles/modal.css"; @import "./styles/editor.css"; -@import "./styles/datepicker.css"; \ No newline at end of file +@import "./styles/datepicker.css"; +@import "./styles/districts.css"; \ No newline at end of file diff --git a/garden/src/pages/council/districts.tsx b/garden/src/pages/council/districts.tsx new file mode 100644 index 0000000..eac6329 --- /dev/null +++ b/garden/src/pages/council/districts.tsx @@ -0,0 +1,350 @@ +import { createSignal, onMount, Show, For } from "solid-js"; +import { useSearchParams } from "@solidjs/router"; +import { api } from "../../api"; +import { auth } from "../../store/auth"; +import { extractError } from "../../utils/api"; +import { useClickOutside } from "../../utils/clickOutside"; +import type { SiteRequest, AdminSite } from "../../types/district"; +import type { PaginatedResponse } from "../../types/admin"; +import Pagination from "../../components/Pagination"; +import StaffGuard from "../../components/StaffGuard"; +import Modal from "../../components/Modal"; + +const STATUS_LABELS: Record = { + "": "Pending & Hold", + pending: "Pending", + hold: "On Hold", + approved: "Approved", + denied: "Denied", +}; + +export default function CouncilDistricts() { + const [searchParams, setSearchParams] = useSearchParams(); + const [tab, setTab] = createSignal<"requests" | "sites">("requests"); + + const [requests, setRequests] = createSignal([]); + const [reqTotal, setReqTotal] = createSignal(0); + const [reqPage, setReqPage] = createSignal(1); + const [reqTotalPages, setReqTotalPages] = createSignal(0); + const [reqLoading, setReqLoading] = createSignal(false); + const [reqStatus, setReqStatus] = createSignal(""); + const [statusOpen, setStatusOpen] = createSignal(false); + let statusRef: HTMLDivElement | undefined; + useClickOutside(() => statusRef, setStatusOpen); + + const [adminSites, setAdminSites] = createSignal([]); + const [siteTotal, setSiteTotal] = createSignal(0); + const [sitePage, setSitePage] = createSignal(1); + const [siteTotalPages, setSiteTotalPages] = createSignal(0); + const [siteLoading, setSiteLoading] = createSignal(false); + const [siteSearch, setSiteSearch] = createSignal(""); + + const [reviewTarget, setReviewTarget] = createSignal(null); + const [reviewAction, setReviewAction] = createSignal(""); + const [reviewError, setReviewError] = createSignal(""); + const [reviewing, setReviewing] = createSignal(false); + + const [editTarget, setEditTarget] = createSignal(null); + const [editTitle, setEditTitle] = createSignal(""); + const [editDescription, setEditDescription] = createSignal(""); + const [editError, setEditError] = createSignal(""); + const [editing, setEditing] = createSignal(false); + + onMount(() => { + const initialTab = searchParams.tab as string; + if (initialTab === "sites") setTab("sites"); + loadRequests(); + }); + + async function loadRequests(pageNumber = 1) { + setReqLoading(true); + const queryParams = new URLSearchParams({ page: String(pageNumber), per_page: "20" }); + if (reqStatus()) queryParams.set("status", reqStatus()); + + const response = await api>(`/council/districts/requests?${queryParams}`, { + token: auth.token(), + }); + + if (response.ok) { + setRequests(response.data.items); + setReqTotal(response.data.total); + setReqPage(response.data.page); + setReqTotalPages(response.data.total_pages); + } + setReqLoading(false); + } + + async function loadSites(pageNumber = 1) { + setSiteLoading(true); + const queryParams = new URLSearchParams({ page: String(pageNumber), per_page: "20" }); + if (siteSearch()) queryParams.set("search", siteSearch()); + + const response = await api>(`/council/districts/sites?${queryParams}`, { + token: auth.token(), + }); + + if (response.ok) { + setAdminSites(response.data.items); + setSiteTotal(response.data.total); + setSitePage(response.data.page); + setSiteTotalPages(response.data.total_pages); + } + setSiteLoading(false); + } + + function switchTab(selectedTab: "requests" | "sites") { + setTab(selectedTab); + setSearchParams({ tab: selectedTab }); + if (selectedTab === "requests") loadRequests(); + else loadSites(); + } + + function pickStatus(value: string) { + setReqStatus(value); + setStatusOpen(false); + loadRequests(1); + } + + function openReview(site: SiteRequest, action: string) { + setReviewTarget(site); + setReviewAction(action); + setReviewError(""); + } + + async function submitReview() { + const target = reviewTarget(); + if (!target) return; + setReviewing(true); + setReviewError(""); + + const response = await api(`/council/districts/sites/${target.ref}/review`, { + method: "POST", + token: auth.token(), + body: { status: reviewAction() }, + }); + + if (response.ok) { + setReviewTarget(null); + loadRequests(reqPage()); + } else { + setReviewError(extractError(response.data)); + } + setReviewing(false); + } + + function openEdit(site: AdminSite) { + setEditTarget(site); + setEditTitle(site.title); + setEditDescription(site.description); + setEditError(""); + } + + async function submitEdit() { + const target = editTarget(); + if (!target) return; + setEditing(true); + setEditError(""); + + const response = await api(`/council/districts/sites/${target.ref}`, { + method: "PATCH", + token: auth.token(), + body: { + title: editTitle(), + description: editDescription(), + }, + }); + + if (response.ok) { + setEditTarget(null); + loadSites(sitePage()); + } else { + setEditError(extractError(response.data)); + } + setEditing(false); + } + + function statusLabel(status: string) { + return STATUS_LABELS[status] || status; + } + + return ( + +
+

Districts

+ +
+ + +
+ + +
+
+ + +
+ + {([key, label]: [string, string]) => ( + + )} + +
+
+
+ + + +
+ +
+
+ Title + District + Submitter + Status + Actions +
+ Loading...
+ }> + No requests found. + }> + + {(request) => ( +
+ + {request.title} + + {request.district} + {request.submitter.display_name} + {statusLabel(request.status)} + + + + + + + + + +
+ )} +
+
+
+ + + loadRequests(pageNumber)} /> + + + + + +
+
+ Title + District + URL + Actions +
+ Loading...
+ }> + No approved sites found. + }> + + {(site) => ( +
+ {site.title} + {site.district} + + {site.url} + + + + +
+ )} +
+
+
+ + + loadSites(pageNumber)} /> + + + + setReviewTarget(null)}> +

+ Are you sure you want to {reviewAction() === "approved" ? "approve" : reviewAction() === "denied" ? "deny" : "put on hold"}{" "} + {reviewTarget()!.title}? +

+ +
{reviewError()}
+
+ +
+
+ + + setEditTarget(null)}> + +
{editError()}
+
+
+ + setEditTitle(e.currentTarget.value)} maxLength={200} /> +
+
+ +