Ask for confirmation for nodes outside of community bounds.
This commit is contained in:
parent
9387df8dd3
commit
4c6556de3f
|
@ -19,6 +19,7 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"fork-awesome": "^1.2.0",
|
"fork-awesome": "^1.2.0",
|
||||||
|
"geolib": "^3.3.3",
|
||||||
"leaflet": "^1.8.0",
|
"leaflet": "^1.8.0",
|
||||||
"pinia": "^2.0.21",
|
"pinia": "^2.0.21",
|
||||||
"sparkson": "^1.3.6",
|
"sparkson": "^1.3.6",
|
||||||
|
|
36
frontend/src/components/FloatingIcon.vue
Normal file
36
frontend/src/components/FloatingIcon.vue
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { ComponentVariant } from "@/types";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
icon: string;
|
||||||
|
variant: ComponentVariant;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<Props>();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<span :class="['floating-icon', props.variant]">
|
||||||
|
<i :class="['fa', `fa-${props.icon}`]" aria-hidden="true" />
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
@import "../scss/variables";
|
||||||
|
|
||||||
|
.floating-icon {
|
||||||
|
display: block;
|
||||||
|
float: left;
|
||||||
|
margin: 0 0.5em 0.25em 0;
|
||||||
|
|
||||||
|
i {
|
||||||
|
font-size: 3em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@each $variant, $color in $variant-colors {
|
||||||
|
.floating-icon.#{$variant} {
|
||||||
|
color: $color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -133,6 +133,14 @@ function centerOnCoordinates() {
|
||||||
function renderMap() {
|
function renderMap() {
|
||||||
const { layers, defaultLayers } = getLayers();
|
const { layers, defaultLayers } = getLayers();
|
||||||
createMap(defaultLayers, layers);
|
createMap(defaultLayers, layers);
|
||||||
|
if (
|
||||||
|
map &&
|
||||||
|
configStore.getConfig.otherCommunityInfo.showBorderForDebugging
|
||||||
|
) {
|
||||||
|
new L.Polygon(
|
||||||
|
configStore.getConfig.otherCommunityInfo.localCommunityPolygon
|
||||||
|
).addTo(map);
|
||||||
|
}
|
||||||
centerOnCoordinates();
|
centerOnCoordinates();
|
||||||
updateMarker();
|
updateMarker();
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,6 +19,7 @@ import {
|
||||||
ComponentVariant,
|
ComponentVariant,
|
||||||
hasOwnProperty,
|
hasOwnProperty,
|
||||||
} from "@/types";
|
} from "@/types";
|
||||||
|
import FloatingIcon from "@/components/FloatingIcon.vue";
|
||||||
import ErrorCard from "@/components/ErrorCard.vue";
|
import ErrorCard from "@/components/ErrorCard.vue";
|
||||||
import ButtonGroup from "@/components/form/ButtonGroup.vue";
|
import ButtonGroup from "@/components/form/ButtonGroup.vue";
|
||||||
import ValidationForm from "@/components/form/ValidationForm.vue";
|
import ValidationForm from "@/components/form/ValidationForm.vue";
|
||||||
|
@ -27,8 +28,12 @@ import { route, RouteName } from "@/router";
|
||||||
import RouteButton from "@/components/form/RouteButton.vue";
|
import RouteButton from "@/components/form/RouteButton.vue";
|
||||||
import { ApiError } from "@/utils/Api";
|
import { ApiError } from "@/utils/Api";
|
||||||
import NodeCoordinatesInput from "@/components/nodes/NodeCoordinatesInput.vue";
|
import NodeCoordinatesInput from "@/components/nodes/NodeCoordinatesInput.vue";
|
||||||
|
import OutsideOfCommunityConfirmationForm from "@/components/nodes/OutsideOfCommunityConfirmationForm.vue";
|
||||||
import CheckboxInput from "@/components/form/CheckboxInput.vue";
|
import CheckboxInput from "@/components/form/CheckboxInput.vue";
|
||||||
import InfoCard from "@/components/InfoCard.vue";
|
import InfoCard from "@/components/InfoCard.vue";
|
||||||
|
import { isPointInPolygon } from "geolib";
|
||||||
|
import { parseToFloat } from "@/utils/Numbers";
|
||||||
|
import { forConstraint } from "../../shared/validation/validator";
|
||||||
|
|
||||||
const configStore = useConfigStore();
|
const configStore = useConfigStore();
|
||||||
const nodeStore = useNodeStore();
|
const nodeStore = useNodeStore();
|
||||||
|
@ -63,6 +68,9 @@ const nicknameModel = ref("" as Nickname);
|
||||||
const emailModel = ref("" as EmailAddress);
|
const emailModel = ref("" as EmailAddress);
|
||||||
const monitoringModel = ref(false);
|
const monitoringModel = ref(false);
|
||||||
|
|
||||||
|
const showOutsideOfCommunityForm = ref(false);
|
||||||
|
const confirmedOutsideOfCommunity = ref(false);
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if (props.hostname) {
|
if (props.hostname) {
|
||||||
hostnameModel.value = props.hostname;
|
hostnameModel.value = props.hostname;
|
||||||
|
@ -75,6 +83,30 @@ onMounted(() => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function isOutsideCommunity(): boolean {
|
||||||
|
if (!forConstraint(CONSTRAINTS.node.coords, false)(coordsModel.value)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const [lat, lng] = coordsModel.value.split(" ").map(parseToFloat);
|
||||||
|
|
||||||
|
return !isPointInPolygon(
|
||||||
|
{ lat, lng },
|
||||||
|
configStore.getConfig.otherCommunityInfo.localCommunityPolygon
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onConfirmOutsideOfCommunity() {
|
||||||
|
showOutsideOfCommunityForm.value = false;
|
||||||
|
confirmedOutsideOfCommunity.value = true;
|
||||||
|
|
||||||
|
await onSubmit();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onCancelOutsideOfCommunity() {
|
||||||
|
showOutsideOfCommunityForm.value = false;
|
||||||
|
window.scrollTo(0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
async function onSubmit() {
|
async function onSubmit() {
|
||||||
generalError.value = false;
|
generalError.value = false;
|
||||||
conflictErrorMessage.value = undefined;
|
conflictErrorMessage.value = undefined;
|
||||||
|
@ -82,6 +114,18 @@ async function onSubmit() {
|
||||||
// Make sure to re-render error message to trigger scrolling into view.
|
// Make sure to re-render error message to trigger scrolling into view.
|
||||||
await nextTick();
|
await nextTick();
|
||||||
|
|
||||||
|
if (
|
||||||
|
configStore.getConfig.otherCommunityInfo.showInfo &&
|
||||||
|
isOutsideCommunity() &&
|
||||||
|
!confirmedOutsideOfCommunity.value
|
||||||
|
) {
|
||||||
|
showOutsideOfCommunityForm.value = true;
|
||||||
|
} else {
|
||||||
|
await createNode();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createNode(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const node = await nodeStore.create({
|
const node = await nodeStore.create({
|
||||||
hostname: hostnameModel.value,
|
hostname: hostnameModel.value,
|
||||||
|
@ -115,7 +159,17 @@ async function onSubmit() {
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<ValidationForm novalidate="" ref="form" @submit="onSubmit">
|
<OutsideOfCommunityConfirmationForm
|
||||||
|
v-if="showOutsideOfCommunityForm"
|
||||||
|
@confirm="onConfirmOutsideOfCommunity"
|
||||||
|
@cancel="onCancelOutsideOfCommunity"
|
||||||
|
/>
|
||||||
|
<ValidationForm
|
||||||
|
v-if="!showOutsideOfCommunityForm"
|
||||||
|
novalidate=""
|
||||||
|
ref="form"
|
||||||
|
@submit="onSubmit"
|
||||||
|
>
|
||||||
<h2>Neuen Knoten anmelden</h2>
|
<h2>Neuen Knoten anmelden</h2>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
|
@ -216,7 +270,10 @@ async function onSubmit() {
|
||||||
<fieldset v-if="configStore.getConfig.monitoring.enabled">
|
<fieldset v-if="configStore.getConfig.monitoring.enabled">
|
||||||
<h3>Möchtest Du automatisiert Status-E-Mails bekommen?</h3>
|
<h3>Möchtest Du automatisiert Status-E-Mails bekommen?</h3>
|
||||||
|
|
||||||
<i class="monitoring-icon fa fa-heartbeat" aria-hidden="true" />
|
<FloatingIcon
|
||||||
|
icon="heartbeat"
|
||||||
|
:variant="ComponentVariant.PRIMARY"
|
||||||
|
/>
|
||||||
|
|
||||||
<p class="help-block">
|
<p class="help-block">
|
||||||
Du kannst Dich automatisiert benachrichtigen lassen, sobald
|
Du kannst Dich automatisiert benachrichtigen lassen, sobald
|
||||||
|
@ -266,8 +323,6 @@ async function onSubmit() {
|
||||||
</InfoCard>
|
</InfoCard>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
<h1>TODO: Check community bounds</h1>
|
|
||||||
|
|
||||||
<ButtonGroup
|
<ButtonGroup
|
||||||
:align="ComponentAlignment.RIGHT"
|
:align="ComponentAlignment.RIGHT"
|
||||||
:button-size="ButtonSize.SMALL"
|
:button-size="ButtonSize.SMALL"
|
||||||
|
@ -294,13 +349,4 @@ async function onSubmit() {
|
||||||
</ValidationForm>
|
</ValidationForm>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped></style>
|
||||||
@import "../../scss/variables";
|
|
||||||
|
|
||||||
.monitoring-icon {
|
|
||||||
float: left;
|
|
||||||
margin: 0 0.25em 0.125em 0;
|
|
||||||
font-size: 3em;
|
|
||||||
color: $variant-color-primary;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
|
@ -0,0 +1,112 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useConfigStore } from "@/stores/config";
|
||||||
|
import ButtonGroup from "@/components/form/ButtonGroup.vue";
|
||||||
|
import { ButtonSize, ComponentAlignment, ComponentVariant } from "@/types";
|
||||||
|
import ActionButton from "@/components/form/ActionButton.vue";
|
||||||
|
import ValidationForm from "@/components/form/ValidationForm.vue";
|
||||||
|
import FloatingIcon from "@/components/FloatingIcon.vue";
|
||||||
|
import { onMounted } from "vue";
|
||||||
|
|
||||||
|
const configStore = useConfigStore();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: "cancel"): void;
|
||||||
|
(e: "confirm"): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
window.scrollTo(0, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
function onCancel() {
|
||||||
|
emit("cancel");
|
||||||
|
}
|
||||||
|
|
||||||
|
function onSubmit() {
|
||||||
|
emit("confirm");
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<ValidationForm class="outside-of-community-form" @submit="onSubmit">
|
||||||
|
<h2>
|
||||||
|
Dein Router steht außerhalb des
|
||||||
|
{{ configStore.getConfig.community.name }} Gebiets
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<strong
|
||||||
|
>Erst mal großartig, dass du Freifunk machen möchtest!</strong
|
||||||
|
>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<FloatingIcon icon="community" :variant="ComponentVariant.SECONDARY" />
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Freifunk steht u.a. für Denzentralität. Deshalb wäre es klasse wenn
|
||||||
|
Du Dich einer Community in Deiner Nähe anschließt oder vielleicht
|
||||||
|
sogar
|
||||||
|
<a
|
||||||
|
href="https://freifunk.net/wie-mache-ich-mit/lokale-gruppe-gruenden/"
|
||||||
|
target="_blank"
|
||||||
|
>Deine eigene gründest</a
|
||||||
|
>. Da aller Anfang schwer ist, macht es für eben diesen vielleicht
|
||||||
|
Sinn sich erst mal einer
|
||||||
|
<a
|
||||||
|
href="https://freifunk.net/wie-mache-ich-mit/community-finden/"
|
||||||
|
target="_blank"
|
||||||
|
>nahegelegen Gruppe</a
|
||||||
|
>
|
||||||
|
anzuschließen. Auch wir von
|
||||||
|
{{ configStore.getConfig.community.name }} stehen Dir natürlich
|
||||||
|
gerne mit Rat und Tat zur Seite und mit den existierenden
|
||||||
|
Hilfsmitteln ist die technische Hürde auch nicht mehr ganz so
|
||||||
|
gigantisch.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Es gibt auch technische Gründe, die gegen eine Erweiterung über die
|
||||||
|
Grenzen von
|
||||||
|
{{ configStore.getConfig.community.name }} hinaus sprechen. Das
|
||||||
|
Problem mit Mesh-Netzen heute ist, dass sie nicht besonders gut
|
||||||
|
skalieren und mit jedem weiteren Router die Leistung nach unten
|
||||||
|
geht.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Im Moment kommen ständig neue Freifunk-Communities dazu. Welche
|
||||||
|
Communities es schon gibt, siehts Du
|
||||||
|
<a
|
||||||
|
href="https://freifunk.net/wie-mache-ich-mit/community-finden/"
|
||||||
|
target="_blank"
|
||||||
|
>in dieser Übersicht</a
|
||||||
|
>.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<ButtonGroup
|
||||||
|
:align="ComponentAlignment.CENTER"
|
||||||
|
:button-size="ButtonSize.MEDIUM"
|
||||||
|
>
|
||||||
|
<ActionButton
|
||||||
|
type="reset"
|
||||||
|
icon="times"
|
||||||
|
:variant="ComponentVariant.SECONDARY"
|
||||||
|
:size="ButtonSize.SMALL"
|
||||||
|
@click="onCancel"
|
||||||
|
>
|
||||||
|
Abbrechen
|
||||||
|
</ActionButton>
|
||||||
|
|
||||||
|
<ActionButton
|
||||||
|
type="submit"
|
||||||
|
icon="dot-circle-o"
|
||||||
|
:variant="ComponentVariant.INFO"
|
||||||
|
:size="ButtonSize.SMALL"
|
||||||
|
>
|
||||||
|
Knoten dennoch anmelden
|
||||||
|
</ActionButton>
|
||||||
|
</ButtonGroup>
|
||||||
|
</ValidationForm>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss"></style>
|
|
@ -1291,6 +1291,11 @@ functional-red-black-tree@^1.0.1:
|
||||||
resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327"
|
resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327"
|
||||||
integrity sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=
|
integrity sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=
|
||||||
|
|
||||||
|
geolib@^3.3.3:
|
||||||
|
version "3.3.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/geolib/-/geolib-3.3.3.tgz#17f5a0dcdc0b051bd631b66f7131d2c14c54a15b"
|
||||||
|
integrity sha512-YO704pzdB/8QQekQuDmFD5uv5RAwAf4rOUPdcMhdEOz+HoPWD0sC7Qqdwb+LAvwIjXVRawx0QgZlocKYh8PFOQ==
|
||||||
|
|
||||||
get-func-name@^2.0.0:
|
get-func-name@^2.0.0:
|
||||||
version "2.0.0"
|
version "2.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/get-func-name/-/get-func-name-2.0.0.tgz#ead774abee72e20409433a066366023dd6887a41"
|
resolved "https://registry.yarnpkg.com/get-func-name/-/get-func-name-2.0.0.tgz#ead774abee72e20409433a066366023dd6887a41"
|
||||||
|
|
Loading…
Reference in a new issue