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"
|
||||
},
|
||||
"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
10
pnpm-lock.yaml
generated
|
|
@ -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':
|
||||
|
|
|
|||
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