|
|
@ -9,7 +9,7 @@ |
|
|
|
</h1> |
|
|
|
|
|
|
|
<!-- Quick Search --> |
|
|
|
<div id="QuickSearch" class="mb-4 flex" v-on:keyup.enter="search()"> |
|
|
|
<div id="QuickSearch" class="mb-4 flex" v-on:keyup.enter="searchAll()"> |
|
|
|
<input |
|
|
|
type="text" |
|
|
|
v-model="searchTerms" |
|
|
@ -17,7 +17,7 @@ |
|
|
|
class="block w-full rounded-l border border-r-0 border-slate-400 px-3 py-2" |
|
|
|
/> |
|
|
|
<button |
|
|
|
@click="search()" |
|
|
|
@click="searchAll()" |
|
|
|
class="px-4 rounded-r bg-slate-200 border border-l-0 border-slate-400" |
|
|
|
> |
|
|
|
<fa icon="magnifying-glass" class="fa-fw"></fa> |
|
|
@ -32,6 +32,8 @@ |
|
|
|
href="#" |
|
|
|
@click=" |
|
|
|
projects = []; |
|
|
|
isLocalActive = true; |
|
|
|
isRemoteActive = false; |
|
|
|
searchLocal(); |
|
|
|
" |
|
|
|
v-bind:class="computedLocalTabClassNames()" |
|
|
@ -49,7 +51,9 @@ |
|
|
|
v-bind:class="computedRemoteTabClassNames()" |
|
|
|
@click=" |
|
|
|
projects = []; |
|
|
|
search(); |
|
|
|
isRemoteActive = true; |
|
|
|
isLocalActive = false; |
|
|
|
searchAll(); |
|
|
|
" |
|
|
|
> |
|
|
|
Remote |
|
|
@ -62,6 +66,49 @@ |
|
|
|
</ul> |
|
|
|
</div> |
|
|
|
|
|
|
|
<div v-if="isLocalActive"> |
|
|
|
<div v-if="!isChoosingSearchBox"> |
|
|
|
<button |
|
|
|
class="ml-2 px-4 py-2 rounded-md bg-blue-200 text-blue-500" |
|
|
|
@click="isChoosingSearchBox = true" |
|
|
|
> |
|
|
|
Select a {{ searchBox ? "Different" : "" }} Location for Nearby Search |
|
|
|
</button> |
|
|
|
</div> |
|
|
|
<div v-else> |
|
|
|
<button v-if="!searchBox && !isNewMarkerSet" class="m-4 px-4 py-2"> |
|
|
|
Choose Location Below for Nearby Search |
|
|
|
</button> |
|
|
|
<button |
|
|
|
v-if="isNewMarkerSet" |
|
|
|
class="m-4 px-4 py-2 rounded-md bg-blue-200 text-blue-500" |
|
|
|
@click="storeSearchBox" |
|
|
|
> |
|
|
|
Store This Location for Nearby Search |
|
|
|
</button> |
|
|
|
<button |
|
|
|
v-if="searchBox" |
|
|
|
class="m-4 px-4 py-2 rounded-md bg-blue-200 text-blue-500" |
|
|
|
@click="forgetSearchBox" |
|
|
|
> |
|
|
|
Delete Stored Location |
|
|
|
</button> |
|
|
|
<button |
|
|
|
v-if="isNewMarkerSet" |
|
|
|
class="m-4 px-4 py-2 rounded-md bg-blue-200 text-blue-500" |
|
|
|
@click="resetLatLong" |
|
|
|
> |
|
|
|
Reset Marker |
|
|
|
</button> |
|
|
|
<button |
|
|
|
class="ml-2 px-4 py-2 rounded-md bg-blue-200 text-blue-500" |
|
|
|
@click="cancelSearchBoxSelect" |
|
|
|
> |
|
|
|
Cancel |
|
|
|
</button> |
|
|
|
</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" |
|
|
@ -71,7 +118,7 @@ |
|
|
|
</div> |
|
|
|
|
|
|
|
<!-- Results List --> |
|
|
|
<InfiniteScroll @reached-bottom="loadMoreData"> |
|
|
|
<InfiniteScroll @reached-bottom="loadMoreData" v-if="!isChoosingSearchBox"> |
|
|
|
<ul> |
|
|
|
<li |
|
|
|
class="border-b border-slate-300" |
|
|
@ -103,42 +150,103 @@ |
|
|
|
</li> |
|
|
|
</ul> |
|
|
|
</InfiniteScroll> |
|
|
|
<AlertMessage |
|
|
|
:alertTitle="alertTitle" |
|
|
|
:alertMessage="alertMessage" |
|
|
|
></AlertMessage> |
|
|
|
|
|
|
|
<div |
|
|
|
v-if="isLocalActive && isChoosingSearchBox" |
|
|
|
style="height: 600px; width: 800px" |
|
|
|
> |
|
|
|
<l-map |
|
|
|
ref="map" |
|
|
|
:center="[localCenterLat, localCenterLong]" |
|
|
|
v-model:zoom="localZoom" |
|
|
|
@click="setMapPoint" |
|
|
|
> |
|
|
|
<l-tile-layer |
|
|
|
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png" |
|
|
|
layer-type="base" |
|
|
|
name="OpenStreetMap" |
|
|
|
/> |
|
|
|
<l-marker |
|
|
|
v-if="isNewMarkerSet" |
|
|
|
:lat-lng="[localCenterLat, localCenterLong]" |
|
|
|
@click="isNewMarkerSet = false" |
|
|
|
/> |
|
|
|
<l-rectangle |
|
|
|
v-if="isNewMarkerSet" |
|
|
|
:bounds="[ |
|
|
|
[localCenterLat - localLatDiff, localCenterLong - localLongDiff], |
|
|
|
[localCenterLat + localLatDiff, localCenterLong + localLongDiff], |
|
|
|
]" |
|
|
|
:weight="1" |
|
|
|
/> |
|
|
|
</l-map> |
|
|
|
</div> |
|
|
|
</section> |
|
|
|
</template> |
|
|
|
|
|
|
|
<script lang="ts"> |
|
|
|
import { LeafletMouseEvent } from "leaflet"; |
|
|
|
import "leaflet/dist/leaflet.css"; |
|
|
|
import { Component, Vue } from "vue-facing-decorator"; |
|
|
|
|
|
|
|
import { accountsDB, db } from "@/db"; |
|
|
|
import { |
|
|
|
LMap, |
|
|
|
LMarker, |
|
|
|
LRectangle, |
|
|
|
LTileLayer, |
|
|
|
} from "@vue-leaflet/vue-leaflet"; |
|
|
|
|
|
|
|
import { accountsDB, db } from "@/db/index"; |
|
|
|
import { Contact } from "@/db/tables/contacts"; |
|
|
|
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings"; |
|
|
|
import { BoundingBox, MASTER_SETTINGS_KEY } from "@/db/tables/settings"; |
|
|
|
import { accessToken } from "@/libs/crypto"; |
|
|
|
import { didInfo } from "@/libs/endorserServer"; |
|
|
|
import AlertMessage from "@/components/AlertMessage"; |
|
|
|
import QuickNav from "@/components/QuickNav"; |
|
|
|
import InfiniteScroll from "@/components/InfiniteScroll"; |
|
|
|
import EntityIcon from "@/components/EntityIcon"; |
|
|
|
import { didInfo, ProjectData } from "@/libs/endorserServer"; |
|
|
|
import QuickNav from "@/components/QuickNav.vue"; |
|
|
|
import InfiniteScroll from "@/components/InfiniteScroll.vue"; |
|
|
|
import EntityIcon from "@/components/EntityIcon.vue"; |
|
|
|
|
|
|
|
const DEFAULT_LAT_LONG_DIFF = 0.01; |
|
|
|
const WORLD_ZOOM = 2; |
|
|
|
const DEFAULT_ZOOM = 2; |
|
|
|
|
|
|
|
interface Notification { |
|
|
|
group: string; |
|
|
|
type: string; |
|
|
|
title: string; |
|
|
|
text: string; |
|
|
|
} |
|
|
|
|
|
|
|
@Component({ |
|
|
|
components: { AlertMessage, QuickNav, InfiniteScroll, EntityIcon }, |
|
|
|
components: { |
|
|
|
LRectangle, |
|
|
|
QuickNav, |
|
|
|
InfiniteScroll, |
|
|
|
EntityIcon, |
|
|
|
LMap, |
|
|
|
LMarker, |
|
|
|
LTileLayer, |
|
|
|
}, |
|
|
|
}) |
|
|
|
export default class DiscoverView extends Vue { |
|
|
|
$notify!: (notification: Notification, timeout?: number) => void; |
|
|
|
|
|
|
|
activeDid = ""; |
|
|
|
allContacts: Array<Contact> = []; |
|
|
|
allMyDids: Array<string> = []; |
|
|
|
apiServer = ""; |
|
|
|
searchTerms = ""; |
|
|
|
alertMessage = ""; |
|
|
|
alertTitle = ""; |
|
|
|
projects: ProjectData[] = []; |
|
|
|
isChoosingSearchBox = false; |
|
|
|
isLocalActive = true; |
|
|
|
isRemoteActive = false; |
|
|
|
isNewMarkerSet = false; |
|
|
|
localCenterLat = 0; |
|
|
|
localCenterLong = 0; |
|
|
|
localLatDiff = DEFAULT_LAT_LONG_DIFF; |
|
|
|
localLongDiff = DEFAULT_LAT_LONG_DIFF; |
|
|
|
localCount = 0; |
|
|
|
localZoom = DEFAULT_ZOOM; |
|
|
|
remoteCount = 0; |
|
|
|
searchBox: { name: string; bbox: BoundingBox } | null = null; |
|
|
|
isLoading = false; |
|
|
|
|
|
|
|
// make this function available to the Vue template |
|
|
@ -149,6 +257,9 @@ export default class DiscoverView extends Vue { |
|
|
|
const settings = await db.settings.get(MASTER_SETTINGS_KEY); |
|
|
|
this.activeDid = settings?.activeDid || ""; |
|
|
|
this.apiServer = settings?.apiServer || ""; |
|
|
|
this.searchBox = settings?.searchBoxes?.[0] || null; |
|
|
|
this.resetLatLong(); |
|
|
|
|
|
|
|
this.allContacts = await db.contacts.toArray(); |
|
|
|
|
|
|
|
await accountsDB.open(); |
|
|
@ -158,8 +269,10 @@ export default class DiscoverView extends Vue { |
|
|
|
this.searchLocal(); |
|
|
|
} |
|
|
|
|
|
|
|
public async buildHeaders() { |
|
|
|
const headers = { "Content-Type": "application/json" }; |
|
|
|
public async buildHeaders(): Promise<HeadersInit> { |
|
|
|
const headers: HeadersInit = { |
|
|
|
"Content-Type": "application/json", |
|
|
|
}; |
|
|
|
|
|
|
|
if (this.activeDid) { |
|
|
|
await accountsDB.open(); |
|
|
@ -180,16 +293,13 @@ export default class DiscoverView extends Vue { |
|
|
|
return headers; |
|
|
|
} |
|
|
|
|
|
|
|
public async search(beforeId?: string) { |
|
|
|
public async searchAll(beforeId?: string) { |
|
|
|
let queryParams = "claimContents=" + encodeURIComponent(this.searchTerms); |
|
|
|
|
|
|
|
if (beforeId) { |
|
|
|
queryParams = queryParams + `&beforeId=${beforeId}`; |
|
|
|
} |
|
|
|
|
|
|
|
this.isRemoteActive = true; |
|
|
|
this.isLocalActive = false; |
|
|
|
|
|
|
|
try { |
|
|
|
this.isLoading = true; |
|
|
|
const response = await fetch( |
|
|
@ -202,12 +312,13 @@ export default class DiscoverView extends Vue { |
|
|
|
|
|
|
|
if (response.status !== 200) { |
|
|
|
const details = await response.text(); |
|
|
|
console.log("Problem with full search:", details); |
|
|
|
this.$notify( |
|
|
|
{ |
|
|
|
group: "alert", |
|
|
|
type: "danger", |
|
|
|
title: "Error", |
|
|
|
text: `There was a problem accessing the server. Please try again later. (${details})`, |
|
|
|
text: `There was a problem accessing the server. Try again later.`, |
|
|
|
}, |
|
|
|
-1, |
|
|
|
); |
|
|
@ -220,14 +331,15 @@ export default class DiscoverView extends Vue { |
|
|
|
const plans: ProjectData[] = results.data; |
|
|
|
if (plans) { |
|
|
|
for (const plan of plans) { |
|
|
|
const { name, description, handleId, rowid, issuerDid } = plan; |
|
|
|
this.projects.push({ name, description, handleId, rowid, issuerDid }); |
|
|
|
const { name, description, handleId, rowid } = plan; |
|
|
|
this.projects.push({ name, description, handleId, rowid }); |
|
|
|
} |
|
|
|
this.remoteCount = this.projects.length; |
|
|
|
} else { |
|
|
|
throw JSON.stringify(results); |
|
|
|
} |
|
|
|
} catch (e) { |
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any |
|
|
|
} catch (e: any) { |
|
|
|
console.log("Error with feed load:", e); |
|
|
|
this.$notify( |
|
|
|
{ |
|
|
@ -244,14 +356,19 @@ export default class DiscoverView extends Vue { |
|
|
|
} |
|
|
|
|
|
|
|
public async searchLocal(beforeId?: string) { |
|
|
|
if (!this.searchBox) { |
|
|
|
this.projects = []; |
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
const claimContents = |
|
|
|
"claimContents=" + encodeURIComponent(this.searchTerms); |
|
|
|
let queryParams = [ |
|
|
|
claimContents, |
|
|
|
"minLocLat=40.901000", |
|
|
|
"maxLocLat=40.904000", |
|
|
|
"westLocLon=-111.914000", |
|
|
|
"eastLocLon=-111.909000", |
|
|
|
"minLocLat=" + this.searchBox.bbox.minLat, |
|
|
|
"maxLocLat=" + this.searchBox.bbox.maxLat, |
|
|
|
"westLocLon=" + this.searchBox.bbox.westLong, |
|
|
|
"eastLocLon=" + this.searchBox.bbox.eastLong, |
|
|
|
].join("&"); |
|
|
|
|
|
|
|
if (beforeId) { |
|
|
@ -260,8 +377,6 @@ export default class DiscoverView extends Vue { |
|
|
|
|
|
|
|
try { |
|
|
|
this.isLoading = true; |
|
|
|
this.isLocalActive = true; |
|
|
|
this.isRemoteActive = false; |
|
|
|
const response = await fetch( |
|
|
|
this.apiServer + "/api/v2/report/plansByLocation?" + queryParams, |
|
|
|
{ |
|
|
@ -272,12 +387,13 @@ export default class DiscoverView extends Vue { |
|
|
|
|
|
|
|
if (response.status !== 200) { |
|
|
|
const details = await response.text(); |
|
|
|
console.log("Problem with nearby search:", details); |
|
|
|
this.$notify( |
|
|
|
{ |
|
|
|
group: "alert", |
|
|
|
type: "danger", |
|
|
|
title: "Error", |
|
|
|
text: `There was a problem accessing the server. Please try again later. (${details})`, |
|
|
|
text: "There was a problem accessing the server. Try again later.", |
|
|
|
}, |
|
|
|
-1, |
|
|
|
); |
|
|
@ -302,7 +418,8 @@ export default class DiscoverView extends Vue { |
|
|
|
} else { |
|
|
|
throw JSON.stringify(results); |
|
|
|
} |
|
|
|
} catch (e) { |
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any |
|
|
|
} catch (e: any) { |
|
|
|
console.log("Error with feed load:", e); |
|
|
|
this.$notify( |
|
|
|
{ |
|
|
@ -328,7 +445,7 @@ export default class DiscoverView extends Vue { |
|
|
|
if (this.isLocalActive) { |
|
|
|
this.searchLocal(latestProject["rowid"]); |
|
|
|
} else if (this.isRemoteActive) { |
|
|
|
this.search(latestProject["rowid"]); |
|
|
|
this.searchAll(latestProject["rowid"]); |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
@ -345,6 +462,128 @@ export default class DiscoverView extends Vue { |
|
|
|
this.$router.push(route); |
|
|
|
} |
|
|
|
|
|
|
|
setMapPoint(event: LeafletMouseEvent) { |
|
|
|
if (this.isNewMarkerSet) { |
|
|
|
this.localLatDiff = Math.abs(event.latlng.lat - this.localCenterLat); |
|
|
|
this.localLongDiff = Math.abs(event.latlng.lng - this.localCenterLong); |
|
|
|
} else { |
|
|
|
// marker is not set |
|
|
|
this.localCenterLat = event.latlng.lat; |
|
|
|
this.localCenterLong = event.latlng.lng; |
|
|
|
|
|
|
|
let latDiff = DEFAULT_LAT_LONG_DIFF; |
|
|
|
let longDiff = DEFAULT_LAT_LONG_DIFF; |
|
|
|
// Guess at a size for the bounding box. |
|
|
|
// This doesn't seem like the right approach but it's the only way I can find to get the screen bounds. |
|
|
|
const bounds = event.target.boxZoom?._map?.getBounds(); |
|
|
|
if (bounds) { |
|
|
|
latDiff = Math.abs(bounds._northEast.lat - bounds._southWest.lat) / 8; |
|
|
|
longDiff = Math.abs(bounds._northEast.lng - bounds._southWest.lng) / 8; |
|
|
|
} |
|
|
|
this.localLatDiff = latDiff; |
|
|
|
this.localLongDiff = longDiff; |
|
|
|
this.isNewMarkerSet = true; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
public resetLatLong() { |
|
|
|
if (this.searchBox?.bbox) { |
|
|
|
const bbox = this.searchBox.bbox; |
|
|
|
this.localCenterLat = (bbox.maxLat + bbox.minLat) / 2; |
|
|
|
this.localCenterLong = (bbox.eastLong + bbox.westLong) / 2; |
|
|
|
this.localLatDiff = (bbox.maxLat - bbox.minLat) / 2; |
|
|
|
this.localLongDiff = (bbox.eastLong - bbox.westLong) / 2; |
|
|
|
this.localZoom = WORLD_ZOOM; |
|
|
|
this.isNewMarkerSet = true; |
|
|
|
} else { |
|
|
|
this.isNewMarkerSet = false; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
public async storeSearchBox() { |
|
|
|
if (this.localCenterLong || this.localCenterLat) { |
|
|
|
try { |
|
|
|
const newSearchBox = { |
|
|
|
name: "Local", |
|
|
|
bbox: { |
|
|
|
eastLong: this.localCenterLong + this.localLongDiff, |
|
|
|
maxLat: this.localCenterLat + this.localLatDiff, |
|
|
|
minLat: this.localCenterLat - this.localLatDiff, |
|
|
|
westLong: this.localCenterLong - this.localLongDiff, |
|
|
|
}, |
|
|
|
}; |
|
|
|
await db.open(); |
|
|
|
db.settings.update(MASTER_SETTINGS_KEY, { |
|
|
|
searchBoxes: [newSearchBox], |
|
|
|
}); |
|
|
|
this.searchBox = newSearchBox; |
|
|
|
this.isChoosingSearchBox = false; |
|
|
|
this.searchLocal(); |
|
|
|
} catch (err) { |
|
|
|
this.$notify( |
|
|
|
{ |
|
|
|
group: "alert", |
|
|
|
type: "danger", |
|
|
|
title: "Error Updating Search Settings", |
|
|
|
text: "Try going to a different page and then coming back.", |
|
|
|
}, |
|
|
|
-1, |
|
|
|
); |
|
|
|
console.error( |
|
|
|
"Telling user to retry the location search setting because:", |
|
|
|
err, |
|
|
|
); |
|
|
|
} |
|
|
|
} else { |
|
|
|
this.$notify( |
|
|
|
{ |
|
|
|
group: "alert", |
|
|
|
type: "warning", |
|
|
|
title: "No Location Selected", |
|
|
|
text: "Select a location on the map.", |
|
|
|
}, |
|
|
|
-1, |
|
|
|
); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
public async forgetSearchBox() { |
|
|
|
try { |
|
|
|
await db.open(); |
|
|
|
db.settings.update(MASTER_SETTINGS_KEY, { |
|
|
|
searchBoxes: [], |
|
|
|
}); |
|
|
|
this.searchBox = null; |
|
|
|
this.localCenterLat = 0; |
|
|
|
this.localCenterLong = 0; |
|
|
|
this.localLatDiff = DEFAULT_LAT_LONG_DIFF; |
|
|
|
this.localLongDiff = DEFAULT_LAT_LONG_DIFF; |
|
|
|
this.localZoom = DEFAULT_ZOOM; |
|
|
|
this.isChoosingSearchBox = false; |
|
|
|
this.isNewMarkerSet = false; |
|
|
|
this.searchLocal(); |
|
|
|
} catch (err) { |
|
|
|
this.$notify( |
|
|
|
{ |
|
|
|
group: "alert", |
|
|
|
type: "danger", |
|
|
|
title: "Error Updating Search Settings", |
|
|
|
text: "Try going to a different page and then coming back.", |
|
|
|
}, |
|
|
|
-1, |
|
|
|
); |
|
|
|
console.error( |
|
|
|
"Telling user to retry the location search setting because:", |
|
|
|
err, |
|
|
|
); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
public cancelSearchBoxSelect() { |
|
|
|
this.isChoosingSearchBox = false; |
|
|
|
this.localZoom = WORLD_ZOOM; |
|
|
|
} |
|
|
|
|
|
|
|
public computedLocalTabClassNames() { |
|
|
|
return { |
|
|
|
"inline-block": true, |
|
|
|