Merge pull request #5843 from Ocelot-Social-Community/map
feat(webapp): map
@ -85,7 +85,7 @@ class Store {
|
||||
await createDefaultAdminUser(session)
|
||||
if (CONFIG.CATEGORIES_ACTIVE) await createCategories(session)
|
||||
const writeTxResultPromise = session.writeTransaction(async (txc) => {
|
||||
await txc.run('CALL apoc.schema.assert({},{},true)') // drop all indices and contraints
|
||||
await txc.run('CALL apoc.schema.assert({},{},true)') // drop all indices and constraints
|
||||
return Promise.all(
|
||||
[
|
||||
'CALL db.index.fulltext.createNodeIndex("user_fulltext_search",["User"],["name", "slug"])',
|
||||
|
||||
@ -20,7 +20,6 @@ services:
|
||||
- GRAPHQL_URI=http://localhost:4000
|
||||
- CLIENT_URI=http://localhost:3000
|
||||
- JWT_SECRET=b/&&7b78BF&fv/Vd
|
||||
- MAPBOX_TOKEN=pk.eyJ1IjoiYnVzZmFrdG9yIiwiYSI6ImNraDNiM3JxcDBhaWQydG1uczhpZWtpOW4ifQ.7TNRTO-o9aK1Y6MyW_Nd4g
|
||||
- PRIVATE_KEY_PASSPHRASE=a7dsf78sadg87ad87sfagsadg78
|
||||
- NEO4J_apoc_import_file_enabled=true
|
||||
- "SSH_USERNAME=${SSH_USERNAME}"
|
||||
|
||||
@ -98,22 +98,22 @@ On a server with Kubernetes cluster:
|
||||
$ kubectl -n default exec -it $(kubectl -n default get pods | grep ocelot-backend | awk '{ print $1 }') -- /bin/sh -c "yarn prod:migrate init"
|
||||
```
|
||||
|
||||
***Cypher commands to show indexes and contraints***
|
||||
***Cypher commands to show indexes and constraints***
|
||||
|
||||
```bash
|
||||
# in browser command line or cypher shell
|
||||
|
||||
# show all indexes and contraints
|
||||
# show all indexes and constraints
|
||||
$ :schema
|
||||
|
||||
# show all indexes
|
||||
$ CALL db.indexes();
|
||||
|
||||
# show all contraints
|
||||
# show all constraints
|
||||
$ CALL db.constraints();
|
||||
```
|
||||
|
||||
***Cypher commands to create and drop indexes and contraints***
|
||||
***Cypher commands to create and drop indexes and constraints***
|
||||
|
||||
```bash
|
||||
# in browser command line or cypher shell
|
||||
@ -126,6 +126,6 @@ $ CALL db.index.fulltext.createNodeIndex("tag_fulltext_search",["Tag"],["id"]);
|
||||
# drop an index
|
||||
$ DROP CONSTRAINT ON ( image:Image ) ASSERT image.url IS UNIQUE
|
||||
|
||||
# drop all indexes and contraints
|
||||
# drop all indexes and constraints
|
||||
$ CALL apoc.schema.assert({},{},true) YIELD label, key RETURN * ;
|
||||
```
|
||||
|
||||
@ -1,7 +1,8 @@
|
||||
SENTRY_DSN_WEBAPP=
|
||||
COMMIT=
|
||||
PUBLIC_REGISTRATION=false
|
||||
INVITE_REGISTRATION=true
|
||||
WEBSOCKETS_URI=ws://localhost:3000/api/graphql
|
||||
GRAPHQL_URI=http://localhost:4000/
|
||||
MAPBOX_TOKEN="pk.eyJ1IjoiYnVzZmFrdG9yIiwiYSI6ImNraDNiM3JxcDBhaWQydG1uczhpZWtpOW4ifQ.7TNRTO-o9aK1Y6MyW_Nd4g"
|
||||
PUBLIC_REGISTRATION=false
|
||||
INVITE_REGISTRATION=true
|
||||
CATEGORIES_ACTIVE=false
|
||||
|
||||
1
webapp/assets/_new/icons/svgs/globe-detailed.svg
Normal file
|
After Width: | Height: | Size: 121 KiB |
@ -3,28 +3,28 @@
|
||||
* @presenter Color
|
||||
*/
|
||||
|
||||
$color-primary: rgb(23, 181, 63);
|
||||
$color-primary-light: rgb(96, 214, 98);
|
||||
$color-primary-dark: rgb(25, 122, 49);
|
||||
$color-primary-active: rgb(25, 194, 67);
|
||||
$color-primary-inverse: rgb(241, 253, 244);
|
||||
$color-secondary: rgb(0, 142, 230);
|
||||
$color-secondary-active: rgb(10, 161, 255);
|
||||
$color-secondary-inverse: rgb(240, 249, 255);
|
||||
$color-success: rgb(23, 181, 63);
|
||||
$color-success-active: rgb(26, 203, 71);
|
||||
$color-success-inverse: rgb(241, 253, 244);
|
||||
$color-danger: rgb(219, 57, 36);
|
||||
$color-danger-light: rgb(242, 97, 65);
|
||||
$color-danger-dark: rgb(158, 43, 28);
|
||||
$color-danger-active: rgb(224, 81, 62);
|
||||
$color-danger-inverse: rgb(253, 243, 242);
|
||||
$color-warning: rgb(230, 121, 25);
|
||||
$color-warning-active: rgb(233, 137, 53);
|
||||
$color-warning-inverse: rgb(253, 247, 241);
|
||||
$color-yellow: rgb(245, 196, 0);
|
||||
$color-yellow-active: rgb(255, 206, 10);
|
||||
$color-yellow-inverse: rgb(255, 252, 240);
|
||||
$color-primary: rgb(23, 181, 63);
|
||||
$color-primary-light: rgb(96, 214, 98);
|
||||
$color-primary-dark: rgb(25, 122, 49);
|
||||
$color-primary-active: rgb(25, 194, 67);
|
||||
$color-primary-inverse: rgb(241, 253, 244);
|
||||
$color-secondary: rgb(0, 142, 230);
|
||||
$color-secondary-active: rgb(10, 161, 255);
|
||||
$color-secondary-inverse: rgb(240, 249, 255);
|
||||
$color-success: rgb(23, 181, 63);
|
||||
$color-success-active: rgb(26, 203, 71);
|
||||
$color-success-inverse: rgb(241, 253, 244);
|
||||
$color-danger: rgb(219, 57, 36);
|
||||
$color-danger-light: rgb(242, 97, 65);
|
||||
$color-danger-dark: rgb(158, 43, 28);
|
||||
$color-danger-active: rgb(224, 81, 62);
|
||||
$color-danger-inverse: rgb(253, 243, 242);
|
||||
$color-warning: rgb(230, 121, 25);
|
||||
$color-warning-active: rgb(233, 137, 53);
|
||||
$color-warning-inverse: rgb(253, 247, 241);
|
||||
$color-yellow: rgb(245, 196, 0);
|
||||
$color-yellow-active: rgb(255, 206, 10);
|
||||
$color-yellow-inverse: rgb(255, 252, 240);
|
||||
|
||||
/**
|
||||
* @tokens Color Neutral
|
||||
|
||||
@ -1,5 +1,19 @@
|
||||
<template>
|
||||
<div>
|
||||
<nuxt-link to="/groups"><base-button icon="users" circle ghost /></nuxt-link>
|
||||
<nuxt-link to="/groups">
|
||||
<base-button
|
||||
icon="users"
|
||||
circle
|
||||
ghost
|
||||
v-tooltip="{
|
||||
content: $t('group.button.tooltip'),
|
||||
placement: 'bottom-start',
|
||||
}"
|
||||
/>
|
||||
</nuxt-link>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {}
|
||||
</script>
|
||||
|
||||
@ -142,6 +142,9 @@
|
||||
style="position: relative; display: inline-block; right: -96%; top: -33px; width: 26px"
|
||||
@click="formData.locationName = ''"
|
||||
></base-button>
|
||||
<ds-text class="location-hint" color="softer">
|
||||
{{ $t('settings.data.labelCityHint') }}
|
||||
</ds-text>
|
||||
|
||||
<ds-space margin-top="small" />
|
||||
|
||||
@ -436,5 +439,9 @@ export default {
|
||||
align-self: flex-end;
|
||||
margin-top: $space-base;
|
||||
}
|
||||
|
||||
> .location-hint {
|
||||
margin-top: -$space-base + $space-xxx-small;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -20,7 +20,7 @@
|
||||
<logo logoType="header" />
|
||||
</nuxt-link>
|
||||
</ds-flex-item>
|
||||
<!-- dynamic-brand-menu -->
|
||||
<!-- dynamic brand menus -->
|
||||
<ds-flex-item
|
||||
v-for="item in menu"
|
||||
:key="item.name"
|
||||
@ -39,8 +39,7 @@
|
||||
</ds-text>
|
||||
</nuxt-link>
|
||||
</ds-flex-item>
|
||||
|
||||
<!-- search-field -->
|
||||
<!-- search field -->
|
||||
<ds-flex-item
|
||||
v-if="isLoggedIn"
|
||||
id="nav-search-box"
|
||||
@ -55,26 +54,26 @@
|
||||
>
|
||||
<search-field />
|
||||
</ds-flex-item>
|
||||
<!-- filter-menu
|
||||
TODO: Filter is only visible on index
|
||||
-->
|
||||
<!-- filter menu -->
|
||||
<!-- TODO: Filter is only visible on index -->
|
||||
<ds-flex-item v-if="isLoggedIn" style="flex-grow: 0; flex-basis: auto">
|
||||
<client-only>
|
||||
<filter-menu v-show="showFilterMenuDropdown" />
|
||||
</client-only>
|
||||
</ds-flex-item>
|
||||
<!-- locale-switch -->
|
||||
<!-- right symbols -->
|
||||
<ds-flex-item style="flex-basis: auto">
|
||||
<div class="main-navigation-right" style="flex-basis: auto">
|
||||
<!-- locale switch -->
|
||||
<locale-switch class="topbar-locale-switch" placement="top" offset="8" />
|
||||
<template v-if="isLoggedIn">
|
||||
<!-- notification menu -->
|
||||
<client-only>
|
||||
<!-- notification-menu -->
|
||||
<notification-menu placement="top" />
|
||||
</client-only>
|
||||
<!-- invite button -->
|
||||
<div v-if="inviteRegistration">
|
||||
<client-only>
|
||||
<!-- invite-button -->
|
||||
<invite-button placement="top" />
|
||||
</client-only>
|
||||
</div>
|
||||
@ -82,7 +81,11 @@
|
||||
<client-only v-if="SHOW_GROUP_BUTTON_IN_HEADER">
|
||||
<group-button />
|
||||
</client-only>
|
||||
<!-- avatar-menu -->
|
||||
<!-- map button -->
|
||||
<client-only v-if="!isEmpty(this.$env.MAPBOX_TOKEN)">
|
||||
<map-button />
|
||||
</client-only>
|
||||
<!-- avatar menu -->
|
||||
<client-only>
|
||||
<avatar-menu placement="top" />
|
||||
</client-only>
|
||||
@ -122,9 +125,9 @@
|
||||
<base-button icon="bars" @click="toggleMobileMenuView" circle />
|
||||
</ds-flex-item>
|
||||
</ds-flex>
|
||||
<!-- search, filter-->
|
||||
<!-- search, filter -->
|
||||
<ds-flex class="mobile-menu">
|
||||
<!-- search-field mobile-->
|
||||
<!-- search field mobile -->
|
||||
<ds-flex-item
|
||||
v-if="isLoggedIn"
|
||||
:class="{ 'hide-mobile-menu': !toggleMobileMenu }"
|
||||
@ -132,7 +135,7 @@
|
||||
>
|
||||
<search-field />
|
||||
</ds-flex-item>
|
||||
<!-- filter menu mobile-->
|
||||
<!-- filter menu mobile -->
|
||||
<ds-flex-item
|
||||
v-if="isLoggedIn"
|
||||
:class="{ 'hide-mobile-menu': !toggleMobileMenu }"
|
||||
@ -143,13 +146,17 @@
|
||||
</client-only>
|
||||
</ds-flex-item>
|
||||
</ds-flex>
|
||||
<!-- switch language, notification, invite, profil -->
|
||||
<!-- right symbols -->
|
||||
<ds-flex style="margin: 0 20px">
|
||||
<!-- locale-switch mobile-->
|
||||
<!-- locale switch mobile -->
|
||||
<ds-flex-item :class="{ 'hide-mobile-menu': !toggleMobileMenu }">
|
||||
<locale-switch class="topbar-locale-switch" placement="top" offset="8" />
|
||||
<locale-switch
|
||||
class="topbar-locale-switch topbar-locale-switch-mobile"
|
||||
placement="top"
|
||||
offset="8"
|
||||
/>
|
||||
</ds-flex-item>
|
||||
<!-- invite-button mobile-->
|
||||
<!-- invite button mobile -->
|
||||
<ds-flex-item
|
||||
:class="{ 'hide-mobile-menu': !toggleMobileMenu }"
|
||||
style="text-align: center"
|
||||
@ -160,14 +167,25 @@
|
||||
</ds-flex-item>
|
||||
<!-- group button -->
|
||||
<ds-flex-item
|
||||
v-if="SHOW_GROUP_BUTTON_IN_HEADER"
|
||||
:class="{ 'hide-mobile-menu': !toggleMobileMenu }"
|
||||
style="text-align: center"
|
||||
>
|
||||
<client-only v-if="SHOW_GROUP_BUTTON_IN_HEADER">
|
||||
<client-only>
|
||||
<group-button />
|
||||
</client-only>
|
||||
</ds-flex-item>
|
||||
<!-- avatar-menu mobile-->
|
||||
<!-- map button -->
|
||||
<ds-flex-item
|
||||
v-if="!isEmpty(this.$env.MAPBOX_TOKEN)"
|
||||
:class="{ 'hide-mobile-menu': !toggleMobileMenu }"
|
||||
style="text-align: center"
|
||||
>
|
||||
<client-only>
|
||||
<map-button />
|
||||
</client-only>
|
||||
</ds-flex-item>
|
||||
<!-- avatar menu mobile -->
|
||||
<ds-flex-item :class="{ 'hide-mobile-menu': !toggleMobileMenu }" style="text-align: end">
|
||||
<client-only>
|
||||
<avatar-menu placement="top" />
|
||||
@ -175,7 +193,7 @@
|
||||
</ds-flex-item>
|
||||
</ds-flex>
|
||||
<div :class="{ 'hide-mobile-menu': !toggleMobileMenu }" class="mobile-menu footer-mobile">
|
||||
<!-- dynamic branding menu -->
|
||||
<!-- dynamic branding menus -->
|
||||
<ul v-if="isHeaderMenu" class="dynamic-branding-mobil">
|
||||
<li v-for="item in menu" :key="item.name">
|
||||
<a v-if="item.url" :href="item.url" :target="item.target">
|
||||
@ -204,8 +222,10 @@
|
||||
</div>
|
||||
</ds-container>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex'
|
||||
import isEmpty from 'lodash/isEmpty'
|
||||
import { SHOW_GROUP_BUTTON_IN_HEADER } from '~/constants/groups.js'
|
||||
import LOGOS from '~/constants/logos.js'
|
||||
import headerMenu from '~/constants/headerMenu.js'
|
||||
@ -215,6 +235,7 @@ import GroupButton from '~/components/Group/GroupButton'
|
||||
import InviteButton from '~/components/InviteButton/InviteButton'
|
||||
import LocaleSwitch from '~/components/LocaleSwitch/LocaleSwitch'
|
||||
import Logo from '~/components/Logo/Logo'
|
||||
import MapButton from '~/components/Map/MapButton'
|
||||
import SearchField from '~/components/features/SearchField/SearchField.vue'
|
||||
import NotificationMenu from '~/components/NotificationMenu/NotificationMenu'
|
||||
import links from '~/constants/links.js'
|
||||
@ -228,6 +249,7 @@ export default {
|
||||
InviteButton,
|
||||
LocaleSwitch,
|
||||
Logo,
|
||||
MapButton,
|
||||
NotificationMenu,
|
||||
PageParamsLink,
|
||||
SearchField,
|
||||
@ -237,6 +259,7 @@ export default {
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isEmpty,
|
||||
links,
|
||||
LOGOS,
|
||||
SHOW_GROUP_BUTTON_IN_HEADER,
|
||||
@ -280,6 +303,9 @@ export default {
|
||||
align-self: center;
|
||||
display: inline-flex;
|
||||
}
|
||||
.topbar-locale-switch-mobile {
|
||||
margin-top: $space-xx-small;
|
||||
}
|
||||
.main-navigation-flex {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
<dropdown ref="menu" :placement="placement" :offset="offset">
|
||||
<template #default="{ toggleMenu }">
|
||||
<a class="locale-menu" href="#" @click.prevent="toggleMenu()">
|
||||
<base-icon name="globe" />
|
||||
<!-- <base-icon name="globe" /> -->
|
||||
<span class="label">{{ current.code.toUpperCase() }}</span>
|
||||
<base-icon class="dropdown-arrow" name="angle-down" />
|
||||
</a>
|
||||
|
||||
30
webapp/components/Map/MapButton.vue
Normal file
@ -0,0 +1,30 @@
|
||||
<template>
|
||||
<div>
|
||||
<nuxt-link to="/map">
|
||||
<base-button
|
||||
class="map-button"
|
||||
circle
|
||||
ghost
|
||||
v-tooltip="{
|
||||
content: $t('map.button.tooltip'),
|
||||
placement: 'bottom-start',
|
||||
}"
|
||||
>
|
||||
<base-icon name="globe-detailed" size="large" />
|
||||
</base-button>
|
||||
</nuxt-link>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'MapButton',
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.map-button {
|
||||
margin-left: 4px;
|
||||
margin-right: 4px;
|
||||
}
|
||||
</style>
|
||||
39
webapp/components/Map/MapStylesButtons.vue
Normal file
@ -0,0 +1,39 @@
|
||||
<template>
|
||||
<div>
|
||||
<base-button
|
||||
:class="['map-style-button', actualStyle === style.url ? '' : '--deactivated']"
|
||||
v-for="style in styles"
|
||||
:key="style.title"
|
||||
filled
|
||||
size="small"
|
||||
@click="setStyle(style.url)"
|
||||
>
|
||||
{{ style.title }}
|
||||
</base-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'MapStylesButtons',
|
||||
props: {
|
||||
styles: { type: Array, required: true },
|
||||
actualStyle: { type: String, required: true },
|
||||
setStyle: { type: Function, required: true },
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.map-style-button {
|
||||
position: relative;
|
||||
margin-left: 6px;
|
||||
margin-bottom: 6px;
|
||||
margin-top: 6px;
|
||||
|
||||
&.--deactivated {
|
||||
color: $text-color-base;
|
||||
background-color: $background-color-softer;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -96,6 +96,7 @@ export default {
|
||||
border-radius: $border-radius-x-large;
|
||||
overflow: hidden;
|
||||
font-weight: $font-weight-bold;
|
||||
letter-spacing: $letter-spacing-large;
|
||||
cursor: pointer;
|
||||
|
||||
&.--danger {
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<span v-if="svgIcon" class="base-icon">
|
||||
<component :is="svgIcon" aria-hidden="true" focusable="false" class="svg" />
|
||||
<component :class="['svg', `--${size}`]" :is="svgIcon" aria-hidden="true" focusable="false" />
|
||||
</span>
|
||||
</template>
|
||||
|
||||
@ -16,6 +16,13 @@ export default {
|
||||
return iconNames.includes(value)
|
||||
},
|
||||
},
|
||||
size: {
|
||||
type: String,
|
||||
default: 'regular',
|
||||
validator(value) {
|
||||
return value.match(/^(small|regular|large)$/)
|
||||
},
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
svgIcon() {
|
||||
@ -42,6 +49,19 @@ export default {
|
||||
> .svg {
|
||||
height: 1.2em;
|
||||
fill: currentColor;
|
||||
|
||||
&.--small {
|
||||
height: 0.8em;
|
||||
}
|
||||
|
||||
&.--regular {
|
||||
height: 1.2em;
|
||||
}
|
||||
|
||||
&.--large {
|
||||
margin-left: 4px;
|
||||
height: 2.2em;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -28,6 +28,7 @@ const sentry = {
|
||||
const options = {
|
||||
VERSION: process.env.VERSION || pkg.version,
|
||||
DESCRIPTION: process.env.DESCRIPTION || pkg.description,
|
||||
MAPBOX_TOKEN: process.env.MAPBOX_TOKEN,
|
||||
PUBLIC_REGISTRATION: process.env.PUBLIC_REGISTRATION === 'true' || false,
|
||||
INVITE_REGISTRATION: process.env.INVITE_REGISTRATION !== 'false', // default = true
|
||||
// Cookies
|
||||
|
||||
@ -13,11 +13,19 @@ export const userFragment = gql`
|
||||
}
|
||||
`
|
||||
|
||||
export const locationAndBadgesFragment = (lang) => gql`
|
||||
fragment locationAndBadges on User {
|
||||
export const locationFragment = (lang) => gql`
|
||||
fragment location on User {
|
||||
locationName
|
||||
location {
|
||||
name: name${lang}
|
||||
lng
|
||||
lat
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const badgesFragment = gql`
|
||||
fragment badges on User {
|
||||
badges {
|
||||
id
|
||||
icon
|
||||
|
||||
@ -5,7 +5,8 @@ import {
|
||||
commentFragment,
|
||||
postCountsFragment,
|
||||
userCountsFragment,
|
||||
locationAndBadgesFragment,
|
||||
locationFragment,
|
||||
badgesFragment,
|
||||
tagsCategoriesAndPinnedFragment,
|
||||
} from './Fragments'
|
||||
|
||||
@ -14,7 +15,8 @@ export default (i18n) => {
|
||||
return gql`
|
||||
${userFragment}
|
||||
${userCountsFragment}
|
||||
${locationAndBadgesFragment(lang)}
|
||||
${locationFragment(lang)}
|
||||
${badgesFragment}
|
||||
${postFragment}
|
||||
${postCountsFragment}
|
||||
${tagsCategoriesAndPinnedFragment}
|
||||
@ -28,7 +30,8 @@ export default (i18n) => {
|
||||
author {
|
||||
...user
|
||||
...userCounts
|
||||
...locationAndBadges
|
||||
...location
|
||||
...badges
|
||||
blocked
|
||||
}
|
||||
comments(orderBy: createdAt_asc) {
|
||||
@ -36,7 +39,8 @@ export default (i18n) => {
|
||||
author {
|
||||
...user
|
||||
...userCounts
|
||||
...locationAndBadges
|
||||
...location
|
||||
...badges
|
||||
}
|
||||
}
|
||||
group {
|
||||
@ -54,7 +58,8 @@ export const filterPosts = (i18n) => {
|
||||
return gql`
|
||||
${userFragment}
|
||||
${userCountsFragment}
|
||||
${locationAndBadgesFragment(lang)}
|
||||
${locationFragment(lang)}
|
||||
${badgesFragment}
|
||||
${postFragment}
|
||||
${postCountsFragment}
|
||||
${tagsCategoriesAndPinnedFragment}
|
||||
@ -67,7 +72,8 @@ export const filterPosts = (i18n) => {
|
||||
author {
|
||||
...user
|
||||
...userCounts
|
||||
...locationAndBadges
|
||||
...location
|
||||
...badges
|
||||
}
|
||||
group {
|
||||
id
|
||||
@ -84,7 +90,8 @@ export const profilePagePosts = (i18n) => {
|
||||
return gql`
|
||||
${userFragment}
|
||||
${userCountsFragment}
|
||||
${locationAndBadgesFragment(lang)}
|
||||
${locationFragment(lang)}
|
||||
${badgesFragment}
|
||||
${postFragment}
|
||||
${postCountsFragment}
|
||||
${tagsCategoriesAndPinnedFragment}
|
||||
@ -102,7 +109,8 @@ export const profilePagePosts = (i18n) => {
|
||||
author {
|
||||
...user
|
||||
...userCounts
|
||||
...locationAndBadges
|
||||
...location
|
||||
...badges
|
||||
}
|
||||
group {
|
||||
id
|
||||
@ -127,7 +135,8 @@ export const relatedContributions = (i18n) => {
|
||||
return gql`
|
||||
${userFragment}
|
||||
${userCountsFragment}
|
||||
${locationAndBadgesFragment(lang)}
|
||||
${locationFragment(lang)}
|
||||
${badgesFragment}
|
||||
${postFragment}
|
||||
${postCountsFragment}
|
||||
${tagsCategoriesAndPinnedFragment}
|
||||
@ -140,7 +149,8 @@ export const relatedContributions = (i18n) => {
|
||||
author {
|
||||
...user
|
||||
...userCounts
|
||||
...locationAndBadges
|
||||
...location
|
||||
...badges
|
||||
}
|
||||
relatedContributions(first: 2) {
|
||||
...post
|
||||
@ -149,7 +159,8 @@ export const relatedContributions = (i18n) => {
|
||||
author {
|
||||
...user
|
||||
...userCounts
|
||||
...locationAndBadges
|
||||
...location
|
||||
...badges
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,26 +1,28 @@
|
||||
import gql from 'graphql-tag'
|
||||
import {
|
||||
userCountsFragment,
|
||||
locationAndBadgesFragment,
|
||||
locationFragment,
|
||||
badgesFragment,
|
||||
userFragment,
|
||||
postFragment,
|
||||
commentFragment,
|
||||
} from './Fragments'
|
||||
|
||||
export default (i18n) => {
|
||||
export const profileUserQuery = (i18n) => {
|
||||
const lang = i18n.locale().toUpperCase()
|
||||
return gql`
|
||||
${userFragment}
|
||||
${userCountsFragment}
|
||||
${locationAndBadgesFragment(lang)}
|
||||
${locationFragment(lang)}
|
||||
${badgesFragment}
|
||||
|
||||
query User($id: ID!, $followedByCount: Int, $followingCount: Int) {
|
||||
query User($id: ID!, $followedByCount: Int!, $followingCount: Int!) {
|
||||
User(id: $id) {
|
||||
...user
|
||||
...userCounts
|
||||
...locationAndBadges
|
||||
...location
|
||||
...badges
|
||||
about
|
||||
locationName
|
||||
createdAt
|
||||
followedByCurrentUser
|
||||
isMuted
|
||||
@ -29,12 +31,14 @@ export default (i18n) => {
|
||||
following(first: $followingCount) {
|
||||
...user
|
||||
...userCounts
|
||||
...locationAndBadges
|
||||
...location
|
||||
...badges
|
||||
}
|
||||
followedBy(first: $followedByCount) {
|
||||
...user
|
||||
...userCounts
|
||||
...locationAndBadges
|
||||
...location
|
||||
...badges
|
||||
}
|
||||
socialMedia {
|
||||
id
|
||||
@ -62,6 +66,48 @@ export const minimisedUserQuery = () => {
|
||||
`
|
||||
}
|
||||
|
||||
export const adminUserQuery = () => {
|
||||
return gql`
|
||||
query ($filter: _UserFilter, $first: Int, $offset: Int, $email: String) {
|
||||
User(
|
||||
email: $email
|
||||
filter: $filter
|
||||
first: $first
|
||||
offset: $offset
|
||||
orderBy: createdAt_desc
|
||||
) {
|
||||
id
|
||||
name
|
||||
slug
|
||||
email
|
||||
role
|
||||
createdAt
|
||||
contributionsCount
|
||||
commentedCount
|
||||
shoutedCount
|
||||
}
|
||||
}
|
||||
`
|
||||
}
|
||||
|
||||
export const mapUserQuery = (i18n) => {
|
||||
const lang = i18n.locale().toUpperCase()
|
||||
return gql`
|
||||
${userFragment}
|
||||
${locationFragment(lang)}
|
||||
${badgesFragment}
|
||||
|
||||
query {
|
||||
User {
|
||||
...user
|
||||
about
|
||||
...location
|
||||
...badges
|
||||
}
|
||||
}
|
||||
`
|
||||
}
|
||||
|
||||
export const notificationQuery = (i18n) => {
|
||||
return gql`
|
||||
${userFragment}
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import gql from 'graphql-tag'
|
||||
// import { locationFragment } from './Fragments'
|
||||
|
||||
// ------ mutations
|
||||
|
||||
@ -146,7 +147,9 @@ export const changeGroupMemberRoleMutation = () => {
|
||||
|
||||
export const groupQuery = (i18n) => {
|
||||
const lang = i18n ? i18n.locale().toUpperCase() : 'EN'
|
||||
// ${locationFragment(lang)}
|
||||
return gql`
|
||||
|
||||
query ($isMember: Boolean, $id: ID, $slug: String, $first: Int, $offset: Int) {
|
||||
Group(isMember: $isMember, id: $id, slug: $slug, first: $first, offset: $offset) {
|
||||
id
|
||||
@ -171,8 +174,11 @@ export const groupQuery = (i18n) => {
|
||||
url
|
||||
}
|
||||
locationName
|
||||
# ...location
|
||||
location {
|
||||
name: name${lang}
|
||||
lng
|
||||
lat
|
||||
}
|
||||
myRole
|
||||
}
|
||||
|
||||
@ -1,14 +1,14 @@
|
||||
<template>
|
||||
<div class="layout-default">
|
||||
<div class="main-navigation">
|
||||
<header-menu :showMobileMenu="showMobileMenu" />
|
||||
<header-menu :showMobileMenu="isMobile" />
|
||||
</div>
|
||||
<ds-container>
|
||||
<div class="main-container">
|
||||
<nuxt />
|
||||
</div>
|
||||
</ds-container>
|
||||
<page-footer v-if="!showMobileMenu" />
|
||||
<page-footer v-if="!isMobile" />
|
||||
<div id="overlay" />
|
||||
<client-only>
|
||||
<modal />
|
||||
@ -17,8 +17,9 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import HeaderMenu from '~/components/HeaderMenu/HeaderMenu'
|
||||
import seo from '~/mixins/seo'
|
||||
import mobile from '~/mixins/mobile'
|
||||
import HeaderMenu from '~/components/HeaderMenu/HeaderMenu'
|
||||
import Modal from '~/components/Modal'
|
||||
import PageFooter from '~/components/PageFooter/PageFooter'
|
||||
|
||||
@ -28,27 +29,10 @@ export default {
|
||||
Modal,
|
||||
PageFooter,
|
||||
},
|
||||
mixins: [seo],
|
||||
data() {
|
||||
return {
|
||||
windowWidth: null,
|
||||
maxMobileWidth: 810,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
showMobileMenu() {
|
||||
if (!this.windowWidth) return false
|
||||
return this.windowWidth <= this.maxMobileWidth
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.windowWidth = window.innerWidth
|
||||
window.addEventListener('resize', () => {
|
||||
this.windowWidth = window.innerWidth
|
||||
})
|
||||
},
|
||||
mixins: [seo, mobile()],
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.main-navigation {
|
||||
background-color: $color-header-background;
|
||||
|
||||
@ -410,6 +410,9 @@
|
||||
"addUserNoOptions": "Keine Nutzer gefunden!",
|
||||
"addUserPlaceholder": "Benutzername",
|
||||
"allGroups": "Alle Gruppen",
|
||||
"button": {
|
||||
"tooltip": "Gruppen anzeigen"
|
||||
},
|
||||
"categories": "Thema ::: Themen",
|
||||
"categoriesTitle": "Themen der Gruppe",
|
||||
"changeMemberRole": "Die Rolle wurde auf „{role}“ geändert!",
|
||||
@ -528,6 +531,24 @@
|
||||
"questions": "Bei Fragen oder Problemen erreichst Du uns per E-Mail an",
|
||||
"title": "{APPLICATION_NAME} befindet sich in der Wartung"
|
||||
},
|
||||
"map": {
|
||||
"alertMessage": "Es kann nicht auf die Karte zugegriffen werden: Der Mapbox-Token ist auf dem Server nicht gesetzt!",
|
||||
"button": {
|
||||
"tooltip": "Landkarte anzeigen"
|
||||
},
|
||||
"markerTypes": {
|
||||
"group": "Gruppe",
|
||||
"theUser": "deine Position",
|
||||
"user": "Benutzer"
|
||||
},
|
||||
"pageTitle": "Landkarte",
|
||||
"styles": {
|
||||
"dark": "Dunkel",
|
||||
"outdoors": "Landschaft",
|
||||
"satellite": "Satellit",
|
||||
"streets": "Straßen"
|
||||
}
|
||||
},
|
||||
"modals": {
|
||||
"deleteUser": {
|
||||
"created": "Erstellt"
|
||||
@ -801,6 +822,7 @@
|
||||
"data": {
|
||||
"labelBio": "Über Dich",
|
||||
"labelCity": "Deine Stadt oder Region",
|
||||
"labelCityHint": "(zeigt ungefähre Position auf der Landkarte)",
|
||||
"labelName": "Dein Name",
|
||||
"labelSlug": "Dein eindeutiger Benutzername",
|
||||
"name": "Deine Daten",
|
||||
|
||||
@ -410,6 +410,9 @@
|
||||
"addUserNoOptions": "No users found!",
|
||||
"addUserPlaceholder": " Username",
|
||||
"allGroups": "All Groups",
|
||||
"button": {
|
||||
"tooltip": "Show groups"
|
||||
},
|
||||
"categories": "Topic ::: Topics",
|
||||
"categoriesTitle": "Topics of the group",
|
||||
"changeMemberRole": "The role has been changed to “{role}”!",
|
||||
@ -528,6 +531,24 @@
|
||||
"questions": "Any Questions or concerns, send an e-mail to",
|
||||
"title": "{APPLICATION_NAME} is under maintenance"
|
||||
},
|
||||
"map": {
|
||||
"alertMessage": "The map cannot be accessed: The Mapbox token is not set on the server!",
|
||||
"button": {
|
||||
"tooltip": "Show map"
|
||||
},
|
||||
"markerTypes": {
|
||||
"group": "group",
|
||||
"theUser": "your position",
|
||||
"user": "user"
|
||||
},
|
||||
"pageTitle": "Map",
|
||||
"styles": {
|
||||
"dark": "Dark",
|
||||
"outdoors": "Outdoors",
|
||||
"satellite": "Satellite",
|
||||
"streets": "Streets"
|
||||
}
|
||||
},
|
||||
"modals": {
|
||||
"deleteUser": {
|
||||
"created": "Created"
|
||||
@ -801,6 +822,7 @@
|
||||
"data": {
|
||||
"labelBio": "About You",
|
||||
"labelCity": "Your City or Region",
|
||||
"labelCityHint": "(shows approximate position on map)",
|
||||
"labelName": "Your Name",
|
||||
"labelSlug": "Your unique user name",
|
||||
"name": "Your data",
|
||||
|
||||
22
webapp/mixins/mobile.js
Normal file
@ -0,0 +1,22 @@
|
||||
export default (mobileWidth = null) => {
|
||||
return {
|
||||
data() {
|
||||
return {
|
||||
windowWidth: null,
|
||||
maxMobileWidth: mobileWidth || 810, // greater counts as desktop
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
isMobile() {
|
||||
if (!this.windowWidth) return false
|
||||
return this.windowWidth <= this.maxMobileWidth
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.windowWidth = window.innerWidth
|
||||
window.addEventListener('resize', () => {
|
||||
this.windowWidth = window.innerWidth
|
||||
})
|
||||
},
|
||||
}
|
||||
}
|
||||
@ -126,6 +126,7 @@ export default {
|
||||
{ src: '~/plugins/vue-filters.js' },
|
||||
{ src: '~/plugins/vue-infinite-loading.js', ssr: false },
|
||||
{ src: '~/plugins/vue-observe-visibility.js', ssr: false },
|
||||
{ src: '~/plugins/v-mapbox.js', mode: 'client' },
|
||||
],
|
||||
|
||||
router: {
|
||||
@ -155,6 +156,11 @@ export default {
|
||||
'@nuxtjs/pwa',
|
||||
],
|
||||
|
||||
buildModules: [
|
||||
// https://composition-api.nuxtjs.org/getting-started/setup#quick-start
|
||||
'@nuxtjs/composition-api/module',
|
||||
],
|
||||
|
||||
/*
|
||||
** Axios module configuration
|
||||
*/
|
||||
|
||||
@ -21,6 +21,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@human-connection/styleguide": "0.5.22",
|
||||
"@mapbox/mapbox-gl-geocoder": "^5.0.1",
|
||||
"@nuxtjs/apollo": "^4.0.0-rc19",
|
||||
"@nuxtjs/axios": "~5.9.7",
|
||||
"@nuxtjs/dotenv": "~1.4.1",
|
||||
@ -39,6 +40,7 @@
|
||||
"intersection-observer": "^0.12.0",
|
||||
"jsonwebtoken": "~8.5.1",
|
||||
"linkify-it": "~3.0.2",
|
||||
"mapbox-gl": "1.13.2",
|
||||
"node-fetch": "^2.6.1",
|
||||
"nuxt": "~2.12.1",
|
||||
"nuxt-dropzone": "^1.0.4",
|
||||
@ -49,6 +51,7 @@
|
||||
"tiptap": "~1.26.6",
|
||||
"tiptap-extensions": "~1.28.8",
|
||||
"trunc-html": "^1.1.2",
|
||||
"v-mapbox": "^1.11.2",
|
||||
"v-tooltip": "~2.1.3",
|
||||
"validator": "^13.0.0",
|
||||
"vue-count-to": "~1.0.13",
|
||||
@ -66,6 +69,7 @@
|
||||
"@babel/plugin-syntax-dynamic-import": "^7.8.3",
|
||||
"@babel/preset-env": "~7.9.0",
|
||||
"@faker-js/faker": "5.1.0",
|
||||
"@nuxtjs/composition-api": "0.32.0",
|
||||
"@storybook/addon-a11y": "^6.3.6",
|
||||
"@storybook/addon-actions": "^5.3.21",
|
||||
"@storybook/addon-notes": "^5.3.18",
|
||||
|
||||
@ -74,10 +74,10 @@
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex'
|
||||
import gql from 'graphql-tag'
|
||||
import { isEmail } from 'validator'
|
||||
import normalizeEmail from '~/components/utils/NormalizeEmail'
|
||||
import PaginationButtons from '~/components/_new/generic/PaginationButtons/PaginationButtons'
|
||||
import { adminUserQuery } from '~/graphql/User'
|
||||
import { FetchAllRoles, updateUserRole } from '~/graphql/admin/Roles'
|
||||
|
||||
export default {
|
||||
@ -138,27 +138,7 @@ export default {
|
||||
apollo: {
|
||||
User: {
|
||||
query() {
|
||||
return gql`
|
||||
query ($filter: _UserFilter, $first: Int, $offset: Int, $email: String) {
|
||||
User(
|
||||
email: $email
|
||||
filter: $filter
|
||||
first: $first
|
||||
offset: $offset
|
||||
orderBy: createdAt_desc
|
||||
) {
|
||||
id
|
||||
name
|
||||
slug
|
||||
email
|
||||
role
|
||||
createdAt
|
||||
contributionsCount
|
||||
commentedCount
|
||||
shoutedCount
|
||||
}
|
||||
}
|
||||
`
|
||||
return adminUserQuery()
|
||||
},
|
||||
variables() {
|
||||
const { offset, first, email, filter } = this
|
||||
|
||||
164
webapp/pages/map.spec.js
Normal file
@ -0,0 +1,164 @@
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
import mapboxgl from 'mapbox-gl'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import VueMeta from 'vue-meta'
|
||||
import Vuex from 'vuex'
|
||||
import Map from './map'
|
||||
|
||||
jest.mock('mapbox-gl', () => {
|
||||
return {
|
||||
GeolocateControl: jest.fn(),
|
||||
Map: jest.fn(() => ({
|
||||
addControl: jest.fn(),
|
||||
on: jest.fn(),
|
||||
remove: jest.fn(),
|
||||
})),
|
||||
NavigationControl: jest.fn(),
|
||||
Popup: jest.fn(() => {
|
||||
return {
|
||||
isOpen: jest.fn(),
|
||||
setLngLat: jest.fn(() => {
|
||||
return {
|
||||
setHTML: jest.fn(() => {
|
||||
return {
|
||||
addTo: jest.fn(),
|
||||
}
|
||||
}),
|
||||
}
|
||||
}),
|
||||
}
|
||||
}),
|
||||
}
|
||||
})
|
||||
|
||||
const localVue = global.localVue
|
||||
localVue.use(VueMeta, { keyName: 'head' })
|
||||
|
||||
const onEventMocks = {}
|
||||
|
||||
const mapOnMock = jest.fn((key, ...args) => {
|
||||
onEventMocks[key] = args[args.length - 1]
|
||||
})
|
||||
const mapAddControlMock = jest.fn()
|
||||
|
||||
const mapMock = {
|
||||
on: mapOnMock,
|
||||
addControl: mapAddControlMock,
|
||||
loadImage: jest.fn(),
|
||||
getCanvas: jest.fn(() => {
|
||||
return {
|
||||
style: {
|
||||
cursor: 'pointer',
|
||||
},
|
||||
}
|
||||
}),
|
||||
}
|
||||
|
||||
const stubs = {
|
||||
'client-only': true,
|
||||
'mgl-map': true,
|
||||
MglFullscreenControl: true,
|
||||
MglNavigationControl: true,
|
||||
MglGeolocateControl: true,
|
||||
MglScaleControl: true,
|
||||
}
|
||||
|
||||
describe('map', () => {
|
||||
let wrapper
|
||||
let mocks
|
||||
|
||||
beforeEach(() => {
|
||||
mocks = {
|
||||
$t: (t) => t,
|
||||
$env: {
|
||||
MAPBOX_TOKEN: 'MY_MAPBOX_TOKEN',
|
||||
},
|
||||
$toast: {
|
||||
error: jest.fn(),
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
describe('mount', () => {
|
||||
const Wrapper = () => {
|
||||
const store = new Vuex.Store({ getters: { 'auth/user': () => false } })
|
||||
return mount(Map, {
|
||||
mocks,
|
||||
localVue,
|
||||
stubs,
|
||||
store,
|
||||
})
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
wrapper = Wrapper()
|
||||
})
|
||||
|
||||
it('renders', () => {
|
||||
expect(wrapper.is('div')).toBeTruthy()
|
||||
})
|
||||
|
||||
it('has correct <head> content', () => {
|
||||
expect(wrapper.vm.$metaInfo.title).toBe('map.pageTitle')
|
||||
})
|
||||
|
||||
describe('trigger map load', () => {
|
||||
beforeEach(async () => {
|
||||
await wrapper.find('mgl-map-stub').vm.$emit('load', { map: mapMock })
|
||||
})
|
||||
|
||||
it('initializes on style load', () => {
|
||||
expect(mapOnMock).toBeCalledWith('style.load', expect.any(Function))
|
||||
})
|
||||
|
||||
it('initializes on mouseenter', () => {
|
||||
expect(mapOnMock).toBeCalledWith('mouseenter', 'markers', expect.any(Function))
|
||||
})
|
||||
|
||||
it('initializes on mouseleave', () => {
|
||||
expect(mapOnMock).toBeCalledWith('mouseleave', 'markers', expect.any(Function))
|
||||
})
|
||||
|
||||
it('calls add map control', () => {
|
||||
expect(mapAddControlMock).toBeCalled()
|
||||
})
|
||||
|
||||
describe('trigger style load event', () => {
|
||||
let spy
|
||||
beforeEach(() => {
|
||||
spy = jest.spyOn(wrapper.vm, 'loadMarkersIconsAndAddMarkers')
|
||||
onEventMocks['style.load']()
|
||||
})
|
||||
|
||||
it('calls loadMarkersIconsAndAddMarkers', () => {
|
||||
expect(spy).toBeCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('trigger mouse enter event', () => {
|
||||
beforeEach(() => {
|
||||
onEventMocks.mouseenter({
|
||||
features: [
|
||||
{
|
||||
geometry: {
|
||||
coordinates: [100, 200],
|
||||
},
|
||||
properties: {
|
||||
type: 'user',
|
||||
},
|
||||
},
|
||||
],
|
||||
lngLat: {
|
||||
lng: 100,
|
||||
lat: 200,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('works without errors and warnings', () => {
|
||||
expect(true).toBe(true)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
483
webapp/pages/map.vue
Normal file
@ -0,0 +1,483 @@
|
||||
<!-- Example Reference: https://codesandbox.io/s/v-mapbox-with-nuxt-lbrt6?file=/pages/index.vue -->
|
||||
<template>
|
||||
<div>
|
||||
<ds-space margin="small">
|
||||
<ds-heading tag="h1">{{ $t('map.pageTitle') }}</ds-heading>
|
||||
</ds-space>
|
||||
<ds-space margin="large" />
|
||||
<client-only v-if="!isEmpty($env.MAPBOX_TOKEN)">
|
||||
<map-styles-buttons
|
||||
v-if="isMobile"
|
||||
:styles="styles"
|
||||
:actualStyle="mapOptions.style"
|
||||
:setStyle="setStyle"
|
||||
/>
|
||||
<mgl-map
|
||||
:mapbox-gl="mapboxgl"
|
||||
:access-token="mapOptions.accessToken"
|
||||
:map-style.sync="mapOptions.style"
|
||||
:center="mapOptions.center"
|
||||
:zoom="mapOptions.zoom"
|
||||
:max-zoom="mapOptions.maxZoom"
|
||||
:cross-source-collisions="false"
|
||||
:fail-if-major-performance-caveat="false"
|
||||
:preserve-drawing-buffer="true"
|
||||
:hash="false"
|
||||
:min-pitch="0"
|
||||
:max-pitch="60"
|
||||
@load="onMapLoad"
|
||||
>
|
||||
<map-styles-buttons
|
||||
v-if="!isMobile"
|
||||
:styles="styles"
|
||||
:actualStyle="mapOptions.style"
|
||||
:setStyle="setStyle"
|
||||
/>
|
||||
<MglFullscreenControl />
|
||||
<MglNavigationControl position="top-right" />
|
||||
<MglGeolocateControl position="top-right" />
|
||||
<MglScaleControl />
|
||||
</mgl-map>
|
||||
</client-only>
|
||||
<empty v-else icon="alert" :message="$t('map.alertMessage')" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { isEmpty, toArray } from 'lodash'
|
||||
import mapboxgl from 'mapbox-gl'
|
||||
import MapboxGeocoder from '@mapbox/mapbox-gl-geocoder'
|
||||
import '@mapbox/mapbox-gl-geocoder/dist/mapbox-gl-geocoder.css'
|
||||
import { mapGetters } from 'vuex'
|
||||
import { profileUserQuery, mapUserQuery } from '~/graphql/User'
|
||||
import { groupQuery } from '~/graphql/groups'
|
||||
import mobile from '~/mixins/mobile'
|
||||
import Empty from '~/components/Empty/Empty'
|
||||
import MapStylesButtons from '~/components/Map/MapStylesButtons'
|
||||
|
||||
const maxMobileWidth = 639 // on this width and smaller the mapbox 'MapboxGeocoder' search gets bigger
|
||||
|
||||
export default {
|
||||
name: 'Map',
|
||||
mixins: [mobile(maxMobileWidth)],
|
||||
components: {
|
||||
Empty,
|
||||
MapStylesButtons,
|
||||
},
|
||||
head() {
|
||||
return {
|
||||
title: this.$t('map.pageTitle'),
|
||||
}
|
||||
},
|
||||
data() {
|
||||
mapboxgl.accessToken = this.$env.MAPBOX_TOKEN
|
||||
return {
|
||||
isEmpty,
|
||||
mapboxgl,
|
||||
activeStyle: null,
|
||||
defaultCenter: [10.452764, 51.165707], // center of Germany: https://www.gpskoordinaten.de/karte/land/DE
|
||||
currentUserLocation: null,
|
||||
currentUserCoordinates: null,
|
||||
users: null,
|
||||
groups: null,
|
||||
markers: {
|
||||
icons: [
|
||||
{
|
||||
id: 'marker-blue',
|
||||
name: 'mapbox-marker-icon-20px-blue.png',
|
||||
},
|
||||
{
|
||||
id: 'marker-orange',
|
||||
name: 'mapbox-marker-icon-20px-orange.png',
|
||||
},
|
||||
{
|
||||
id: 'marker-green',
|
||||
name: 'mapbox-marker-icon-20px-green.png',
|
||||
},
|
||||
],
|
||||
isImagesLoaded: false,
|
||||
geoJSON: [],
|
||||
isGeoJSON: false,
|
||||
isSourceAndLayerAdded: false,
|
||||
isFlyToCenter: false,
|
||||
popup: null,
|
||||
popupOnLeaveTimeoutId: null,
|
||||
},
|
||||
}
|
||||
},
|
||||
async mounted() {
|
||||
this.currentUserLocation = await this.getUserLocation(this.currentUser.id)
|
||||
this.currentUserCoordinates = this.currentUserLocation
|
||||
? this.getCoordinates(this.currentUserLocation)
|
||||
: null
|
||||
this.addMarkersOnCheckPrepared()
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
currentUser: 'auth/user',
|
||||
}),
|
||||
isPreparedForMarkers() {
|
||||
return (
|
||||
!this.markers.isGeoJSON &&
|
||||
this.markers.isImagesLoaded &&
|
||||
this.currentUser &&
|
||||
this.users &&
|
||||
this.groups
|
||||
)
|
||||
},
|
||||
styles() {
|
||||
return toArray(this.availableStyles)
|
||||
},
|
||||
availableStyles() {
|
||||
// https://docs.mapbox.com/api/maps/styles/
|
||||
const availableStyles = {
|
||||
outdoors: {
|
||||
url: 'mapbox://styles/mapbox/outdoors-v12?optimize=true',
|
||||
},
|
||||
streets: {
|
||||
url: 'mapbox://styles/mapbox/streets-v11?optimize=true',
|
||||
// use the newest version?
|
||||
// url: 'mapbox://styles/mapbox/streets-v12',
|
||||
},
|
||||
satellite: {
|
||||
url: 'mapbox://styles/mapbox/satellite-streets-v11?optimize=true',
|
||||
},
|
||||
dark: {
|
||||
url: 'mapbox://styles/mapbox/dark-v10?optimize=true',
|
||||
},
|
||||
}
|
||||
Object.keys(availableStyles).map((key) => {
|
||||
availableStyles[key].title = this.$t('map.styles.' + key)
|
||||
})
|
||||
return availableStyles
|
||||
},
|
||||
mapOptions() {
|
||||
return {
|
||||
// accessToken: this.$env.MAPBOX_TOKEN, // is set already above
|
||||
style: !this.activeStyle ? this.availableStyles.outdoors.url : this.activeStyle,
|
||||
center: this.mapCenter,
|
||||
zoom: this.mapZoom,
|
||||
maxZoom: 22,
|
||||
// projection: 'globe', // the package is probably to old, because of Vue2: https://docs.mapbox.com/mapbox-gl-js/example/globe/
|
||||
}
|
||||
},
|
||||
mapCenter() {
|
||||
return this.currentUserCoordinates ? this.currentUserCoordinates : this.defaultCenter
|
||||
},
|
||||
mapZoom() {
|
||||
return this.currentUserCoordinates ? 10 : 4
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
isPreparedForMarkers(newValue) {
|
||||
if (newValue) {
|
||||
this.addMarkersOnCheckPrepared()
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onMapLoad({ map }) {
|
||||
this.map = map
|
||||
|
||||
// set the default atmosphere style
|
||||
// this.map.setFog({}) // the package is probably to old, because of Vue2: https://docs.mapbox.com/mapbox-gl-js/example/globe/
|
||||
|
||||
this.map.on('style.load', (value) => {
|
||||
// Triggered when `setStyle` is called.
|
||||
this.markers.isImagesLoaded = false
|
||||
this.markers.isSourceAndLayerAdded = false
|
||||
this.loadMarkersIconsAndAddMarkers()
|
||||
})
|
||||
|
||||
// add search field for locations
|
||||
this.map.addControl(
|
||||
new MapboxGeocoder({
|
||||
accessToken: this.$env.MAPBOX_TOKEN,
|
||||
mapboxgl: this.mapboxgl,
|
||||
}),
|
||||
)
|
||||
|
||||
// example for popup: https://docs.mapbox.com/mapbox-gl-js/example/popup-on-hover/
|
||||
// create a popup, but don't add it to the map yet
|
||||
this.markers.popup = new mapboxgl.Popup({
|
||||
closeButton: false,
|
||||
closeOnClick: true,
|
||||
})
|
||||
|
||||
this.map.on('mouseenter', 'markers', (e) => {
|
||||
// if (e.features[0].properties.type !== 'theUser') {}
|
||||
if (this.popupOnLeaveTimeoutId) {
|
||||
clearTimeout(this.popupOnLeaveTimeoutId)
|
||||
this.popupOnLeaveTimeoutId = null
|
||||
}
|
||||
if (this.markers.popup.isOpen()) {
|
||||
this.map.getCanvas().style.cursor = ''
|
||||
this.markers.popup.remove()
|
||||
}
|
||||
|
||||
// Change the cursor style as a UI indicator.
|
||||
this.map.getCanvas().style.cursor = 'pointer'
|
||||
|
||||
// Copy coordinates array.
|
||||
const coordinates = e.features[0].geometry.coordinates.slice()
|
||||
const markerTypeLabel =
|
||||
e.features[0].properties.type === 'group'
|
||||
? this.$t('map.markerTypes.group')
|
||||
: e.features[0].properties.type === 'user'
|
||||
? this.$t('map.markerTypes.user')
|
||||
: this.$t('map.markerTypes.theUser')
|
||||
const markerProfileLinkTitle =
|
||||
(e.features[0].properties.type === 'group' ? '&' : '@') + e.features[0].properties.slug
|
||||
const markerProfileLink =
|
||||
(e.features[0].properties.type === 'group' ? '/group' : '/profile') +
|
||||
`/${e.features[0].properties.id}/${e.features[0].properties.slug}`
|
||||
let description = `
|
||||
<div>
|
||||
<div>
|
||||
<b>${e.features[0].properties.name}</b> <i>(${markerTypeLabel})</i>
|
||||
</div>
|
||||
<div>
|
||||
<a href="${markerProfileLink}" target="_blank">${markerProfileLinkTitle}</a>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
description +=
|
||||
e.features[0].properties.about && e.features[0].properties.about.length > 0
|
||||
? `
|
||||
<hr>
|
||||
<div>
|
||||
${e.features[0].properties.about}
|
||||
</div>`
|
||||
: ''
|
||||
|
||||
// Ensure that if the map is zoomed out such that multiple
|
||||
// copies of the feature are visible, the popup appears
|
||||
// over the copy being pointed to.
|
||||
while (Math.abs(e.lngLat.lng - coordinates[0]) > 180) {
|
||||
coordinates[0] += e.lngLat.lng > coordinates[0] ? 360 : -360
|
||||
}
|
||||
|
||||
// Populate the popup and set its coordinates
|
||||
// based on the feature found.
|
||||
this.markers.popup.setLngLat(coordinates).setHTML(description).addTo(this.map)
|
||||
})
|
||||
|
||||
this.map.on('mouseleave', 'markers', (e) => {
|
||||
if (this.markers.popup.isOpen()) {
|
||||
this.popupOnLeaveTimeoutId = setTimeout(() => {
|
||||
this.map.getCanvas().style.cursor = ''
|
||||
this.markers.popup.remove()
|
||||
}, 3000)
|
||||
}
|
||||
})
|
||||
|
||||
this.loadMarkersIconsAndAddMarkers()
|
||||
},
|
||||
language(map) {
|
||||
// example in mapbox-gl-language: https://github.com/mapbox/mapbox-gl-language/blob/master/index.js
|
||||
map.getStyle().layers.forEach(function (thisLayer) {
|
||||
if (thisLayer.id.indexOf('-label') > 0) {
|
||||
// seems to use user language. specific language would be `name_de`, but is not compatible with all maps
|
||||
// variant sets all 'text-field' layers to languages of their countries
|
||||
map.setLayoutProperty(thisLayer.id, 'text-field', ['get', 'name'])
|
||||
}
|
||||
})
|
||||
},
|
||||
setStyle(url) {
|
||||
this.map.setStyle(url)
|
||||
this.activeStyle = url
|
||||
},
|
||||
loadMarkersIconsAndAddMarkers() {
|
||||
Promise.all(
|
||||
this.markers.icons.map(
|
||||
(marker) =>
|
||||
new Promise((resolve, reject) => {
|
||||
// our images have to be in the 'static/img/*' folder otherwise they are not reachable via URL
|
||||
this.map.loadImage('img/mapbox/marker-icons/' + marker.name, (error, image) => {
|
||||
if (error) throw error
|
||||
this.map.addImage(marker.id, image)
|
||||
resolve()
|
||||
})
|
||||
}),
|
||||
),
|
||||
).then(() => {
|
||||
this.markers.isImagesLoaded = true
|
||||
this.language(this.map)
|
||||
this.addMarkersOnCheckPrepared()
|
||||
})
|
||||
},
|
||||
addMarkersOnCheckPrepared() {
|
||||
// set geoJSON for markers
|
||||
if (this.isPreparedForMarkers) {
|
||||
// add markers for "users"
|
||||
this.users.forEach((user) => {
|
||||
if (user.id !== this.currentUser.id && user.location) {
|
||||
this.markers.geoJSON.push({
|
||||
type: 'Feature',
|
||||
properties: {
|
||||
type: 'user',
|
||||
iconName: 'marker-green',
|
||||
iconRotate: 0.0,
|
||||
id: user.id,
|
||||
slug: user.slug,
|
||||
name: user.name,
|
||||
about: user.about,
|
||||
},
|
||||
geometry: {
|
||||
type: 'Point',
|
||||
coordinates: this.getCoordinates(user.location),
|
||||
},
|
||||
})
|
||||
}
|
||||
})
|
||||
// add markers for "groups"
|
||||
this.groups.forEach((group) => {
|
||||
if (group.location) {
|
||||
this.markers.geoJSON.push({
|
||||
type: 'Feature',
|
||||
properties: {
|
||||
type: 'group',
|
||||
iconName: 'marker-blue',
|
||||
iconRotate: 0.0,
|
||||
id: group.id,
|
||||
slug: group.slug,
|
||||
name: group.name,
|
||||
about: group.about,
|
||||
},
|
||||
geometry: {
|
||||
type: 'Point',
|
||||
coordinates: this.getCoordinates(group.location),
|
||||
},
|
||||
})
|
||||
}
|
||||
})
|
||||
// add marker for "currentUser"
|
||||
if (this.currentUserCoordinates) {
|
||||
this.markers.geoJSON.push({
|
||||
type: 'Feature',
|
||||
properties: {
|
||||
type: 'theUser',
|
||||
iconName: 'marker-orange',
|
||||
iconRotate: 45.0,
|
||||
id: this.currentUser.id,
|
||||
slug: this.currentUser.slug,
|
||||
name: this.currentUser.name,
|
||||
about: this.currentUser.about,
|
||||
},
|
||||
geometry: {
|
||||
type: 'Point',
|
||||
coordinates: this.currentUserCoordinates,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
this.markers.isGeoJSON = true
|
||||
}
|
||||
|
||||
// add source and layer
|
||||
if (!this.markers.isSourceAndLayerAdded && this.markers.isGeoJSON && this.map) {
|
||||
this.map.addSource('markers', {
|
||||
type: 'geojson',
|
||||
data: {
|
||||
type: 'FeatureCollection',
|
||||
features: this.markers.geoJSON,
|
||||
},
|
||||
})
|
||||
this.map.addLayer({
|
||||
id: 'markers',
|
||||
type: 'symbol',
|
||||
source: 'markers',
|
||||
layout: {
|
||||
'icon-image': ['get', 'iconName'], // get the "icon-image" from the source's "iconName" property
|
||||
'icon-allow-overlap': true,
|
||||
'icon-size': 1.0,
|
||||
'icon-rotate': ['get', 'iconRotate'], // get the "icon-rotate" from the source's "iconRotate" property
|
||||
// 'text-field': ['get', 'name'], // get the "text-field" from the source's "name" property
|
||||
// 'text-font': ['Open Sans Semibold', 'Arial Unicode MS Bold'],
|
||||
// 'text-offset': [0, 0],
|
||||
// 'text-anchor': 'top',
|
||||
// 'text-allow-overlap': true,
|
||||
},
|
||||
})
|
||||
|
||||
this.markers.isSourceAndLayerAdded = true
|
||||
}
|
||||
|
||||
// fly to center if never done
|
||||
if (!this.markers.isFlyToCenter && this.markers.isSourceAndLayerAdded) {
|
||||
this.mapFlyToCenter()
|
||||
this.markers.isFlyToCenter = true
|
||||
}
|
||||
},
|
||||
mapFlyToCenter() {
|
||||
if (this.map) {
|
||||
// example: https://docs.mapbox.com/mapbox-gl-js/example/center-on-feature/
|
||||
this.map.flyTo({
|
||||
center: this.mapCenter,
|
||||
zoom: this.mapZoom,
|
||||
})
|
||||
}
|
||||
},
|
||||
getCoordinates(location) {
|
||||
return [location.lng, location.lat]
|
||||
},
|
||||
async getUserLocation(id) {
|
||||
try {
|
||||
const {
|
||||
data: { User: users },
|
||||
} = await this.$apollo.query({
|
||||
query: profileUserQuery(this.$i18n),
|
||||
variables: {
|
||||
id,
|
||||
followedByCount: 0,
|
||||
followingCount: 0,
|
||||
},
|
||||
})
|
||||
return users && users[0] && users[0].location ? users[0].location : null
|
||||
} catch (err) {
|
||||
this.$toast.error(err.message)
|
||||
return null
|
||||
}
|
||||
},
|
||||
},
|
||||
apollo: {
|
||||
User: {
|
||||
query() {
|
||||
return mapUserQuery(this.$i18n)
|
||||
},
|
||||
variables() {
|
||||
return {}
|
||||
},
|
||||
update({ User }) {
|
||||
this.users = User
|
||||
this.addMarkersOnCheckPrepared()
|
||||
},
|
||||
fetchPolicy: 'cache-and-network',
|
||||
},
|
||||
Group: {
|
||||
query() {
|
||||
return groupQuery(this.$i18n)
|
||||
},
|
||||
variables() {
|
||||
return {}
|
||||
},
|
||||
update({ Group }) {
|
||||
this.groups = Group
|
||||
this.addMarkersOnCheckPrepared()
|
||||
},
|
||||
fetchPolicy: 'cache-and-network',
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
// description: https: //github.com/geospoc/v-mapbox/tree/v1.11.2/docs
|
||||
// code example: https: //codesandbox.io/embed/v-mapbox-map-demo-k1l1n?autoresize=1&fontsize=14&hidenavigation=1&theme=dark
|
||||
@import 'mapbox-gl/dist/mapbox-gl.css';
|
||||
@import 'v-mapbox/dist/v-mapbox.css';
|
||||
|
||||
.mgl-map-wrapper {
|
||||
height: 70vh;
|
||||
}
|
||||
</style>
|
||||
@ -184,8 +184,7 @@ import MasonryGrid from '~/components/MasonryGrid/MasonryGrid.vue'
|
||||
import MasonryGridItem from '~/components/MasonryGrid/MasonryGridItem.vue'
|
||||
import TabNavigation from '~/components/_new/generic/TabNavigation/TabNavigation'
|
||||
import { profilePagePosts } from '~/graphql/PostQuery'
|
||||
import UserQuery from '~/graphql/User'
|
||||
import { updateUserMutation } from '~/graphql/User.js'
|
||||
import { profileUserQuery, updateUserMutation } from '~/graphql/User'
|
||||
import { muteUser, unmuteUser } from '~/graphql/settings/MutedUsers'
|
||||
import { blockUser, unblockUser } from '~/graphql/settings/BlockedUsers'
|
||||
import UpdateQuery from '~/components/utils/UpdateQuery'
|
||||
@ -408,7 +407,7 @@ export default {
|
||||
},
|
||||
User: {
|
||||
query() {
|
||||
return UserQuery(this.$i18n)
|
||||
return profileUserQuery(this.$i18n)
|
||||
},
|
||||
variables() {
|
||||
return {
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<ds-form v-model="form" :schema="formSchema" @submit="submit">
|
||||
<ds-form class="settings-form" v-model="form" :schema="formSchema" @submit="submit">
|
||||
<template #default="{ errors }">
|
||||
<base-card>
|
||||
<h2 class="title">{{ $t('settings.data.name') }}</h2>
|
||||
@ -22,6 +22,9 @@
|
||||
:loading="loadingGeo"
|
||||
@input.native="handleCityInput"
|
||||
/>
|
||||
<ds-text class="location-hint" color="softer">
|
||||
{{ $t('settings.data.labelCityHint') }}
|
||||
</ds-text>
|
||||
<!-- eslint-enable vue/use-v-on-exact -->
|
||||
<ds-input
|
||||
id="about"
|
||||
@ -158,3 +161,12 @@ export default {
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
// .settings-form {
|
||||
// >
|
||||
.location-hint {
|
||||
margin-top: -$space-x-small - $space-xxx-small - $space-xxx-small;
|
||||
}
|
||||
// }
|
||||
</style>
|
||||
|
||||
35
webapp/plugins/v-mapbox.js
Normal file
@ -0,0 +1,35 @@
|
||||
// Vue2 + Mapbox Reference: https://github.com/geospoc/v-mapbox/issues/702
|
||||
import Vue from 'vue'
|
||||
import {
|
||||
MglGeojsonLayer,
|
||||
MglVectorLayer,
|
||||
MglMap,
|
||||
MglMarker,
|
||||
MglPopup,
|
||||
MglAttributionControl,
|
||||
MglScaleControl,
|
||||
MglNavigationControl,
|
||||
MglGeolocateControl,
|
||||
MglFullscreenControl,
|
||||
} from 'v-mapbox'
|
||||
|
||||
// Map
|
||||
Vue.component('MglMap', MglMap)
|
||||
|
||||
// overview of all: https://github.com/geospoc/v-mapbox/tree/v1.11.2/src/components
|
||||
// mapbox: https://docs.mapbox.com/mapbox-gl-js/api/markers/
|
||||
|
||||
// Controls
|
||||
Vue.component('MglAttributionControl', MglAttributionControl)
|
||||
Vue.component('MglScaleControl', MglScaleControl)
|
||||
Vue.component('MglNavigationControl', MglNavigationControl)
|
||||
Vue.component('MglGeolocateControl', MglGeolocateControl)
|
||||
Vue.component('MglFullscreenControl', MglFullscreenControl)
|
||||
|
||||
// Layers
|
||||
Vue.component('MglGeojsonLayer', MglGeojsonLayer)
|
||||
Vue.component('MglVectorLayer', MglVectorLayer)
|
||||
|
||||
// Marker & Popup
|
||||
Vue.component('MglMarker', MglMarker)
|
||||
Vue.component('MglPopup', MglPopup)
|
||||
12
webapp/static/img/mapbox/marker-icons/README.md
Normal file
@ -0,0 +1,12 @@
|
||||
# Mabbox markers
|
||||
|
||||
I found the Mapbox markers to be downloaded at the bottom of the page:
|
||||
<https://docs.mapbox.com/help/glossary/sprite/>
|
||||
|
||||
At URL:
|
||||
<https://docs.mapbox.com/help/data/marker-icons.zip>
|
||||
|
||||
## Folder For Images Reachable By URL
|
||||
|
||||
It looks like that not all folders, as example the `assets/*` folder, is reachable by URL.
|
||||
Our images have to be in the `static/img/*` folder.
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 1.6 KiB |
@ -0,0 +1,16 @@
|
||||
<!-- Create a custom map style: https://studio.mapbox.com -->
|
||||
<svg id="marker" data-name="marker" xmlns="http://www.w3.org/2000/svg" width="20" height="48" viewBox="0 0 20 48">
|
||||
<g id="mapbox-marker-icon">
|
||||
<g id="icon">
|
||||
<ellipse id="shadow" cx="10" cy="27" rx="9" ry="5" fill="#c4c4c4" opacity="0.3" style="isolation: isolate"/>
|
||||
<g id="mask" opacity="0.3">
|
||||
<g id="group">
|
||||
<path id="shadow-2" data-name="shadow" fill="#bfbfbf" d="M10,32c5,0,9-2.2,9-5s-4-5-9-5-9,2.2-9,5S5,32,10,32Z" fill-rule="evenodd"/>
|
||||
</g>
|
||||
</g>
|
||||
<path id="color" fill="#4264fb" stroke="#314ccd" stroke-width="0.5" d="M19.25,10.4a13.0663,13.0663,0,0,1-1.4607,5.2235,41.5281,41.5281,0,0,1-3.2459,5.5483c-1.1829,1.7369-2.3662,3.2784-3.2541,4.3859-.4438.5536-.8135.9984-1.0721,1.3046-.0844.1-.157.1852-.2164.2545-.06-.07-.1325-.1564-.2173-.2578-.2587-.3088-.6284-.7571-1.0723-1.3147-.8879-1.1154-2.0714-2.6664-3.2543-4.41a42.2677,42.2677,0,0,1-3.2463-5.5535A12.978,12.978,0,0,1,.75,10.4,9.4659,9.4659,0,0,1,10,.75,9.4659,9.4659,0,0,1,19.25,10.4Z"/>
|
||||
<path id="circle" fill="#fff" stroke="#314ccd" stroke-width="0.5" d="M13.55,10A3.55,3.55,0,1,1,10,6.45,3.5484,3.5484,0,0,1,13.55,10Z"/>
|
||||
</g>
|
||||
</g>
|
||||
<rect width="20" height="48" fill="none"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
@ -0,0 +1,16 @@
|
||||
<!-- Create a custom map style: https://studio.mapbox.com -->
|
||||
<svg id="marker" data-name="marker" xmlns="http://www.w3.org/2000/svg" width="20" height="48" viewBox="0 0 20 48">
|
||||
<g id="mapbox-marker-icon">
|
||||
<g id="icon">
|
||||
<ellipse id="shadow" cx="10" cy="27" rx="9" ry="5" fill="#c4c4c4" opacity="0.3" style="isolation: isolate"/>
|
||||
<g id="mask" opacity="0.3">
|
||||
<g id="group">
|
||||
<path id="shadow-2" data-name="shadow" fill="#bfbfbf" d="M10,32c5,0,9-2.2,9-5s-4-5-9-5-9,2.2-9,5S5,32,10,32Z" fill-rule="evenodd"/>
|
||||
</g>
|
||||
</g>
|
||||
<path id="color" fill="#5b7897" stroke="#23374d" stroke-width="0.5" d="M19.25,10.4a13.0663,13.0663,0,0,1-1.4607,5.2235,41.5281,41.5281,0,0,1-3.2459,5.5483c-1.1829,1.7369-2.3662,3.2784-3.2541,4.3859-.4438.5536-.8135.9984-1.0721,1.3046-.0844.1-.157.1852-.2164.2545-.06-.07-.1325-.1564-.2173-.2578-.2587-.3088-.6284-.7571-1.0723-1.3147-.8879-1.1154-2.0714-2.6664-3.2543-4.41a42.2677,42.2677,0,0,1-3.2463-5.5535A12.978,12.978,0,0,1,.75,10.4,9.4659,9.4659,0,0,1,10,.75,9.4659,9.4659,0,0,1,19.25,10.4Z"/>
|
||||
<path id="circle" fill="#fff" stroke="#23374d" stroke-width="0.5" d="M13.55,10A3.55,3.55,0,1,1,10,6.45,3.5484,3.5484,0,0,1,13.55,10Z"/>
|
||||
</g>
|
||||
</g>
|
||||
<rect width="20" height="48" fill="none"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
@ -0,0 +1,16 @@
|
||||
<!-- Create a custom map style: https://studio.mapbox.com -->
|
||||
<svg id="marker" data-name="marker" xmlns="http://www.w3.org/2000/svg" width="20" height="48" viewBox="0 0 20 48">
|
||||
<g id="mapbox-marker-icon">
|
||||
<g id="icon">
|
||||
<ellipse id="shadow" cx="10" cy="27" rx="9" ry="5" fill="#c4c4c4" opacity="0.3" style="isolation: isolate"/>
|
||||
<g id="mask" opacity="0.3">
|
||||
<g id="group">
|
||||
<path id="shadow-2" data-name="shadow" fill="#bfbfbf" d="M10,32c5,0,9-2.2,9-5s-4-5-9-5-9,2.2-9,5S5,32,10,32Z" fill-rule="evenodd"/>
|
||||
</g>
|
||||
</g>
|
||||
<path id="color" fill="#33c377" stroke="#269561" stroke-width="0.5" d="M19.25,10.4a13.0663,13.0663,0,0,1-1.4607,5.2235,41.5281,41.5281,0,0,1-3.2459,5.5483c-1.1829,1.7369-2.3662,3.2784-3.2541,4.3859-.4438.5536-.8135.9984-1.0721,1.3046-.0844.1-.157.1852-.2164.2545-.06-.07-.1325-.1564-.2173-.2578-.2587-.3088-.6284-.7571-1.0723-1.3147-.8879-1.1154-2.0714-2.6664-3.2543-4.41a42.2677,42.2677,0,0,1-3.2463-5.5535A12.978,12.978,0,0,1,.75,10.4,9.4659,9.4659,0,0,1,10,.75,9.4659,9.4659,0,0,1,19.25,10.4Z"/>
|
||||
<path id="circle" fill="#fff" stroke="#269561" stroke-width="0.5" d="M13.55,10A3.55,3.55,0,1,1,10,6.45,3.5484,3.5484,0,0,1,13.55,10Z"/>
|
||||
</g>
|
||||
</g>
|
||||
<rect width="20" height="48" fill="none"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
@ -0,0 +1,16 @@
|
||||
<!-- Create a custom map style: https://studio.mapbox.com -->
|
||||
<svg id="marker" data-name="marker" xmlns="http://www.w3.org/2000/svg" width="20" height="48" viewBox="0 0 20 48">
|
||||
<g id="mapbox-marker-icon">
|
||||
<g id="icon">
|
||||
<ellipse id="shadow" cx="10" cy="27" rx="9" ry="5" fill="#c4c4c4" opacity="0.3" style="isolation: isolate"/>
|
||||
<g id="mask" opacity="0.3">
|
||||
<g id="group">
|
||||
<path id="shadow-2" data-name="shadow" fill="#bfbfbf" d="M10,32c5,0,9-2.2,9-5s-4-5-9-5-9,2.2-9,5S5,32,10,32Z" fill-rule="evenodd"/>
|
||||
</g>
|
||||
</g>
|
||||
<path id="color" fill="#f79640" stroke="#ba7334" stroke-width="0.5" d="M19.25,10.4a13.0663,13.0663,0,0,1-1.4607,5.2235,41.5281,41.5281,0,0,1-3.2459,5.5483c-1.1829,1.7369-2.3662,3.2784-3.2541,4.3859-.4438.5536-.8135.9984-1.0721,1.3046-.0844.1-.157.1852-.2164.2545-.06-.07-.1325-.1564-.2173-.2578-.2587-.3088-.6284-.7571-1.0723-1.3147-.8879-1.1154-2.0714-2.6664-3.2543-4.41a42.2677,42.2677,0,0,1-3.2463-5.5535A12.978,12.978,0,0,1,.75,10.4,9.4659,9.4659,0,0,1,10,.75,9.4659,9.4659,0,0,1,19.25,10.4Z"/>
|
||||
<path id="circle" fill="#fff" stroke="#ba7334" stroke-width="0.5" d="M13.55,10A3.55,3.55,0,1,1,10,6.45,3.5484,3.5484,0,0,1,13.55,10Z"/>
|
||||
</g>
|
||||
</g>
|
||||
<rect width="20" height="48" fill="none"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
@ -0,0 +1,16 @@
|
||||
<!-- Create a custom map style: https://studio.mapbox.com -->
|
||||
<svg id="marker" data-name="marker" xmlns="http://www.w3.org/2000/svg" width="20" height="48" viewBox="0 0 20 48">
|
||||
<g id="mapbox-marker-icon">
|
||||
<g id="icon">
|
||||
<ellipse id="shadow" cx="10" cy="27" rx="9" ry="5" fill="#c4c4c4" opacity="0.3" style="isolation: isolate"/>
|
||||
<g id="mask" opacity="0.3">
|
||||
<g id="group">
|
||||
<path id="shadow-2" data-name="shadow" fill="#bfbfbf" d="M10,32c5,0,9-2.2,9-5s-4-5-9-5-9,2.2-9,5S5,32,10,32Z" fill-rule="evenodd"/>
|
||||
</g>
|
||||
</g>
|
||||
<path id="color" fill="#ee4e8b" stroke="#b43b71" stroke-width="0.5" d="M19.25,10.4a13.0663,13.0663,0,0,1-1.4607,5.2235,41.5281,41.5281,0,0,1-3.2459,5.5483c-1.1829,1.7369-2.3662,3.2784-3.2541,4.3859-.4438.5536-.8135.9984-1.0721,1.3046-.0844.1-.157.1852-.2164.2545-.06-.07-.1325-.1564-.2173-.2578-.2587-.3088-.6284-.7571-1.0723-1.3147-.8879-1.1154-2.0714-2.6664-3.2543-4.41a42.2677,42.2677,0,0,1-3.2463-5.5535A12.978,12.978,0,0,1,.75,10.4,9.4659,9.4659,0,0,1,10,.75,9.4659,9.4659,0,0,1,19.25,10.4Z"/>
|
||||
<path id="circle" fill="#fff" stroke="#b43b71" stroke-width="0.5" d="M13.55,10A3.55,3.55,0,1,1,10,6.45,3.5484,3.5484,0,0,1,13.55,10Z"/>
|
||||
</g>
|
||||
</g>
|
||||
<rect width="20" height="48" fill="none"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
@ -0,0 +1,16 @@
|
||||
<!-- Create a custom map style: https://studio.mapbox.com -->
|
||||
<svg id="marker" data-name="marker" xmlns="http://www.w3.org/2000/svg" width="20" height="48" viewBox="0 0 20 48">
|
||||
<g id="mapbox-marker-icon">
|
||||
<g id="icon">
|
||||
<ellipse id="shadow" cx="10" cy="27" rx="9" ry="5" fill="#c4c4c4" opacity="0.3" style="isolation: isolate"/>
|
||||
<g id="mask" opacity="0.3">
|
||||
<g id="group">
|
||||
<path id="shadow-2" data-name="shadow" fill="#bfbfbf" d="M10,32c5,0,9-2.2,9-5s-4-5-9-5-9,2.2-9,5S5,32,10,32Z" fill-rule="evenodd"/>
|
||||
</g>
|
||||
</g>
|
||||
<path id="color" fill="#7753eb" stroke="#5a3fc0" stroke-width="0.5" d="M19.25,10.4a13.0663,13.0663,0,0,1-1.4607,5.2235,41.5281,41.5281,0,0,1-3.2459,5.5483c-1.1829,1.7369-2.3662,3.2784-3.2541,4.3859-.4438.5536-.8135.9984-1.0721,1.3046-.0844.1-.157.1852-.2164.2545-.06-.07-.1325-.1564-.2173-.2578-.2587-.3088-.6284-.7571-1.0723-1.3147-.8879-1.1154-2.0714-2.6664-3.2543-4.41a42.2677,42.2677,0,0,1-3.2463-5.5535A12.978,12.978,0,0,1,.75,10.4,9.4659,9.4659,0,0,1,10,.75,9.4659,9.4659,0,0,1,19.25,10.4Z"/>
|
||||
<path id="circle" fill="#fff" stroke="#5a3fc0" stroke-width="0.5" d="M13.55,10A3.55,3.55,0,1,1,10,6.45,3.5484,3.5484,0,0,1,13.55,10Z"/>
|
||||
</g>
|
||||
</g>
|
||||
<rect width="20" height="48" fill="none"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
@ -0,0 +1,16 @@
|
||||
<!-- Create a custom map style: https://studio.mapbox.com -->
|
||||
<svg id="marker" data-name="marker" xmlns="http://www.w3.org/2000/svg" width="20" height="48" viewBox="0 0 20 48">
|
||||
<g id="mapbox-marker-icon">
|
||||
<g id="icon">
|
||||
<ellipse id="shadow" cx="10" cy="27" rx="9" ry="5" fill="#c4c4c4" opacity="0.3" style="isolation: isolate"/>
|
||||
<g id="mask" opacity="0.3">
|
||||
<g id="group">
|
||||
<path id="shadow-2" data-name="shadow" fill="#bfbfbf" d="M10,32c5,0,9-2.2,9-5s-4-5-9-5-9,2.2-9,5S5,32,10,32Z" fill-rule="evenodd"/>
|
||||
</g>
|
||||
</g>
|
||||
<path id="color" fill="#f84d4d" stroke="#951212" stroke-width="0.5" d="M19.25,10.4a13.0663,13.0663,0,0,1-1.4607,5.2235,41.5281,41.5281,0,0,1-3.2459,5.5483c-1.1829,1.7369-2.3662,3.2784-3.2541,4.3859-.4438.5536-.8135.9984-1.0721,1.3046-.0844.1-.157.1852-.2164.2545-.06-.07-.1325-.1564-.2173-.2578-.2587-.3088-.6284-.7571-1.0723-1.3147-.8879-1.1154-2.0714-2.6664-3.2543-4.41a42.2677,42.2677,0,0,1-3.2463-5.5535A12.978,12.978,0,0,1,.75,10.4,9.4659,9.4659,0,0,1,10,.75,9.4659,9.4659,0,0,1,19.25,10.4Z"/>
|
||||
<path id="circle" fill="#fff" stroke="#951212" stroke-width="0.5" d="M13.55,10A3.55,3.55,0,1,1,10,6.45,3.5484,3.5484,0,0,1,13.55,10Z"/>
|
||||
</g>
|
||||
</g>
|
||||
<rect width="20" height="48" fill="none"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
@ -0,0 +1,16 @@
|
||||
<!-- Create a custom map style: https://studio.mapbox.com -->
|
||||
<svg id="marker" data-name="marker" xmlns="http://www.w3.org/2000/svg" width="20" height="48" viewBox="0 0 20 48">
|
||||
<g id="mapbox-marker-icon">
|
||||
<g id="icon">
|
||||
<ellipse id="shadow" cx="10" cy="27" rx="9" ry="5" fill="#c4c4c4" opacity="0.3" style="isolation: isolate"/>
|
||||
<g id="mask" opacity="0.3">
|
||||
<g id="group">
|
||||
<path id="shadow-2" data-name="shadow" fill="#bfbfbf" d="M10,32c5,0,9-2.2,9-5s-4-5-9-5-9,2.2-9,5S5,32,10,32Z" fill-rule="evenodd"/>
|
||||
</g>
|
||||
</g>
|
||||
<path id="color" fill="#d9d838" stroke="#a4a62d" stroke-width="0.5" d="M19.25,10.4a13.0663,13.0663,0,0,1-1.4607,5.2235,41.5281,41.5281,0,0,1-3.2459,5.5483c-1.1829,1.7369-2.3662,3.2784-3.2541,4.3859-.4438.5536-.8135.9984-1.0721,1.3046-.0844.1-.157.1852-.2164.2545-.06-.07-.1325-.1564-.2173-.2578-.2587-.3088-.6284-.7571-1.0723-1.3147-.8879-1.1154-2.0714-2.6664-3.2543-4.41a42.2677,42.2677,0,0,1-3.2463-5.5535A12.978,12.978,0,0,1,.75,10.4,9.4659,9.4659,0,0,1,10,.75,9.4659,9.4659,0,0,1,19.25,10.4Z"/>
|
||||
<path id="circle" fill="#fff" stroke="#a4a62d" stroke-width="0.5" d="M13.55,10A3.55,3.55,0,1,1,10,6.45,3.5484,3.5484,0,0,1,13.55,10Z"/>
|
||||
</g>
|
||||
</g>
|
||||
<rect width="20" height="48" fill="none"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |