TypeScript checking is clean.

Tons and tons of broken functionality. Now the crawl begins.
This commit is contained in:
Eli Ribble 2026-03-22 02:55:17 +00:00
parent d9a98e9eb2
commit 03301518f0
No known key found for this signature in database
7 changed files with 176 additions and 73 deletions

View file

@ -13,6 +13,7 @@
"vue-router": "^5.0.4"
},
"devDependencies": {
"@types/bootstrap": "^5.2.10",
"esbuild": "^0.25.5",
"esbuild-plugin-vue3": "^0.5.1",
"esbuild-sass-plugin": "^3.7.0",

10
pnpm-lock.yaml generated
View file

@ -30,6 +30,9 @@ importers:
specifier: ^5.0.4
version: 5.0.4(@vue/compiler-sfc@3.5.30)(pinia@3.0.4(typescript@5.9.3)(vue@3.5.30(typescript@5.9.3)))(vue@3.5.30(typescript@5.9.3))
devDependencies:
'@types/bootstrap':
specifier: ^5.2.10
version: 5.2.10
esbuild:
specifier: ^0.25.5
version: 0.25.12
@ -365,6 +368,9 @@ packages:
'@popperjs/core@2.11.8':
resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==}
'@types/bootstrap@5.2.10':
resolution: {integrity: sha512-F2X+cd6551tep0MvVZ6nM8v7XgGN/twpdNDjqS1TUM7YFNEtQYWk+dKAnH+T1gr6QgCoGMPl487xw/9hXooa2g==}
'@types/geojson@7946.0.16':
resolution: {integrity: sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==}
@ -1143,6 +1149,10 @@ snapshots:
'@popperjs/core@2.11.8': {}
'@types/bootstrap@5.2.10':
dependencies:
'@popperjs/core': 2.11.8
'@types/geojson@7946.0.16': {}
'@types/supercluster@7.1.3':

View file

@ -0,0 +1,131 @@
<template>
<div class="col-md-3 border-end p-0 reports-list">
<div class="p-3 bg-light border-bottom">
<div class="input-group input-group-sm">
<span class="input-group-text"><i class="bi bi-search"></i></span>
<input
type="text"
class="form-control"
placeholder="Filter reports..."
v-model="searchFilter"
/>
</div>
<div class="mt-2 d-flex gap-2">
<button
class="btn btn-sm"
:class="
typeFilter === 'all' ? 'btn-primary' : 'btn-outline-secondary'
"
@click="typeFilter = 'all'"
>
All
</button>
<button
class="btn btn-sm"
:class="
typeFilter === 'nuisance' ? 'btn-danger' : 'btn-outline-secondary'
"
@click="typeFilter = 'nuisance'"
>
<i class="bi bi-mosquito"></i>Mosquito Nuisance
</button>
<button
class="btn btn-sm"
:class="typeFilter === 'water' ? 'btn-info' : 'btn-outline-secondary'"
@click="typeFilter = 'water'"
>
<i class="bi bi-droplet"></i> Water
</button>
</div>
</div>
<div class="list-group list-group-flush">
<div
v-for="comm in filteredCommunications"
:key="comm.id"
class="list-group-item report-card p-3"
:class="{
active: selectedCommunication && selectedCommunication.id === comm.id,
}"
@click="selectCommunication(comm)"
>
<!-- First row: icon, type badge, and time -->
<div class="d-flex justify-content-between align-items-center mb-2">
<div class="d-flex align-items-center">
<i
v-if="comm.type === 'publicreport.nuisance'"
class="bi bi-mosquito icon-nuisance fs-4 me-2"
>
</i>
<i
v-if="comm.type === 'publicreport.water'"
class="bi bi-droplet-fill icon-standing-water fs-4 me-2"
></i>
<span
class="badge"
:class="
comm.type === 'publicreport.nuisance' ? 'bg-danger' : 'bg-info'
"
>
{{
comm.type === "publicreport.nuisance"
? "Nuisance"
: "Standing Water"
}}
</span>
</div>
<small>
<TimeRelative :time="comm.created" />
</small>
</div>
<!-- Details section: full width -->
<div>
<div>
<i class="bi bi-geo-alt text-muted"></i>
<span class="fw-medium">{{
comm.public_report.address.postal_code
}}</span>
</div>
<small>{{ formatAddress(comm.public_report.address) }}</small>
<div
v-if="
comm.public_report.images && comm.public_report.images.length > 0
"
class="mt-1"
>
<small class="text-muted">
<i class="bi bi-camera"></i>
{{ comm.public_report.images.length }} photo(s)
</small>
</div>
</div>
</div>
</div>
<div
v-if="filteredCommunications.length === 0"
class="text-center text-muted p-4"
>
<i class="bi bi-inbox fs-1"></i>
<p class="mt-2">No reports found</p>
</div>
</div>
</template>
<script setup lang="ts">
interface Props {}
const props = withDefaults(defineProps<Props>(), {
onFilterChange,
});
// Computed properties
const filteredCommunications = computed(() => {
return communication.all.value.filter((c) => {
const matchesType =
typeFilter.value === "all" || c.type === typeFilter.value;
return matchesType && filterMatches(searchFilter.value, c);
});
});
</script>

8
ts/env.d.ts vendored Normal file
View file

@ -0,0 +1,8 @@
declare global {
interface Window {
bootstrap: typeof import("bootstrap");
SSEManager: typeof import("./sse-manager").SSEManager;
}
}
export {};

View file

@ -1,70 +0,0 @@
export function SetupSidebar() {
var popoverTriggerList = [].slice.call(
document.querySelectorAll('[data-bs-toggle="popover"]'),
);
var popoverList = popoverTriggerList.map(function (popoverTriggerEl) {
return new bootstrap.Popover(popoverTriggerEl);
});
console.log("Initialized ", popoverTriggerList.length, " popovers");
var tooltipTriggerList = [].slice.call(
document.querySelectorAll('[data-bs-toggle="tooltip"]'),
);
var tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) {
let t = new bootstrap.Tooltip(tooltipTriggerEl);
return t;
});
console.log("Initialized ", tooltipTriggerList.length, " tooltips");
restoreLocalStorage();
setTooltipsForSidebar();
SSEManager.subscribe("*", function (e) {
if (e.type != "heartbeat") {
updateUserState();
}
});
document.getElementById("sidebarToggle").addEventListener("click", () => {
const sidebar = document.getElementById("sidebar");
sidebar.classList.toggle("collapsed");
document.getElementById("content").classList.toggle("expanded");
setTooltipsForSidebar();
localStorage.setItem(
"sidebar.expanded",
(!sidebar.classList.contains("collapsed")).toString(),
);
});
updateUserState();
}
function restoreLocalStorage() {
const expanded = localStorage.getItem("sidebar.expanded");
if (expanded == "false") {
document.getElementById("sidebar").classList.add("collapsed");
document.getElementById("content").classList.add("expanded");
} else {
document.getElementById("sidebar").classList.remove("collapsed");
document.getElementById("content").classList.remove("expanded");
localStorage.setItem("sidebar.expanded", "true");
}
}
function setTooltipsForSidebar() {
const sidebarTooltips = document.querySelectorAll(
'#sidebar [data-bs-toggle="tooltip"]',
);
const isExpanded = document
.getElementById("content")
.classList.contains("expanded");
sidebarTooltips.forEach((t) => {
const tooltip = bootstrap.Tooltip.getOrCreateInstance(t);
if (isExpanded) {
tooltip.enable();
} else {
tooltip.disable();
}
});
}
async function updateUserState() {
const response = await fetch("/api/user/self");
const data = await response.json();
Object.keys(data).forEach((key) => {
store_user[key] = data[key];
});
}

5
ts/vue-shim.d.ts vendored Normal file
View file

@ -0,0 +1,5 @@
declare module "*.vue" {
import type { DefineComponent } from "vue";
const component: DefineComponent<object, object, any>;
export default component;
}

18
tsconfig.json Normal file
View file

@ -0,0 +1,18 @@
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"moduleResolution": "bundler",
"strict": true,
"jsx": "preserve",
"esModuleInterop": true,
"skipLibCheck": true,
"noEmit": true,
"paths": {
"@/*": ["./ts/*"]
}
},
"include": ["ts/**/*", "ts/**/*.vue", "ts/vue-shim.d.ts"],
"exclude": ["node_modules"]
}