forked from trent_larson/crowd-funder-for-time-pwa
Changes: - Move v-model directives before other attributes - Move v-bind directives before event handlers - Reorder attributes for better readability - Fix template attribute ordering across components - Improve eslint rules - add default vite config for testing (handles nostr error too) This follows Vue.js style guide recommendations for attribute ordering and improves template consistency.
882 lines
26 KiB
Vue
882 lines
26 KiB
Vue
<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 & People
|
|
</h1>
|
|
|
|
<OnboardingDialog ref="onboardingDialog" />
|
|
|
|
<!-- Quick Search -->
|
|
<div
|
|
id="QuickSearch"
|
|
class="mt-8 mb-4 flex"
|
|
:style="{ visibility: isSearchVisible ? 'visible' : 'hidden' }"
|
|
>
|
|
<input
|
|
v-model="searchTerms"
|
|
type="text"
|
|
placeholder="Search…"
|
|
class="block w-full rounded-l border border-r-0 border-slate-400 px-3 py-2"
|
|
@keyup.enter="searchSelected()"
|
|
/>
|
|
<button
|
|
class="px-4 rounded-r bg-slate-200 border border-l-0 border-slate-400"
|
|
@click="searchSelected()"
|
|
>
|
|
<font-awesome icon="magnifying-glass" class="fa-fw"></font-awesome>
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Result Tabs -->
|
|
<!-- Top Level Selection -->
|
|
<div class="text-center text-slate-500 border-b border-slate-300 mb-4">
|
|
<ul class="flex flex-wrap justify-center gap-4 -mb-px">
|
|
<li>
|
|
<a
|
|
href="#"
|
|
:class="computedProjectsTabStyleClassNames()"
|
|
@click="
|
|
projects = [];
|
|
userProfiles = [];
|
|
isProjectsActive = true;
|
|
isPeopleActive = false;
|
|
searchSelected();
|
|
"
|
|
>
|
|
Projects
|
|
</a>
|
|
</li>
|
|
<li>
|
|
<a
|
|
href="#"
|
|
:class="computedPeopleTabStyleClassNames()"
|
|
@click="
|
|
projects = [];
|
|
userProfiles = [];
|
|
isProjectsActive = false;
|
|
isPeopleActive = true;
|
|
searchSelected();
|
|
"
|
|
>
|
|
People
|
|
</a>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
|
|
<!-- Secondary 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="#"
|
|
:class="computedLocalTabStyleClassNames()"
|
|
@click="
|
|
projects = [];
|
|
userProfiles = [];
|
|
isLocalActive = true;
|
|
isMappedActive = false;
|
|
isAnywhereActive = false;
|
|
isSearchVisible = true;
|
|
tempSearchBox = null;
|
|
searchLocal();
|
|
"
|
|
>
|
|
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="#"
|
|
:class="computedMappedTabStyleClassNames()"
|
|
@click="
|
|
projects = [];
|
|
userProfiles = [];
|
|
isLocalActive = false;
|
|
isMappedActive = true;
|
|
isAnywhereActive = false;
|
|
isSearchVisible = false;
|
|
searchTerms = '';
|
|
tempSearchBox = null;
|
|
"
|
|
>
|
|
<!-- search is triggered when map component gets to "ready" state -->
|
|
Mapped
|
|
</a>
|
|
</li>
|
|
<li>
|
|
<a
|
|
href="#"
|
|
:class="computedRemoteTabStyleClassNames()"
|
|
@click="
|
|
projects = [];
|
|
userProfiles = [];
|
|
isLocalActive = false;
|
|
isMappedActive = false;
|
|
isAnywhereActive = true;
|
|
isSearchVisible = true;
|
|
tempSearchBox = null;
|
|
searchAll();
|
|
"
|
|
>
|
|
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="isAnywhereActive"
|
|
>
|
|
{{ 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' })"
|
|
>
|
|
<font-awesome 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
|
|
v-if="isLoading"
|
|
class="fixed left-6 bottom-24 text-center text-4xl leading-none bg-slate-400 text-white w-14 py-2.5 rounded-full"
|
|
>
|
|
<font-awesome icon="spinner" class="fa-spin-pulse"></font-awesome>
|
|
</div>
|
|
<div
|
|
v-else-if="projects.length === 0 && userProfiles.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="isAnywhereActive"
|
|
>No projects were found with that search.</span
|
|
>
|
|
</p>
|
|
</div>
|
|
|
|
<!-- Results List -->
|
|
<InfiniteScroll @reached-bottom="loadMoreData">
|
|
<ul id="listDiscoverResults">
|
|
<!-- Projects List -->
|
|
<template v-if="isProjectsActive">
|
|
<li
|
|
v-for="project in projects"
|
|
:key="project.handleId"
|
|
class="border-b border-slate-300"
|
|
>
|
|
<a
|
|
class="block py-4 flex gap-4 cursor-pointer"
|
|
@click="onClickLoadItem(project.handleId)"
|
|
>
|
|
<div>
|
|
<ProjectIcon
|
|
:entity-id="project.handleId"
|
|
:icon-size="48"
|
|
:image-url="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">
|
|
<font-awesome
|
|
icon="user"
|
|
class="fa-fw text-slate-400"
|
|
></font-awesome>
|
|
{{
|
|
didInfo(
|
|
project.issuerDid,
|
|
activeDid,
|
|
allMyDids,
|
|
allContacts,
|
|
)
|
|
}}
|
|
</div>
|
|
</div>
|
|
</a>
|
|
</li>
|
|
</template>
|
|
|
|
<!-- Profiles List -->
|
|
<template v-else>
|
|
<li
|
|
v-for="profile in userProfiles"
|
|
:key="profile.issuerDid"
|
|
class="border-b border-slate-300"
|
|
>
|
|
<a
|
|
class="block py-4 flex gap-4 cursor-pointer"
|
|
@click="onClickLoadItem(profile?.rowId || '')"
|
|
>
|
|
<div class="grow">
|
|
<div class="text-sm">
|
|
<font-awesome
|
|
icon="user"
|
|
class="fa-fw text-slate-400"
|
|
></font-awesome>
|
|
{{
|
|
didInfo(
|
|
profile.issuerDid,
|
|
activeDid,
|
|
allMyDids,
|
|
allContacts,
|
|
)
|
|
}}
|
|
</div>
|
|
<p
|
|
v-if="profile.description"
|
|
class="mt-1 text-sm text-slate-600"
|
|
>
|
|
{{ profile.description }}
|
|
</p>
|
|
<div
|
|
v-if="isAnywhereActive && profile.locLat && profile.locLon"
|
|
class="mt-1 text-xs text-slate-500"
|
|
>
|
|
<font-awesome
|
|
icon="location-dot"
|
|
class="fa-fw"
|
|
></font-awesome>
|
|
{{
|
|
(profile.locLat > 0 ? "North" : "South") +
|
|
" in " +
|
|
(profile.locLon > 0 ? "Eastern" : "Western") +
|
|
" Hemisphere"
|
|
}}
|
|
</div>
|
|
</div>
|
|
</a>
|
|
</li>
|
|
</template>
|
|
</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, RouteLocationNormalizedLoaded } 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,
|
|
DEFAULT_PARTNER_API_SERVER,
|
|
} from "../constants/app";
|
|
import {
|
|
db,
|
|
logConsoleAndDb,
|
|
retrieveSettingsForActiveAccount,
|
|
} from "../db/index";
|
|
import { Contact } from "../db/tables/contacts";
|
|
import { BoundingBox } from "../db/tables/settings";
|
|
import { PlanData } from "../interfaces";
|
|
import {
|
|
didInfo,
|
|
errorStringForLog,
|
|
getHeaders,
|
|
} from "../libs/endorserServer";
|
|
import { OnboardPage, retrieveAccountDids } from "../libs/util";
|
|
|
|
interface Tile {
|
|
indexLat: number;
|
|
indexLon: number;
|
|
minFoundLat: number;
|
|
maxFoundLat: number;
|
|
minFoundLon: number;
|
|
maxFoundLon: number;
|
|
recordCount: number;
|
|
}
|
|
|
|
@Component({
|
|
components: {
|
|
InfiniteScroll,
|
|
LMap,
|
|
LTileLayer,
|
|
OnboardingDialog,
|
|
ProjectIcon,
|
|
QuickNav,
|
|
TopMessage,
|
|
},
|
|
})
|
|
export default class DiscoverView extends Vue {
|
|
$notify!: (notification: NotificationIface, timeout?: number) => void;
|
|
$router!: Router;
|
|
$route!: RouteLocationNormalizedLoaded;
|
|
|
|
activeDid = "";
|
|
allContacts: Array<Contact> = [];
|
|
allMyDids: Array<string> = [];
|
|
apiServer = "";
|
|
isLoading = false;
|
|
isLocalActive = false;
|
|
isMappedActive = false;
|
|
isAnywhereActive = true;
|
|
isProjectsActive = true;
|
|
isPeopleActive = false;
|
|
isSearchVisible = true;
|
|
localCenterLat = 0;
|
|
localCenterLong = 0;
|
|
localCount = -1;
|
|
markers: { [key: string]: L.Marker } = {};
|
|
partnerApiServer = DEFAULT_PARTNER_API_SERVER;
|
|
projects: PlanData[] = [];
|
|
remoteCount = -1;
|
|
searchBox: { name: string; bbox: BoundingBox } | null = null;
|
|
searchTerms = "";
|
|
tempSearchBox: BoundingBox | null = null;
|
|
userProfiles: UserProfile[] = [];
|
|
zoomedSoDoNotMove = false;
|
|
|
|
// make this function available to the Vue template
|
|
didInfo = didInfo;
|
|
|
|
async mounted() {
|
|
|
|
this.searchTerms = this.$route.query["searchText"]?.toString() || "";
|
|
|
|
const searchPeople = !!this.$route.query["searchPeople"];
|
|
|
|
const settings = await retrieveSettingsForActiveAccount();
|
|
this.activeDid = (settings.activeDid as string) || "";
|
|
this.apiServer = (settings.apiServer as string) || "";
|
|
this.partnerApiServer =
|
|
(settings.partnerApiServer as string) || this.partnerApiServer;
|
|
this.searchBox = settings.searchBoxes?.[0] || null;
|
|
|
|
this.allContacts = await db.contacts.toArray();
|
|
|
|
this.allMyDids = await retrieveAccountDids();
|
|
|
|
if (!settings.finishedOnboarding) {
|
|
(this.$refs.onboardingDialog as OnboardingDialog).open(
|
|
OnboardPage.Discover,
|
|
);
|
|
}
|
|
|
|
// Someday we'll have enough people that we can default to their local area.
|
|
// if (this.searchBox) {
|
|
// this.isLocalActive = true;
|
|
// this.isAnywhereActive = false;
|
|
// await this.searchLocal();
|
|
//
|
|
// const bbox = this.searchBox.bbox;
|
|
// this.localCenterLat = (bbox.maxLat + bbox.minLat) / 2;
|
|
// this.localCenterLong = (bbox.eastLong + bbox.westLong) / 2;
|
|
// } else {
|
|
|
|
if (searchPeople) {
|
|
this.isPeopleActive = true;
|
|
this.isProjectsActive = false;
|
|
this.isMappedActive = true;
|
|
this.isAnywhereActive = false;
|
|
}
|
|
|
|
if (this.isMappedActive) {
|
|
// The map will be loaded when it's ready
|
|
// and if we try to do it here before the map is ready then we get errors.
|
|
} else {
|
|
await this.searchSelected();
|
|
}
|
|
}
|
|
|
|
public resetCounts() {
|
|
this.localCount = -1;
|
|
this.remoteCount = -1;
|
|
}
|
|
|
|
public async searchSelected() {
|
|
if (this.isLocalActive) {
|
|
await this.searchLocal();
|
|
} else if (this.isMappedActive) {
|
|
const mapRef = this.$refs.projectMap as L.Map;
|
|
this.requestTiles(mapRef.leafletObject); // not ideal because I found this from experimentation, not documentation
|
|
} 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 = [];
|
|
this.userProfiles = [];
|
|
}
|
|
|
|
let queryParams = "claimContents=" + encodeURIComponent(this.searchTerms);
|
|
|
|
if (beforeId) {
|
|
queryParams = queryParams + `&beforeId=${beforeId}`;
|
|
}
|
|
|
|
const endpoint = this.isProjectsActive
|
|
? this.apiServer + "/api/v2/report/plans"
|
|
: this.partnerApiServer + "/api/partner/userProfile";
|
|
|
|
try {
|
|
this.isLoading = true;
|
|
const response = await fetch(endpoint + "?" + queryParams, {
|
|
method: "GET",
|
|
headers: await getHeaders(this.activeDid),
|
|
});
|
|
|
|
if (response.status !== 200) {
|
|
const details = await response.text();
|
|
throw details;
|
|
}
|
|
|
|
const results = await response.json();
|
|
|
|
if (this.isProjectsActive) {
|
|
this.userProfiles = [];
|
|
const plans: PlanData[] = results.data;
|
|
if (plans) {
|
|
this.projects.push(...plans);
|
|
this.remoteCount = this.projects.length;
|
|
} else {
|
|
throw JSON.stringify(results);
|
|
}
|
|
} else { // people search must be active
|
|
this.projects = [];
|
|
const profiles: UserProfile[] = results.data;
|
|
if (profiles) {
|
|
this.userProfiles.push(...profiles);
|
|
this.remoteCount = this.userProfiles.length;
|
|
} else {
|
|
throw JSON.stringify(results);
|
|
}
|
|
}
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
} catch (e: any) {
|
|
console.error("Error with search all:", e);
|
|
// this sometimes gives different information
|
|
console.error("Error with search all (error added): " + e);
|
|
this.$notify(
|
|
{
|
|
group: "alert",
|
|
type: "danger",
|
|
title: "Error Searching",
|
|
text:
|
|
e.userMessage ||
|
|
"There was a problem retrieving " +
|
|
(this.isProjectsActive ? "projects" : "profiles") +
|
|
".",
|
|
},
|
|
5000,
|
|
);
|
|
} 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 = [];
|
|
this.userProfiles = [];
|
|
return;
|
|
}
|
|
|
|
if (!beforeId) {
|
|
// this was an initial search so clear any previous results
|
|
this.projects = [];
|
|
this.userProfiles = [];
|
|
}
|
|
|
|
const claimContents =
|
|
"claimContents=" + encodeURIComponent(this.searchTerms);
|
|
|
|
let queryParams = [
|
|
claimContents,
|
|
"minLocLat=" + searchBox.minLat,
|
|
"maxLocLat=" + searchBox.maxLat,
|
|
"minLocLon=" + searchBox.westLong,
|
|
"maxLocLon=" + searchBox.eastLong,
|
|
].join("&");
|
|
|
|
if (beforeId) {
|
|
queryParams = queryParams + `&beforeId=${beforeId}`;
|
|
}
|
|
|
|
const endpoint = this.isProjectsActive
|
|
? this.apiServer + "/api/v2/report/plansByLocation"
|
|
: this.partnerApiServer + "/api/partner/userProfile";
|
|
|
|
try {
|
|
this.isLoading = true;
|
|
const response = await fetch(endpoint + "?" + queryParams, {
|
|
method: "GET",
|
|
headers: await getHeaders(this.activeDid),
|
|
});
|
|
|
|
if (response.status !== 200) {
|
|
const details = await response.text();
|
|
throw details;
|
|
}
|
|
|
|
const results = await response.json();
|
|
|
|
if (this.isProjectsActive) {
|
|
this.userProfiles = [];
|
|
const plans: PlanData[] = results.data;
|
|
if (plans) {
|
|
this.projects.push(...plans);
|
|
this.localCount = this.projects.length;
|
|
} else {
|
|
throw JSON.stringify(results);
|
|
}
|
|
} else {
|
|
this.projects = [];
|
|
const profiles: UserProfile[] = results.data;
|
|
if (profiles) {
|
|
this.userProfiles.push(...profiles);
|
|
this.localCount = this.userProfiles.length;
|
|
} else {
|
|
throw JSON.stringify(results);
|
|
}
|
|
}
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
} catch (e: any) {
|
|
console.error("Error with search local:", e);
|
|
this.$notify(
|
|
{
|
|
group: "alert",
|
|
type: "danger",
|
|
title: "Error",
|
|
text:
|
|
e.userMessage ||
|
|
"There was a problem retrieving " +
|
|
(this.isProjectsActive ? "projects" : "profiles") +
|
|
".",
|
|
},
|
|
5000,
|
|
);
|
|
} 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 (payload) {
|
|
if (this.isProjectsActive && this.projects.length > 0) {
|
|
const latestProject = this.projects[this.projects.length - 1];
|
|
if (this.isLocalActive || this.isMappedActive) {
|
|
this.searchLocal(latestProject.rowId);
|
|
} else if (this.isAnywhereActive) {
|
|
this.searchAll(latestProject.rowId);
|
|
}
|
|
} else if (this.isPeopleActive && this.userProfiles.length > 0) {
|
|
const latestProfile = this.userProfiles[this.userProfiles.length - 1];
|
|
if (this.isLocalActive || this.isMappedActive) {
|
|
this.searchLocal(latestProfile.rowId || "");
|
|
} else if (this.isAnywhereActive) {
|
|
this.searchAll(latestProfile.rowId || "");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
clearMarkers() {
|
|
Object.values(this.markers).forEach((marker) => marker.remove());
|
|
this.markers = {};
|
|
}
|
|
|
|
async onMapReady(map: L.Map) {
|
|
// doing this here instead of on the l-map element avoids a recentering after a drag then zoom at startup
|
|
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.leafletObject (or maybe 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
|
|
this.clearMarkers();
|
|
|
|
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 endpoint = this.isProjectsActive
|
|
? this.apiServer + "/api/v2/report/planCountsByBBox"
|
|
: this.partnerApiServer + "/api/partner/userProfileCountsByBBox";
|
|
const response = await fetch(endpoint + "?" + queryParams);
|
|
if (response.status === 200) {
|
|
this.clearMarkers();
|
|
const results = await response.json();
|
|
if (results.data?.tiles?.length > 0) {
|
|
for (const tile: 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 +
|
|
"_" +
|
|
tile.minFoundLat +
|
|
"X" +
|
|
tile.minFoundLon +
|
|
"-" +
|
|
tile.maxFoundLat +
|
|
"X" +
|
|
tile.maxFoundLon
|
|
] = marker;
|
|
}
|
|
}
|
|
} 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 or profile entry found in the list
|
|
* @param id of the project or profile
|
|
**/
|
|
onClickLoadItem(id: string) {
|
|
const route = {
|
|
path: this.isProjectsActive
|
|
? "/project/" + encodeURIComponent(id)
|
|
: "/userProfile/" + encodeURIComponent(id),
|
|
};
|
|
this.$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.isAnywhereActive,
|
|
"text-black": this.isAnywhereActive,
|
|
"border-black": this.isAnywhereActive,
|
|
"font-semibold": this.isAnywhereActive,
|
|
|
|
"text-blue-600": !this.isAnywhereActive,
|
|
"border-transparent": !this.isAnywhereActive,
|
|
"hover:border-slate-400": !this.isAnywhereActive,
|
|
};
|
|
}
|
|
|
|
public computedProjectsTabStyleClassNames() {
|
|
return {
|
|
"inline-block": true,
|
|
"py-3": true,
|
|
"rounded-t-lg": true,
|
|
"border-b-2": true,
|
|
|
|
active: this.isProjectsActive,
|
|
"text-black": this.isProjectsActive,
|
|
"border-black": this.isProjectsActive,
|
|
"font-semibold": this.isProjectsActive,
|
|
|
|
"text-blue-600": !this.isProjectsActive,
|
|
"border-transparent": !this.isProjectsActive,
|
|
"hover:border-slate-400": !this.isProjectsActive,
|
|
};
|
|
}
|
|
|
|
public computedPeopleTabStyleClassNames() {
|
|
return {
|
|
"inline-block": true,
|
|
"py-3": true,
|
|
"rounded-t-lg": true,
|
|
"border-b-2": true,
|
|
|
|
active: this.isPeopleActive,
|
|
"text-black": this.isPeopleActive,
|
|
"border-black": this.isPeopleActive,
|
|
"font-semibold": this.isPeopleActive,
|
|
|
|
"text-blue-600": !this.isPeopleActive,
|
|
"border-transparent": !this.isPeopleActive,
|
|
"hover:border-slate-400": !this.isPeopleActive,
|
|
};
|
|
}
|
|
}
|
|
</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>
|