|
|
|
<template>
|
|
|
|
<QuickNav selected="Discover" />
|
|
|
|
<TopMessage />
|
|
|
|
|
|
|
|
<!-- CONTENT -->
|
|
|
|
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
|
|
|
|
<!-- Heading -->
|
|
|
|
<h1 id="ViewHeading" class="text-4xl text-center font-light">
|
|
|
|
Discover Projects
|
|
|
|
</h1>
|
|
|
|
|
|
|
|
<OnboardingDialog ref="onboardingDialog" />
|
|
|
|
|
|
|
|
<!-- Quick Search -->
|
|
|
|
<div
|
|
|
|
id="QuickSearch"
|
|
|
|
class="mt-8 mb-4 flex"
|
|
|
|
v-on:keyup.enter="searchSelected()"
|
|
|
|
:style="{ visibility: isSearchVisible ? 'visible' : 'hidden' }"
|
|
|
|
>
|
|
|
|
<input
|
|
|
|
type="text"
|
|
|
|
v-model="searchTerms"
|
|
|
|
placeholder="Search…"
|
|
|
|
class="block w-full rounded-l border border-r-0 border-slate-400 px-3 py-2"
|
|
|
|
/>
|
|
|
|
<button
|
|
|
|
@click="searchSelected()"
|
|
|
|
class="px-4 rounded-r bg-slate-200 border border-l-0 border-slate-400"
|
|
|
|
>
|
|
|
|
<fa icon="magnifying-glass" class="fa-fw"></fa>
|
|
|
|
</button>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<!-- Result Tabs -->
|
|
|
|
<div class="text-center text-slate-500 border-b border-slate-300">
|
|
|
|
<ul class="flex flex-wrap justify-center gap-4 -mb-px">
|
|
|
|
<li>
|
|
|
|
<a
|
|
|
|
href="#"
|
|
|
|
@click="
|
|
|
|
projects = [];
|
|
|
|
isLocalActive = true;
|
|
|
|
isMappedActive = false;
|
|
|
|
isRemoteActive = false;
|
|
|
|
isSearchVisible = true;
|
|
|
|
tempSearchBox = null;
|
|
|
|
searchLocal();
|
|
|
|
"
|
|
|
|
v-bind:class="computedLocalTabStyleClassNames()"
|
|
|
|
>
|
|
|
|
Nearby
|
|
|
|
<!-- restore when the links don't jump around for different numbers
|
|
|
|
<span
|
|
|
|
class="font-semibold text-sm bg-slate-200 px-1.5 py-0.5 rounded-md"
|
|
|
|
v-if="isLocalActive"
|
|
|
|
>
|
|
|
|
{{ localCount > -1 ? localCount : "?" }}
|
|
|
|
</span>
|
|
|
|
-->
|
|
|
|
</a>
|
|
|
|
</li>
|
|
|
|
<li>
|
|
|
|
<a
|
|
|
|
href="#"
|
|
|
|
@click="
|
|
|
|
projects = [];
|
|
|
|
isLocalActive = false;
|
|
|
|
isMappedActive = true;
|
|
|
|
isRemoteActive = false;
|
|
|
|
isSearchVisible = false;
|
|
|
|
searchTerms = '';
|
|
|
|
tempSearchBox = null;
|
|
|
|
"
|
|
|
|
v-bind:class="computedMappedTabStyleClassNames()"
|
|
|
|
>
|
|
|
|
Mapped
|
|
|
|
</a>
|
|
|
|
</li>
|
|
|
|
<li>
|
|
|
|
<a
|
|
|
|
href="#"
|
|
|
|
@click="
|
|
|
|
projects = [];
|
|
|
|
isLocalActive = false;
|
|
|
|
isMappedActive = false;
|
|
|
|
isRemoteActive = true;
|
|
|
|
isSearchVisible = true;
|
|
|
|
tempSearchBox = null;
|
|
|
|
searchAll();
|
|
|
|
"
|
|
|
|
v-bind:class="computedRemoteTabStyleClassNames()"
|
|
|
|
>
|
|
|
|
Anywhere
|
|
|
|
<!-- restore when the links don't jump around for different numbers
|
|
|
|
<span
|
|
|
|
class="font-semibold text-sm bg-slate-200 px-1.5 py-0.5 rounded-md"
|
|
|
|
v-if="isRemoteActive"
|
|
|
|
>
|
|
|
|
{{ remoteCount > -1 ? remoteCount : "?" }}
|
|
|
|
</span>
|
|
|
|
-->
|
|
|
|
</a>
|
|
|
|
</li>
|
|
|
|
</ul>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<div v-if="isLocalActive">
|
|
|
|
<div class="text-center">
|
|
|
|
<button
|
|
|
|
class="ml-2 mt-2 px-4 py-2 rounded-md bg-blue-200 text-blue-500"
|
|
|
|
@click="$router.push({ name: 'search-area' })"
|
|
|
|
>
|
|
|
|
<fa icon="location-dot" class="fa-fw" />
|
|
|
|
Select a {{ searchBox ? "Different" : "" }} Location for Nearby Search
|
|
|
|
</button>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<div v-if="isMappedActive && !tempSearchBox">
|
|
|
|
<div class="mt-4 h-96 w-5/6 mx-auto">
|
|
|
|
<l-map
|
|
|
|
ref="projectMap"
|
|
|
|
@ready="onMapReady"
|
|
|
|
@moveend="onMoveEnd"
|
|
|
|
@movestart="onMoveStart"
|
|
|
|
@zoomend="onZoomEnd"
|
|
|
|
@zoomstart="onZoomStart"
|
|
|
|
>
|
|
|
|
<l-tile-layer
|
|
|
|
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
|
|
|
|
layer-type="base"
|
|
|
|
name="OpenStreetMap"
|
|
|
|
/>
|
|
|
|
</l-map>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<!-- Loading Animation -->
|
|
|
|
<div
|
|
|
|
class="fixed left-6 bottom-24 text-center text-4xl leading-none bg-slate-400 text-white w-14 py-2.5 rounded-full"
|
|
|
|
v-if="isLoading"
|
|
|
|
>
|
|
|
|
<fa icon="spinner" class="fa-spin-pulse"></fa>
|
|
|
|
</div>
|
|
|
|
<div v-else-if="projects.length === 0" class="text-center mt-8">
|
|
|
|
<p class="text-lg text-slate-500">
|
|
|
|
<span v-if="isLocalActive">
|
|
|
|
<span v-if="searchBox"> None found in the selected area. </span>
|
|
|
|
<!-- Otherwise there's no search area selected so we'll just leave the search box for them to click. -->
|
|
|
|
</span>
|
|
|
|
<span v-else-if="isRemoteActive"
|
|
|
|
>No projects were found with that search.</span
|
|
|
|
>
|
|
|
|
</p>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<!-- Results List -->
|
|
|
|
<InfiniteScroll @reached-bottom="loadMoreData">
|
|
|
|
<ul id="listDiscoverResults">
|
|
|
|
<li
|
|
|
|
class="border-b border-slate-300"
|
|
|
|
v-for="project in projects"
|
|
|
|
:key="project.handleId"
|
|
|
|
>
|
|
|
|
<a
|
|
|
|
@click="onClickLoadProject(project.handleId)"
|
|
|
|
class="block py-4 flex gap-4 cursor-pointer"
|
|
|
|
>
|
|
|
|
<div>
|
|
|
|
<ProjectIcon
|
|
|
|
:entityId="project.handleId"
|
|
|
|
:iconSize="48"
|
|
|
|
:imageUrl="project.image"
|
|
|
|
class="block border border-slate-300 rounded-md max-h-12 max-w-12"
|
|
|
|
/>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<div class="grow">
|
|
|
|
<h2 class="text-base font-semibold">{{ project.name }}</h2>
|
|
|
|
<div class="text-sm">
|
|
|
|
<fa icon="user" class="fa-fw text-slate-400"></fa>
|
|
|
|
{{
|
|
|
|
didInfo(project.issuerDid, activeDid, allMyDids, allContacts)
|
|
|
|
}}
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</a>
|
|
|
|
</li>
|
|
|
|
</ul>
|
|
|
|
</InfiniteScroll>
|
|
|
|
</section>
|
|
|
|
</template>
|
|
|
|
|
|
|
|
<script lang="ts">
|
|
|
|
import "leaflet/dist/leaflet.css";
|
|
|
|
import * as L from "leaflet";
|
|
|
|
import { Component, Vue } from "vue-facing-decorator";
|
|
|
|
import { LMap, LTileLayer } from "@vue-leaflet/vue-leaflet";
|
|
|
|
import { Router } from "vue-router";
|
|
|
|
|
|
|
|
import QuickNav from "@/components/QuickNav.vue";
|
|
|
|
import InfiniteScroll from "@/components/InfiniteScroll.vue";
|
|
|
|
import ProjectIcon from "@/components/ProjectIcon.vue";
|
|
|
|
import OnboardingDialog from "@/components/OnboardingDialog.vue";
|
|
|
|
import TopMessage from "@/components/TopMessage.vue";
|
|
|
|
import { NotificationIface } from "@/constants/app";
|
|
|
|
import {
|
|
|
|
db,
|
|
|
|
logConsoleAndDb,
|
|
|
|
retrieveSettingsForActiveAccount,
|
|
|
|
} from "@/db/index";
|
|
|
|
import { Contact } from "@/db/tables/contacts";
|
|
|
|
import { BoundingBox } from "@/db/tables/settings";
|
|
|
|
import {
|
|
|
|
didInfo,
|
|
|
|
errorStringForLog,
|
|
|
|
getHeaders,
|
|
|
|
PlanData,
|
|
|
|
} from "@/libs/endorserServer";
|
|
|
|
import { OnboardPage, retrieveAccountDids } from "@/libs/util";
|
|
|
|
|
|
|
|
@Component({
|
|
|
|
components: {
|
|
|
|
InfiniteScroll,
|
|
|
|
LMap,
|
|
|
|
LTileLayer,
|
|
|
|
OnboardingDialog,
|
|
|
|
ProjectIcon,
|
|
|
|
QuickNav,
|
|
|
|
TopMessage,
|
|
|
|
},
|
|
|
|
})
|
|
|
|
export default class DiscoverView extends Vue {
|
|
|
|
$notify!: (notification: NotificationIface, timeout?: number) => void;
|
|
|
|
|
|
|
|
activeDid = "";
|
|
|
|
allContacts: Array<Contact> = [];
|
|
|
|
allMyDids: Array<string> = [];
|
|
|
|
apiServer = "";
|
|
|
|
searchTerms = "";
|
|
|
|
projects: PlanData[] = [];
|
|
|
|
isLoading = false;
|
|
|
|
isLocalActive = true;
|
|
|
|
isMappedActive = false;
|
|
|
|
isRemoteActive = false;
|
|
|
|
isSearchVisible = true;
|
|
|
|
localCenterLat = 0;
|
|
|
|
localCenterLong = 0;
|
|
|
|
localCount = -1;
|
|
|
|
markers: { [key: string]: L.Marker } = {};
|
|
|
|
remoteCount = -1;
|
|
|
|
searchBox: { name: string; bbox: BoundingBox } | null = null;
|
|
|
|
tempSearchBox: BoundingBox | null = null;
|
|
|
|
zoomedSoDoNotMove = false;
|
|
|
|
|
|
|
|
// make this function available to the Vue template
|
|
|
|
didInfo = didInfo;
|
|
|
|
|
|
|
|
async mounted() {
|
|
|
|
const settings = await retrieveSettingsForActiveAccount();
|
|
|
|
this.activeDid = (settings.activeDid as string) || "";
|
|
|
|
this.apiServer = (settings.apiServer as string) || "";
|
|
|
|
this.searchBox = settings.searchBoxes?.[0] || null;
|
|
|
|
|
|
|
|
this.allContacts = await db.contacts.toArray();
|
|
|
|
|
|
|
|
this.allMyDids = await retrieveAccountDids();
|
|
|
|
|
|
|
|
this.searchTerms = (this.$route as Router).query["searchText"] || "";
|
|
|
|
|
|
|
|
if (!settings.finishedOnboarding) {
|
|
|
|
(this.$refs.onboardingDialog as OnboardingDialog).open(
|
|
|
|
OnboardPage.Discover,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (this.searchBox) {
|
|
|
|
await this.searchLocal();
|
|
|
|
|
|
|
|
const bbox = this.searchBox.bbox;
|
|
|
|
this.localCenterLat = (bbox.maxLat + bbox.minLat) / 2;
|
|
|
|
this.localCenterLong = (bbox.eastLong + bbox.westLong) / 2;
|
|
|
|
} else {
|
|
|
|
this.isLocalActive = false;
|
|
|
|
this.isMappedActive = false;
|
|
|
|
this.isRemoteActive = true;
|
|
|
|
await this.searchAll();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public resetCounts() {
|
|
|
|
this.localCount = -1;
|
|
|
|
this.remoteCount = -1;
|
|
|
|
}
|
|
|
|
|
|
|
|
public async searchSelected() {
|
|
|
|
if (this.isLocalActive) {
|
|
|
|
await this.searchLocal();
|
|
|
|
} else if (this.isMappedActive) {
|
|
|
|
this.isRemoteActive = true;
|
|
|
|
await this.searchAll();
|
|
|
|
} else {
|
|
|
|
await this.searchAll();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public async searchAll(beforeId?: string) {
|
|
|
|
this.resetCounts();
|
|
|
|
|
|
|
|
if (!beforeId) {
|
|
|
|
// this was an initial search so clear any previous results
|
|
|
|
this.projects = [];
|
|
|
|
}
|
|
|
|
|
|
|
|
let queryParams = "claimContents=" + encodeURIComponent(this.searchTerms);
|
|
|
|
|
|
|
|
if (beforeId) {
|
|
|
|
queryParams = queryParams + `&beforeId=${beforeId}`;
|
|
|
|
}
|
|
|
|
|
|
|
|
try {
|
|
|
|
this.isLoading = true;
|
|
|
|
const response = await fetch(
|
|
|
|
this.apiServer + "/api/v2/report/plans?" + queryParams,
|
|
|
|
{
|
|
|
|
method: "GET",
|
|
|
|
headers: await getHeaders(this.activeDid),
|
|
|
|
},
|
|
|
|
);
|
|
|
|
|
|
|
|
if (response.status !== 200) {
|
|
|
|
const details = await response.text();
|
|
|
|
console.error("Problem with full search:", details);
|
|
|
|
this.$notify(
|
|
|
|
{
|
|
|
|
group: "alert",
|
|
|
|
type: "danger",
|
|
|
|
title: "Error",
|
|
|
|
text: `There was a problem accessing the server. Try again later.`,
|
|
|
|
},
|
|
|
|
-1,
|
|
|
|
);
|
|
|
|
|
|
|
|
throw details;
|
|
|
|
}
|
|
|
|
|
|
|
|
const results = await response.json();
|
|
|
|
|
|
|
|
const plans: PlanData[] = results.data;
|
|
|
|
if (plans) {
|
|
|
|
for (const plan of plans) {
|
|
|
|
const { name, description, handleId, image, issuerDid, rowid } = plan;
|
|
|
|
this.projects.push({
|
|
|
|
name,
|
|
|
|
description,
|
|
|
|
handleId,
|
|
|
|
image,
|
|
|
|
issuerDid,
|
|
|
|
rowid,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
this.remoteCount = this.projects.length;
|
|
|
|
} else {
|
|
|
|
throw JSON.stringify(results);
|
|
|
|
}
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
|
|
} catch (e: any) {
|
|
|
|
console.error("Error with feed load:", e);
|
|
|
|
// this sometimes gives different information
|
|
|
|
console.error("Error with feed load (error added): " + e);
|
|
|
|
this.$notify(
|
|
|
|
{
|
|
|
|
group: "alert",
|
|
|
|
type: "danger",
|
|
|
|
title: "Error",
|
|
|
|
text: e.userMessage || "There was a problem retrieving projects.",
|
|
|
|
},
|
|
|
|
-1,
|
|
|
|
);
|
|
|
|
} finally {
|
|
|
|
this.isLoading = false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public async searchLocal(beforeId?: string) {
|
|
|
|
this.resetCounts();
|
|
|
|
|
|
|
|
const searchBox =
|
|
|
|
(this.isMappedActive && this.tempSearchBox) ||
|
|
|
|
(this.isLocalActive && this.searchBox?.bbox);
|
|
|
|
|
|
|
|
if (!searchBox) {
|
|
|
|
this.projects = [];
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!beforeId) {
|
|
|
|
// this was an initial search so clear any previous results
|
|
|
|
this.projects = [];
|
|
|
|
}
|
|
|
|
|
|
|
|
const claimContents =
|
|
|
|
"claimContents=" + encodeURIComponent(this.searchTerms);
|
|
|
|
|
|
|
|
let queryParams = [
|
|
|
|
claimContents,
|
|
|
|
"minLocLat=" + searchBox.minLat,
|
|
|
|
"maxLocLat=" + searchBox.maxLat,
|
|
|
|
"westLocLon=" + searchBox.westLong,
|
|
|
|
"eastLocLon=" + searchBox.eastLong,
|
|
|
|
].join("&");
|
|
|
|
|
|
|
|
if (beforeId) {
|
|
|
|
queryParams = queryParams + `&beforeId=${beforeId}`;
|
|
|
|
}
|
|
|
|
|
|
|
|
try {
|
|
|
|
this.isLoading = true;
|
|
|
|
const response = await fetch(
|
|
|
|
this.apiServer + "/api/v2/report/plansByLocation?" + queryParams,
|
|
|
|
{
|
|
|
|
method: "GET",
|
|
|
|
headers: await getHeaders(this.activeDid),
|
|
|
|
},
|
|
|
|
);
|
|
|
|
|
|
|
|
if (response.status !== 200) {
|
|
|
|
const details = await response.text();
|
|
|
|
console.error("Problem with nearby search:", details);
|
|
|
|
this.$notify(
|
|
|
|
{
|
|
|
|
group: "alert",
|
|
|
|
type: "danger",
|
|
|
|
title: "Error",
|
|
|
|
text: "There was a problem accessing the server. Try again later.",
|
|
|
|
},
|
|
|
|
-1,
|
|
|
|
);
|
|
|
|
throw await response.text();
|
|
|
|
}
|
|
|
|
|
|
|
|
const results = await response.json();
|
|
|
|
|
|
|
|
if (results.data) {
|
|
|
|
if (beforeId) {
|
|
|
|
const plans: PlanData[] = results.data;
|
|
|
|
for (const plan of plans) {
|
|
|
|
const { name, description, handleId, issuerDid, rowid } = plan;
|
|
|
|
this.projects.push({
|
|
|
|
name,
|
|
|
|
description,
|
|
|
|
handleId,
|
|
|
|
issuerDid,
|
|
|
|
rowid,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
this.projects = results.data;
|
|
|
|
}
|
|
|
|
this.localCount = this.projects.length;
|
|
|
|
} else {
|
|
|
|
throw JSON.stringify(results);
|
|
|
|
}
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
|
|
} catch (e: any) {
|
|
|
|
console.error("Error with feed load:", e);
|
|
|
|
this.$notify(
|
|
|
|
{
|
|
|
|
group: "alert",
|
|
|
|
type: "danger",
|
|
|
|
title: "Error",
|
|
|
|
text: e.userMessage || "There was a problem retrieving projects.",
|
|
|
|
},
|
|
|
|
-1,
|
|
|
|
);
|
|
|
|
} finally {
|
|
|
|
this.isLoading = false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Data loader used by infinite scroller
|
|
|
|
* @param payload is the flag from the InfiniteScroll indicating if it should load
|
|
|
|
**/
|
|
|
|
async loadMoreData(payload: boolean) {
|
|
|
|
if (this.projects.length > 0 && payload) {
|
|
|
|
const latestProject = this.projects[this.projects.length - 1];
|
|
|
|
if (this.isLocalActive) {
|
|
|
|
this.searchLocal(latestProject["rowid"]);
|
|
|
|
} else if (this.isMappedActive) {
|
|
|
|
this.searchLocal(latestProject["rowid"]);
|
|
|
|
} else if (this.isRemoteActive) {
|
|
|
|
this.searchAll(latestProject["rowid"]);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
async onMapReady(map: L.Map) {
|
|
|
|
// doing this here instead of the l-map element avoids a recentering after the first drag
|
|
|
|
map.setView([this.localCenterLat, this.localCenterLong], 2);
|
|
|
|
this.requestTiles(map);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Tried but failed to use other vue-leaflet methods update:zoom and update:bounds
|
|
|
|
// To access the from this.$refs, use this.$refs.projectMap.mapObject
|
|
|
|
|
|
|
|
onMoveStart(/* event: L.LocationEvent */) {
|
|
|
|
// don't remove markers because they follow the map when moving (and the experience is jarring)
|
|
|
|
}
|
|
|
|
|
|
|
|
async onMoveEnd(event: L.LocationEvent) {
|
|
|
|
if (this.zoomedSoDoNotMove) {
|
|
|
|
// since a zoom triggers a moveend, too, don't duplicate a tile request
|
|
|
|
this.zoomedSoDoNotMove = false;
|
|
|
|
} else {
|
|
|
|
// not part of a zoom so request tiles
|
|
|
|
await this.requestTiles(event.target);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
onZoomStart(/* event: L.LocationEvent */) {
|
|
|
|
// remove markers because otherwise they jump around at zoom end
|
|
|
|
Object.values(this.markers).forEach((marker) => marker.remove());
|
|
|
|
this.markers = {};
|
|
|
|
|
|
|
|
this.zoomedSoDoNotMove = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
async onZoomEnd(event: L.LocationEvent) {
|
|
|
|
await this.requestTiles(event.target);
|
|
|
|
}
|
|
|
|
|
|
|
|
async requestTiles(targetMap: L.Map) {
|
|
|
|
try {
|
|
|
|
const bounds = targetMap.getBounds();
|
|
|
|
const queryParams = [
|
|
|
|
"minLocLat=" + bounds?.getSouthWest().lat,
|
|
|
|
"maxLocLat=" + bounds?.getNorthEast().lat,
|
|
|
|
"westLocLon=" + bounds?.getSouthWest().lng,
|
|
|
|
"eastLocLon=" + bounds?.getNorthEast().lng,
|
|
|
|
].join("&");
|
|
|
|
const response = await fetch(
|
|
|
|
this.apiServer + "/api/v2/report/planCountsByBBox?" + queryParams,
|
|
|
|
);
|
|
|
|
if (response.status === 200) {
|
|
|
|
Object.values(this.markers).forEach((marker) => marker.remove());
|
|
|
|
this.markers = {};
|
|
|
|
const results = await response.json();
|
|
|
|
if (results.data?.tiles?.length > 0) {
|
|
|
|
for (const tile of results.data.tiles) {
|
|
|
|
const pinLat = (tile.minFoundLat + tile.maxFoundLat) / 2;
|
|
|
|
const pinLon = (tile.minFoundLon + tile.maxFoundLon) / 2;
|
|
|
|
const numberIcon = L.divIcon({
|
|
|
|
className: "numbered-marker",
|
|
|
|
html: `<strong>${tile.recordCount}</strong>`,
|
|
|
|
iconSize: [24, 24],
|
|
|
|
// Why isn't this showing?
|
|
|
|
iconAnchor: [12, 12], // coordinates of the tip of the icon relative to the top-left corner of the icon
|
|
|
|
});
|
|
|
|
const marker = L.marker([pinLat, pinLon], { icon: numberIcon });
|
|
|
|
marker.addTo(targetMap);
|
|
|
|
marker.on("click", () => {
|
|
|
|
this.tempSearchBox = {
|
|
|
|
minLat: tile.minFoundLat,
|
|
|
|
maxLat: tile.maxFoundLat,
|
|
|
|
westLong: tile.minFoundLon,
|
|
|
|
eastLong: tile.maxFoundLon,
|
|
|
|
};
|
|
|
|
this.searchLocal();
|
|
|
|
});
|
|
|
|
this.markers["" + tile.indexLat + "X" + tile.indexLon] = marker;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
await this.searchLocal();
|
|
|
|
} else {
|
|
|
|
throw {
|
|
|
|
message: "Got an error loading projects on the map.",
|
|
|
|
response: {
|
|
|
|
status: response.status,
|
|
|
|
statusText: response.statusText,
|
|
|
|
url: response.url,
|
|
|
|
},
|
|
|
|
};
|
|
|
|
}
|
|
|
|
} catch (e) {
|
|
|
|
logConsoleAndDb(
|
|
|
|
"Error loading projects on the map: " + errorStringForLog(e),
|
|
|
|
true,
|
|
|
|
);
|
|
|
|
this.$notify(
|
|
|
|
{
|
|
|
|
group: "alert",
|
|
|
|
type: "danger",
|
|
|
|
title: "Map Error",
|
|
|
|
text: "There was a problem loading projects on the map.",
|
|
|
|
},
|
|
|
|
3000,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Handle clicking on a project entry found in the list
|
|
|
|
* @param id of the project
|
|
|
|
**/
|
|
|
|
onClickLoadProject(id: string) {
|
|
|
|
const route = {
|
|
|
|
path: "/project/" + encodeURIComponent(id),
|
|
|
|
};
|
|
|
|
(this.$router as Router).push(route);
|
|
|
|
}
|
|
|
|
|
|
|
|
public computedLocalTabStyleClassNames() {
|
|
|
|
return {
|
|
|
|
"inline-block": true,
|
|
|
|
"py-3": true,
|
|
|
|
"rounded-t-lg": true,
|
|
|
|
"border-b-2": true,
|
|
|
|
|
|
|
|
active: this.isLocalActive,
|
|
|
|
"text-black": this.isLocalActive,
|
|
|
|
"border-black": this.isLocalActive,
|
|
|
|
"font-semibold": this.isLocalActive,
|
|
|
|
|
|
|
|
"text-blue-600": !this.isLocalActive,
|
|
|
|
"border-transparent": !this.isLocalActive,
|
|
|
|
"hover:border-slate-400": !this.isLocalActive,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
public computedMappedTabStyleClassNames() {
|
|
|
|
return {
|
|
|
|
"inline-block": true,
|
|
|
|
"py-3": true,
|
|
|
|
"rounded-t-lg": true,
|
|
|
|
"border-b-2": true,
|
|
|
|
|
|
|
|
active: this.isMappedActive,
|
|
|
|
"text-black": this.isMappedActive,
|
|
|
|
"border-black": this.isMappedActive,
|
|
|
|
"font-semibold": this.isMappedActive,
|
|
|
|
|
|
|
|
"text-blue-600": !this.isMappedActive,
|
|
|
|
"border-transparent": !this.isMappedActive,
|
|
|
|
"hover:border-slate-400": !this.isMappedActive,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
public computedRemoteTabStyleClassNames() {
|
|
|
|
return {
|
|
|
|
"inline-block": true,
|
|
|
|
"py-3": true,
|
|
|
|
"rounded-t-lg": true,
|
|
|
|
"border-b-2": true,
|
|
|
|
|
|
|
|
active: this.isRemoteActive,
|
|
|
|
"text-black": this.isRemoteActive,
|
|
|
|
"border-black": this.isRemoteActive,
|
|
|
|
"font-semibold": this.isRemoteActive,
|
|
|
|
|
|
|
|
"text-blue-600": !this.isRemoteActive,
|
|
|
|
"border-transparent": !this.isRemoteActive,
|
|
|
|
"hover:border-slate-400": !this.isRemoteActive,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
}
|
|
|
|
</script>
|
|
|
|
<style>
|
|
|
|
.numbered-marker {
|
|
|
|
display: flex;
|
|
|
|
align-items: center;
|
|
|
|
justify-content: center;
|
|
|
|
text-align: center;
|
|
|
|
font-size: 14px;
|
|
|
|
font-weight: bold;
|
|
|
|
color: white;
|
|
|
|
background: blue;
|
|
|
|
width: 24px;
|
|
|
|
height: 24px;
|
|
|
|
border-radius: 50%;
|
|
|
|
border: 2px solid white;
|
|
|
|
}
|
|
|
|
</style>
|