TypeScript checking is clean.
Tons and tons of broken functionality. Now the crawl begins.
This commit is contained in:
parent
d9a98e9eb2
commit
03301518f0
7 changed files with 176 additions and 73 deletions
|
|
@ -13,6 +13,7 @@
|
||||||
"vue-router": "^5.0.4"
|
"vue-router": "^5.0.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/bootstrap": "^5.2.10",
|
||||||
"esbuild": "^0.25.5",
|
"esbuild": "^0.25.5",
|
||||||
"esbuild-plugin-vue3": "^0.5.1",
|
"esbuild-plugin-vue3": "^0.5.1",
|
||||||
"esbuild-sass-plugin": "^3.7.0",
|
"esbuild-sass-plugin": "^3.7.0",
|
||||||
|
|
@ -22,10 +23,10 @@
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "node build.js",
|
"build": "node build.js",
|
||||||
"build:prod": "node build.js --minify",
|
"build:prod": "node build.js --minify",
|
||||||
"dev": "pnpm typecheck:watch & pnpm watch",
|
"dev": "pnpm typecheck:watch & pnpm watch",
|
||||||
"generate-icons": "node generate-icons.js",
|
"generate-icons": "node generate-icons.js",
|
||||||
"typecheck": "tsc --noEmit",
|
"typecheck": "tsc --noEmit",
|
||||||
"typecheck:watch": "tsc --noEmit --watch --preserveWatchOutput",
|
"typecheck:watch": "tsc --noEmit --watch --preserveWatchOutput",
|
||||||
"watch": "node build.js --watch"
|
"watch": "node build.js --watch"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
10
pnpm-lock.yaml
generated
10
pnpm-lock.yaml
generated
|
|
@ -30,6 +30,9 @@ importers:
|
||||||
specifier: ^5.0.4
|
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))
|
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:
|
devDependencies:
|
||||||
|
'@types/bootstrap':
|
||||||
|
specifier: ^5.2.10
|
||||||
|
version: 5.2.10
|
||||||
esbuild:
|
esbuild:
|
||||||
specifier: ^0.25.5
|
specifier: ^0.25.5
|
||||||
version: 0.25.12
|
version: 0.25.12
|
||||||
|
|
@ -365,6 +368,9 @@ packages:
|
||||||
'@popperjs/core@2.11.8':
|
'@popperjs/core@2.11.8':
|
||||||
resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==}
|
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':
|
'@types/geojson@7946.0.16':
|
||||||
resolution: {integrity: sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==}
|
resolution: {integrity: sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==}
|
||||||
|
|
||||||
|
|
@ -1143,6 +1149,10 @@ snapshots:
|
||||||
|
|
||||||
'@popperjs/core@2.11.8': {}
|
'@popperjs/core@2.11.8': {}
|
||||||
|
|
||||||
|
'@types/bootstrap@5.2.10':
|
||||||
|
dependencies:
|
||||||
|
'@popperjs/core': 2.11.8
|
||||||
|
|
||||||
'@types/geojson@7946.0.16': {}
|
'@types/geojson@7946.0.16': {}
|
||||||
|
|
||||||
'@types/supercluster@7.1.3':
|
'@types/supercluster@7.1.3':
|
||||||
|
|
|
||||||
131
ts/components/CommunicationColumnList.vue
Normal file
131
ts/components/CommunicationColumnList.vue
Normal 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
8
ts/env.d.ts
vendored
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
bootstrap: typeof import("bootstrap");
|
||||||
|
SSEManager: typeof import("./sse-manager").SSEManager;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export {};
|
||||||
|
|
@ -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
5
ts/vue-shim.d.ts
vendored
Normal 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
18
tsconfig.json
Normal 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"]
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue