file: create client directory with files

This commit is contained in:
Leon Xu
2022-01-08 16:48:25 -08:00
parent 19b420d351
commit 353dbbd0a5
25 changed files with 21178 additions and 0 deletions
+4
View File
@@ -0,0 +1,4 @@
> 1%
last 2 versions
not dead
not ie 11
+17
View File
@@ -0,0 +1,17 @@
module.exports = {
root: true,
env: {
node: true
},
'extends': [
'plugin:vue/vue3-essential',
'eslint:recommended'
],
parserOptions: {
parser: '@babel/eslint-parser'
},
rules: {
'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off'
}
}
+25
View File
@@ -0,0 +1,25 @@
.DS_Store
node_modules
/dist
# local env files
.env
.env.local
.env.*.local
# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
node_modules
+24
View File
@@ -0,0 +1,24 @@
# client
## Project setup
```
npm install
```
### Compiles and hot-reloads for development
```
npm run serve
```
### Compiles and minifies for production
```
npm run build
```
### Lints and fixes files
```
npm run lint
```
### Customize configuration
See [Configuration Reference](https://cli.vuejs.org/config/).
+5
View File
@@ -0,0 +1,5 @@
module.exports = {
presets: [
'@vue/cli-plugin-babel/preset'
]
}
+19
View File
@@ -0,0 +1,19 @@
{
"compilerOptions": {
"target": "es5",
"module": "esnext",
"baseUrl": "./",
"moduleResolution": "node",
"paths": {
"@/*": [
"src/*"
]
},
"lib": [
"esnext",
"dom",
"dom.iterable",
"scripthost"
]
}
}
+20505
View File
File diff suppressed because it is too large Load Diff
+30
View File
@@ -0,0 +1,30 @@
{
"name": "client",
"version": "0.1.0",
"private": true,
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint"
},
"dependencies": {
"axios": "^0.24.0",
"core-js": "^3.8.3",
"leaflet": "^1.7.1",
"vue": "^3.2.13",
"vue-router": "^4.0.3"
},
"devDependencies": {
"@babel/core": "^7.12.16",
"@babel/eslint-parser": "^7.12.16",
"@vue/cli-plugin-babel": "~5.0.0-rc.1",
"@vue/cli-plugin-eslint": "~5.0.0-rc.1",
"@vue/cli-plugin-router": "~5.0.0-rc.1",
"@vue/cli-service": "~5.0.0-rc.1",
"autoprefixer": "^10.4.2",
"eslint": "^7.32.0",
"eslint-plugin-vue": "^8.0.3",
"postcss": "^8.4.5",
"tailwindcss": "^3.0.12"
}
}
+6
View File
@@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

+32
View File
@@ -0,0 +1,32 @@
<!DOCTYPE html>
<html lang="">
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
<link rel="icon" href="<%= BASE_URL %>favicon.ico" />
<link
rel="stylesheet"
href="https://unpkg.com/leaflet@1.7.1/dist/leaflet.css"
integrity="sha512-xodZBNTC5n17Xt2atTPuE1HxjVMSvLVW9ocqUKLsCC5CXdbqCmblAshOMAS6/keqq/sMZMZ19scR4PsZChSR7A=="
crossorigin=""
/>
<link
rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/css/all.min.css"
crossorigin="anonymous"
referrerpolicy="no-referrer"
/>
<title>Geocoding App</title>
</head>
<body>
<noscript>
<strong
>We're sorry but Geocoding App doesn't work properly without JavaScript
enabled. Please enable it to continue.</strong
>
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>
+4
View File
@@ -0,0 +1,4 @@
<template>
<nav></nav>
<router-view />
</template>
Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

+1
View File
@@ -0,0 +1 @@
<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="map-marker-alt" class="svg-inline--fa fa-map-marker-alt fa-w-12" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 384 512"><path fill="#0000FF" d="M172.268 501.67C26.97 291.031 0 269.413 0 192 0 85.961 85.961 0 192 0s192 85.961 192 192c0 77.413-26.97 99.031-172.268 309.67-9.535 13.774-29.93 13.773-39.464 0zM192 272c44.183 0 80-35.817 80-80s-35.817-80-80-80-80 35.817-80 80 35.817 80 80 80z"></path></svg>

After

Width:  |  Height:  |  Size: 487 B

+1
View File
@@ -0,0 +1 @@
<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="map-marker-alt" class="svg-inline--fa fa-map-marker-alt fa-w-12" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 384 512"><path fill="#FF0000" d="M172.268 501.67C26.97 291.031 0 269.413 0 192 0 85.961 85.961 0 192 0s192 85.961 192 192c0 77.413-26.97 99.031-172.268 309.67-9.535 13.774-29.93 13.773-39.464 0zM192 272c44.183 0 80-35.817 80-80s-35.817-80-80-80-80 35.817-80 80 35.817 80 80 80z"></path></svg>

After

Width:  |  Height:  |  Size: 487 B

+3
View File
@@ -0,0 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
+21
View File
@@ -0,0 +1,21 @@
<template>
<div class="h-full w-full flex items-center justify-center">
<span
class="
block
w-[60px]
h-[60px]
border-[5px] border-solid border-transparent border-t-black
rounded-full
animate-spin
"
>
</span>
</div>
</template>
<script>
export default {
name: "loadingSpinner",
};
</script>
+37
View File
@@ -0,0 +1,37 @@
<template>
<div
class="
h-screen
absolute
w-full
z-10
flex
justify-center
items-start
pt-[125px]
bg-black/50
"
>
<div
class="flex flex-col bg-white w-[80%] sm:w-[450px] px-6 py-4 rounded-md"
>
<h1 class="text-lg mb-1">Error: {{ this.geoErrorMsg }}</h1>
<p class="text-sm mb-4">
To take advantage of this applications features, please ensure location
services are enabled
</p>
<button
@click="$emit('closeGeoError')"
class="self-start py-2 px-4 bg-red-500 text-white rounded-md text-sm"
>
Close
</button>
</div>
</div>
</template>
<script>
export default {
props: ["geoErrorMsg"],
};
</script>
+59
View File
@@ -0,0 +1,59 @@
<template>
<div class="hello">
<h1>{{ msg }}</h1>
<p>
For a guide and recipes on how to configure / customize this project,<br>
check out the
<a href="https://cli.vuejs.org" target="_blank" rel="noopener">vue-cli documentation</a>.
</p>
<h3>Installed CLI Plugins</h3>
<ul>
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-babel" target="_blank" rel="noopener">babel</a></li>
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-router" target="_blank" rel="noopener">router</a></li>
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-eslint" target="_blank" rel="noopener">eslint</a></li>
</ul>
<h3>Essential Links</h3>
<ul>
<li><a href="https://vuejs.org" target="_blank" rel="noopener">Core Docs</a></li>
<li><a href="https://forum.vuejs.org" target="_blank" rel="noopener">Forum</a></li>
<li><a href="https://chat.vuejs.org" target="_blank" rel="noopener">Community Chat</a></li>
<li><a href="https://twitter.com/vuejs" target="_blank" rel="noopener">Twitter</a></li>
<li><a href="https://news.vuejs.org" target="_blank" rel="noopener">News</a></li>
</ul>
<h3>Ecosystem</h3>
<ul>
<li><a href="https://router.vuejs.org" target="_blank" rel="noopener">vue-router</a></li>
<li><a href="https://vuex.vuejs.org" target="_blank" rel="noopener">vuex</a></li>
<li><a href="https://github.com/vuejs/vue-devtools#vue-devtools" target="_blank" rel="noopener">vue-devtools</a></li>
<li><a href="https://vue-loader.vuejs.org" target="_blank" rel="noopener">vue-loader</a></li>
<li><a href="https://github.com/vuejs/awesome-vue" target="_blank" rel="noopener">awesome-vue</a></li>
</ul>
</div>
</template>
<script>
export default {
name: 'HelloWorld',
props: {
msg: String
}
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
h3 {
margin: 40px 0 0;
}
ul {
list-style-type: none;
padding: 0;
}
li {
display: inline-block;
margin: 0 10px;
}
a {
color: #42b983;
}
</style>
+154
View File
@@ -0,0 +1,154 @@
<template>
<div
class="
w-full
md:w-auto
absolute
md:top-[40px] md:left-[60px]
z-[2]
flex
gap-4
px-6
py-8
md:px-0 md:py-0
bg-transparent
"
>
<!-- Search -->
<div class="relative flex-1 md:min-w-[350px]">
<!-- Search Input -->
<input
class="
pl-9
pr-4
py-3
text-[14px]
focus:outline-none
w-full
shadow-md
rounded-md
"
type="text"
placeholder="Start your search..."
v-model="searchQuery"
@input="search"
@focus="$emit('toggleSearchResults')"
/>
<!-- Search Icon -->
<div class="absolute top-0 left-[8px] h-full flex items-center">
<i class="fas fa-search"></i>
</div>
<!-- Search Results -->
<div class="absolute mt-[8px] w-full">
<!-- Search Queries -->
<div
v-if="searchQuery && searchResults"
class="h-[200px] overflow-scroll bg-white rounded-md"
>
<!-- Loading Spinner -->
<BaseLoader v-if="!searchData" />
<!-- Display Results -->
<div v-else>
<div
class="
px-4
py-2
flex
gap-x-2
cursor-pointer
hover:bg-slate-600 hover:text-white
"
v-for="(result, index) in searchData"
:key="index"
@click="selectResult(result)"
>
<i class="fas fa-map-marker-alt"></i>
<p class="text-[12px]">{{ result.place_name_en }}</p>
</div>
</div>
</div>
<!-- Selected Search Result -->
<div
v-if="selectedResult"
class="mt-[8px] px-4 py-3 bg-white rounded-md"
>
<i
@click="removeResult"
class="flex justify-end far fa-times-circle"
></i>
<h1 class="text-lg">{{ selectedResult.text }}</h1>
<p class="text-xs mb-1">
{{ selectedResult.properties.address }}, {{ selectedResult.city }},
{{ selectedResult.state }}
</p>
<p class="text-xs">{{ selectedResult.properties.category }}</p>
</div>
</div>
</div>
<!-- Geolocation -->
<div
class="px-4 bg-white flex items-center shadow-md rounded-md min-h-[45px]"
@click="$emit('getGeolocation')"
:class="{ 'bg-slate-600': coords }"
>
<i
class="fas fa-location-arrow 'text-slate-600' text-[18px]"
:class="{ 'text-white': coords, 'animate-pulse': fetchCoords }"
></i>
</div>
</div>
</template>
<script>
import { ref } from "vue";
import axios from "axios";
import BaseLoader from "./BaseLoader.vue";
export default {
props: ["fetchCoords", "coords", "searchResults"],
components: { BaseLoader },
setup(props, { emit }) {
const searchQuery = ref(null);
const searchData = ref(null);
const queryTimeout = ref(null);
const selectedResult = ref(null);
const search = () => {
clearTimeout(queryTimeout.value);
// reset data on a new search
searchData.value = null;
queryTimeout.value = setTimeout(async () => {
// Only make search, if there is value in query input
if (searchQuery.value !== "") {
const params = new URLSearchParams({
fuzzyMatch: true,
language: "en",
limit: 10,
proximity: props.coords
? `${props.coords.lng},${props.coords.lat}`
: "0,0",
});
const data = await axios.get(
`api/search/${searchQuery.value}?${params}`
);
searchData.value = data.data.features;
}
}, 750);
};
const selectResult = (result) => {
selectedResult.value = result;
emit("plotResult", result.geometry);
};
const removeResult = () => {
selectedResult.value = null;
emit("removeResult");
};
return {
searchQuery,
search,
searchData,
selectResult,
selectedResult,
removeResult,
};
},
};
</script>
+6
View File
@@ -0,0 +1,6 @@
import { createApp } from "vue";
import App from "./App.vue";
import router from "./router";
import "./assets/tailwind.css";
createApp(App).use(router).mount("#app");
+17
View File
@@ -0,0 +1,17 @@
import { createRouter, createWebHistory } from "vue-router";
import HomeView from "../views/HomeView.vue";
const routes = [
{
path: "/",
name: "home",
component: HomeView,
},
];
const router = createRouter({
history: createWebHistory(process.env.BASE_URL),
routes,
});
export default router;
+187
View File
@@ -0,0 +1,187 @@
<template>
<div class="h-screen relative">
<BaseModal
@closeGeoError="closeGeoError"
v-if="geoError"
:geoErrorMsg="geoErrorMsg"
/>
<MapFeatures
@getGeolocation="getGeolocation"
@plotResult="plotResult"
@toggleSearchResults="toggleSearchResults"
@removeResult="removeResult"
:coords="coords"
:fetchCoords="fetchCoords"
:searchResults="searchResults"
/>
<div id="map" class="h-full z-[1]"></div>
</div>
</template>
<script>
import leaflet from "leaflet";
import { onMounted, ref } from "vue";
import BaseModal from "@/components/BaseModal.vue";
import MapFeatures from "@/components/MapFeatures.vue";
export default {
name: "HomeView",
components: {
BaseModal,
MapFeatures,
},
setup() {
let map;
onMounted(() => {
// init map
map = leaflet.map("map").setView([47.60357, -122.329449], 10);
// add tile layer
leaflet
.tileLayer(
`https://api.mapbox.com/styles/v1/{id}/tiles/{z}/{x}/{y}?access_token=${process.env.VUE_APP_API_KEY}`,
{
attribution:
'Map data &copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors, Imagery © <a href="https://www.mapbox.com/">Mapbox</a>',
maxZoom: 18,
id: "mapbox/streets-v11",
tileSize: 512,
zoomOffset: -1,
accessToken: process.env.VUE_APP_API_KEY,
}
)
.addTo(map);
map.on("moveend", () => {
closeSearchResults();
});
getGeolocation();
});
const coords = ref(null);
const fetchCoords = ref(null);
const geoMarker = ref(null);
const geoError = ref(null);
const geoErrorMsg = ref(null);
const getGeolocation = () => {
if (!coords.value) {
// check session storage for coords
if (sessionStorage.getItem("coords")) {
coords.value = JSON.parse(sessionStorage.getItem("coords"));
plotGeoLocation(coords.value);
return;
}
fetchCoords.value = true;
navigator.geolocation.getCurrentPosition(setCoords, getLocError);
}
coords.value = null;
sessionStorage.removeItem("coords");
map.removeLayer(geoMarker.value);
return;
};
const setCoords = (pos) => {
// stop fetching coords
fetchCoords.value = null;
// set coords in session storage
const setSessionCoords = {
lat: pos.coords.latitude,
lng: pos.coords.longitude,
};
sessionStorage.setItem("coords", JSON.stringify(setSessionCoords));
// set ref coords value
coords.value = setSessionCoords;
plotGeoLocation(coords.value);
};
const getLocError = (err) => {
fetchCoords.value = null;
geoError.value = true;
geoErrorMsg.value = err.message;
};
const plotGeoLocation = (coords) => {
// create custom marker
const customMarker = leaflet.icon({
iconUrl: require("../assets/map-marker-red.svg"),
iconSize: [35, 35],
});
// create new marker with coords and custom icon
geoMarker.value = leaflet
.marker([coords.lat, coords.lng], {
icon: customMarker,
})
.addTo(map);
// setting map view to current location
map.setView([coords.lat, coords.lng], 10);
};
const closeGeoError = () => {
geoError.value = null;
geoErrorMsg.value = null;
};
const resultMarker = ref(null);
const plotResult = (coords) => {
// Check to see if resultMarker has value
if (resultMarker.value) {
map.removeLayer(resultMarker.value);
}
// create custom marker
const customMarker = leaflet.icon({
iconUrl: require("../assets/map-marker-blue.svg"),
iconSize: [35, 35],
});
// create new marker with coords and custom icon
resultMarker.value = leaflet
.marker([coords.coordinates[1], coords.coordinates[0]], {
icon: customMarker,
})
.addTo(map);
// setting map view to current location
map.setView([coords.coordinates[1], coords.coordinates[0]], 14);
};
const searchResults = ref(null);
const toggleSearchResults = () => {
searchResults.value = !searchResults.value;
};
const closeSearchResults = () => {
searchResults.value = null;
};
const removeResult = () => {
map.removeLayer(resultMarker.value);
};
return {
coords,
fetchCoords,
geoMarker,
closeGeoError,
geoError,
geoErrorMsg,
getGeolocation,
plotResult,
searchResults,
toggleSearchResults,
closeSearchResults,
removeResult,
};
},
};
</script>
+8
View File
@@ -0,0 +1,8 @@
module.exports = {
content: ["./index.html", "./src/**/*.{vue,js,ts,jsx,tsx}"],
jit: true,
theme: {
extend: {},
},
plugins: [],
};
+13
View File
@@ -0,0 +1,13 @@
const { defineConfig } = require("@vue/cli-service");
const path = require("path");
module.exports = defineConfig({
transpileDependencies: true,
outputDir: path.resolve(__dirname, "../server/public"),
devServer: {
proxy: {
"/api": {
target: "http://localhost:3000",
},
},
},
});