chore: migrate to composition API and auth0-spa

This commit is contained in:
2020-01-21 15:51:51 +01:00
parent 565a3aa66e
commit e6c87e2f46
44 changed files with 2489 additions and 1585 deletions
+16 -10
View File
@@ -5,14 +5,14 @@
</v-card-title> </v-card-title>
<v-container> <v-container>
<v-layout row wrap> <v-layout row wrap>
<v-flex xs12 sm6><strong>Datum:</strong> {{event.date}} ({{ weekday }})</v-flex> <v-flex xs12 sm6><strong class="mr-1" v-text="$t('events.date')" />{{event.date}} ({{ weekday }} {{ daysUntil }})</v-flex>
<v-flex xs12 sm6 v-if="event.time"><strong>Tid:</strong> {{event.time}}</v-flex> <v-flex xs12 sm6 v-if="event.time"><strong class="mr-1" v-text="$t('events.time')" />{{event.time}}</v-flex>
</v-layout> </v-layout>
<v-layout row wrap> <v-layout row wrap>
<v-flex xs12 sm6 md3><strong>Var:</strong><v-icon class="ml-1 mr-1" v-if="hasUser" v-on:click="toggleIgnore('danceHall', event.danceHall.name)" small title="Dölj">mdi-eye-off</v-icon>{{event.danceHall.name}}</v-flex> <v-flex xs12 sm6 md3><strong class="mr-1" v-text="$t('events.hall')" /><v-icon class="ml-1 mr-1" v-if="hasUser" v-on:click="toggleIgnore('danceHall', event.danceHall.name)" small :title="$t('events.hide')">mdi-eye-off</v-icon>{{event.danceHall.name}}</v-flex>
<v-flex xs12 sm6 md3><strong>Ort:</strong><v-icon class="ml-1 mr-1" v-if="hasUser" v-on:click="toggleIgnore('city', event.danceHall.city)" small title="Dölj">mdi-eye-off</v-icon>{{event.danceHall.city}}</v-flex> <v-flex xs12 sm6 md3><strong class="mr-1" v-text="$t('events.city')" /><v-icon class="ml-1 mr-1" v-if="hasUser" v-on:click="toggleIgnore('city', event.danceHall.city)" small :title="$t('events.hide')">mdi-eye-off</v-icon>{{event.danceHall.city}}</v-flex>
<v-flex xs12 sm6 md3><strong>Kommun:</strong><v-icon class="ml-1 mr-1" v-if="hasUser" v-on:click="toggleIgnore('municipality', event.danceHall.municipality)" small title="Dölj">mdi-eye-off</v-icon>{{event.danceHall.municipality}}</v-flex> <v-flex xs12 sm6 md3><strong class="mr-1" v-text="$t('events.municipality')" /><v-icon class="ml-1 mr-1" v-if="hasUser" v-on:click="toggleIgnore('municipality', event.danceHall.municipality)" small :title="$t('events.hide')">mdi-eye-off</v-icon>{{event.danceHall.municipality}}</v-flex>
<v-flex xs12 sm6 md3><strong>Län:</strong><v-icon class="ml-1 mr-1" v-if="hasUser" v-on:click="toggleIgnore('state', event.danceHall.state)" small title="Dölj">mdi-eye-off</v-icon>{{event.danceHall.state}}</v-flex> <v-flex xs12 sm6 md3><strong class="mr-1" v-text="$t('events.state')" /><v-icon class="ml-1 mr-1" v-if="hasUser" v-on:click="toggleIgnore('state', event.danceHall.state)" small :title="$t('events.hide')">mdi-eye-off</v-icon>{{event.danceHall.state}}</v-flex>
</v-layout> </v-layout>
<v-layout row wrap v-for="distance in event.distances" :key="event.origin"> <v-layout row wrap v-for="distance in event.distances" :key="event.origin">
<v-flex xs12 sm6> <v-flex xs12 sm6>
@@ -31,6 +31,8 @@
</template> </template>
<script> <script>
import dayjs from 'dayjs'
export default { export default {
props: { props: {
event: { event: {
@@ -46,10 +48,14 @@
required: true required: true
} }
}, },
computed: { setup(props) {
weekday() { const time = (props.event.time || '').split('-')[0].replace('.', ':')
return window.$nuxt.$moment(this.event.date).format('dddd'); const weekday = dayjs(props.event.date).format('dddd')
const daysUntil = dayjs(`${props.event.date} ${time}`).fromNow()
return {
weekday,
daysUntil
} }
} },
}; };
</script> </script>
+33
View File
@@ -0,0 +1,33 @@
<template>
<div>
<v-layout row wrap v-for="event in events.events" :key="event.id" v-if="events && events.events">
<v-flex xs12>
<Event :event="event" :has-user="hasUser" :toggleIgnore="toggleIgnore"/>
</v-flex>
</v-layout>
</div>
</template>
<script>
import Event from '../Event';
export default {
components: {
Event,
},
props: {
hasUser: {
type: Boolean,
required: true
},
toggleIgnore: {
type: Function,
required: true
},
events: {
type: Object,
required: true
},
},
}
</script>
+136 -183
View File
@@ -1,249 +1,202 @@
<template> <template>
<div> <div :key="isAuthenticated">
<app-loader :show="isLoading" /> <v-container fluid grid-list-md class="app-fade-in" :key="range">
<app-loader v-if="isSubmitting" overlay> <v-layout row wrap v-if="!isAuthenticated">
<p>
{{submitInfo}}
</p>
</app-loader>
<v-container fluid grid-list-md v-if="isReady || isSubmitting || isSubmitted || isRefreshing" class="app-fade-in">
<v-layout row wrap v-if="!hasUser">
<v-flex xs12> <v-flex xs12>
<p><b>OBS! Logga in för att kunna filtrera listan</b></p> <p><b v-text="$t('events.login')"/></p>
</v-flex> </v-flex>
</v-layout> </v-layout>
<v-layout row wrap> <v-layout row wrap>
<v-flex xs12> <v-flex xs12>
<v-text-field <v-text-field
v-model="origin" v-model="origin"
label="Startpunkt" :label="$t('origins.origin')"
placeholder="Address/geokoordinat" :placeholder="$t('origins.geolocation')"
> >
<v-tooltip top slot="append-outer"> <v-tooltip top slot="append-outer">
<template v-slot:activator="{ on }"> <template v-slot:activator="{ on }">
<v-icon v-on="on" v-on:click="fetchAddress()">mdi-crosshairs-gps</v-icon> <v-icon v-on="on" v-on:click="fetchAddress()">mdi-crosshairs-gps</v-icon>
</template> </template>
<span>Hämta nuvarande position</span> <span v-text="$t('origins.fetchAddress')"/>
</v-tooltip> </v-tooltip>
<v-tooltip top slot="prepend" v-if="hasUser"> <v-tooltip top slot="prepend" v-if="isAuthenticated">
<template v-slot:activator="{ on }"> <template v-slot:activator="{ on }">
<v-icon v-on="on" :disabled="!origin" v-on:click="saveOrigin(origin)">mdi-bookmark-plus-outline</v-icon> <v-icon v-on="on" :disabled="!origin" v-on:click="saveOrigin(origin)">mdi-bookmark-plus-outline</v-icon>
</template> </template>
<span>Spara startpunkt</span> <span v-text="$t('origins.save')"/>
</v-tooltip> </v-tooltip>
</v-text-field> </v-text-field>
</v-flex> </v-flex>
</v-layout> </v-layout>
<v-layout row wrap> <v-layout row wrap>
<v-flex> <v-flex>
<v-btn-toggle v-if="$vuetify.breakpoint.smAndUp" v-model="range" mandatory @change="changeRange"> <v-btn-toggle v-if="$vuetify.breakpoint.smAndUp" v-model="range" mandatory>
<v-btn text v-for="r in ranges" :key="r.value" :value="r.value">{{r.name}}</v-btn> <v-btn text v-for="r in ranges" :key="r.value" :value="r.value" v-text="$t(`events.range.${r.value}`)"/>
</v-btn-toggle> </v-btn-toggle>
<v-select outline v-if="$vuetify.breakpoint.xsOnly" v-model="range" @change="changeRange" :items="ranges" item-text="name" item-value="value"></v-select> <v-select outline v-if="$vuetify.breakpoint.xsOnly" v-model="range" :items="ranges" item-text="name" item-value="value"/>
</v-flex>
</v-layout>
<v-layout row wrap v-for="event in events" :key="event.id">
<v-flex xs12>
<Event :event="event" :has-user="hasUser" :toggleIgnore="toggleIgnore" />
</v-flex> </v-flex>
</v-layout> </v-layout>
<list :events="data" :has-user="isAuthenticated" :toggleIgnore="toggleIgnore" />
</v-container> </v-container>
<v-snackbar <v-snackbar
v-model="snackbar" v-model="snackbar.active"
:color="snackColor" :color="snackbar.color"
:timeout="5000" :timeout="5000"
> >
{{ snackText }} {{ snackbar.text }}
</v-snackbar> </v-snackbar>
</div> </div>
</template> </template>
<script> <script>
import { useAuth } from '../../../plugins/auth'
import List from './List'
import { useRouter, useMutations } from '@u3u/vue-hooks'
import { computed, ref, watch } from '@vue/composition-api'
import { useLazyQuery, useMutation } from '../../../plugins/apollo'
import { import {
findEvents,
fetchAddress, fetchAddress,
findEvents,
saveOrigin,
toggleIgnoreBand, toggleIgnoreBand,
toggleIgnoreDanceHall,
toggleIgnoreCity, toggleIgnoreCity,
toggleIgnoreDanceHall,
toggleIgnoreMunicipality, toggleIgnoreMunicipality,
toggleIgnoreState, toggleIgnoreState
saveOrigin} from "~/utils/graph-client"; } from '../../../utils/graph-client'
import { useTranslation } from '../../../plugins/i18n'
import auth from "~/utils/auth";
import Event from "./Event";
export default { export default {
components: { components: {
Event List
}, },
data() { setup() {
return { const { setTitle } = useMutations(['setTitle'])
status: "loading", const { t } = useTranslation();
origin: undefined, setTitle(t('app.links.events'))
origins: [], const { loading: authLoading, isAuthenticated } = useAuth()
submitInfo: "", const { route, router } = useRouter();
submitError: "", const range = computed({get: () => route.value.query.range || 'ONE_WEEK', set: value => router.push(`/?range=${value}`)})
user: undefined, const [query, { loading, data, error, refetch }] = useLazyQuery(findEvents)
snackbar: false, watch(() => range.value, (r, o) => {
snackColor: "success", query({
snackText: "", variables: {
range: "ONE_WEEK", range: r,
ranges: [ includeOrigins: isAuthenticated.value
{name: "1 vecka", value: "ONE_WEEK"}, }
{name: "2 veckor", value: "TWO_WEEKS"}, })
{name: "1 månad", value: "ONE_MONTH"}, }, { lazy: false})
{name: "1 kvartal", value: "ONE_QUARTER"}, const submitting = ref(true)
{name: "1 år", value: "ONE_YEAR"} const ranges = [
] { name: '1 vecka', value: 'ONE_WEEK' },
}; { name: '2 veckor', value: 'TWO_WEEKS' },
}, { name: '1 månad', value: 'ONE_MONTH' },
computed: { { name: '1 kvartal', value: 'ONE_QUARTER' },
isLoading() { { name: '1 år', value: 'ONE_YEAR' }
return this.status === "loading"; ]
}, const snackbar = ref({ active: false, color: 'success', text: '' })
isReady() {
return this.status === "ready"; const origin = ref('')
}, const fetchEvents = () => {
isSubmitting() { const origins = [...(data.value.origins || [])]
return this.status === "submitting"; if (origin.value) {
}, origins.push(origin.value)
isSubmitted() {
return this.status === "submitted";
},
isRefreshing() {
return this.status === "refreshing";
},
hasUser() {
return this.user !== undefined && this.user !== null;
}
},
mounted() {
this.$store.commit('setTitle', 'Evenemang');
const {range} = this.$route.query;
this.range = range || "ONE_WEEK";
this.fetchUser();
this.fetchEvents();
},
watch: {
"$route.query"() {
const {range} = this.$route.query;
this.range = range;
this.fetchUser();
this.fetchEvents();
}
},
methods: {
fetchEvents (status) {
this.status = status || "loading";
const origins = this.origins;
if (this.origin) {
origins.push(this.origin);
} }
const variables = { query({
range: this.range, variables: {
origins: origins.length > 0 ? origins : null, range: range.value,
includeOrigins: this.hasUser origins,
}; includeOrigins: (isAuthenticated.value || false)
findEvents(variables) }
.then(this.eventsFetched) })
.catch(this.eventsFailed); }
},
eventsFetched(response) { const [doToggleIgnoreBand, {loading: ignoringBand, error: errorIgnoreBand}] = useMutation(toggleIgnoreBand)
if (response.errors) { const [doToggleIgnoreDanceHall, {loading: ignoringDanceHall, error: errorIgnoreDanceHall}] = useMutation(toggleIgnoreDanceHall)
throw new Error("Fetch failed"); const [doToggleIgnoreCity, {loading: ignoringCity, error: errorIgnoreCity}] = useMutation(toggleIgnoreCity)
const [doToggleIgnoreMunicipality, {loading: ignoringMunicipality, error: errorIgnoreMunicipality}] = useMutation(toggleIgnoreMunicipality)
const [doToggleIgnoreState, {loading: ignoringState, error: errorIgnoreState}] = useMutation(toggleIgnoreState)
const toggleIgnoreSuccess = (name) => {
return () => {
fetchEvents();
snackbar.value.color = 'success';
snackbar.value.text = `${name} har dolts`;
snackbar.value.active = true;
} }
this.events = response.data.events; }
this.origins = response.data.origins || [];
this.status = "ready"; const toggleIgnoreFailed = (name) => {
}, return () => {
eventsFailed() { snackbar.value.color = 'error';
this.status = "load-failed"; snackbar.value.text = `${name} kunde inte döljas`;
}, snackbar.value.active = true;
fetchUser() { }
this.user = auth.getUserInfo(); }
},
toggleIgnore(type, name) { const toggleIgnore = (type, name) => {
switch (type) { switch (type) {
case 'band': case 'band':
toggleIgnoreBand({name: name}) doToggleIgnoreBand({ variables: { name: name } })
.then(this.toggleIgnoreSuccess(name)) .then(toggleIgnoreSuccess(name))
.catch(this.toggleIgnoreFailed); .catch(toggleIgnoreFailed);
break; break;
case 'danceHall': case 'danceHall':
toggleIgnoreDanceHall({name: name}) doToggleIgnoreDanceHall({ variables: { name: name } })
.then(this.toggleIgnoreSuccess(name)) .then(toggleIgnoreSuccess(name))
.catch(this.toggleIgnoreFailed); .catch(toggleIgnoreFailed);
break; break;
case 'city': case 'city':
toggleIgnoreCity({name: name}) doToggleIgnoreCity({ variables: { name: name } })
.then(this.toggleIgnoreSuccess(name)) .then(toggleIgnoreSuccess(name))
.catch(this.toggleIgnoreFailed); .catch(toggleIgnoreFailed);
break; break;
case 'municipality': case 'municipality':
toggleIgnoreMunicipality({name: name}) doToggleIgnoreMunicipality({ variables: { name: name } })
.then(this.toggleIgnoreSuccess(name)) .then(toggleIgnoreSuccess(name))
.catch(this.toggleIgnoreFailed); .catch(toggleIgnoreFailed);
break; break;
case 'state': case 'state':
toggleIgnoreState({name: name}) doToggleIgnoreState({ variables: { name: name } })
.then(this.toggleIgnoreSuccess(name)) .then(toggleIgnoreSuccess(name))
.catch(this.toggleIgnoreFailed); .catch(toggleIgnoreFailed);
break; break;
} }
}, }
toggleIgnoreSuccess(name) {
return () => { const [doSaveOrigin, {loading: savingOrigin, error: errorSaveOrigin}] = useMutation(saveOrigin)
this.fetchEvents('refreshing'); const [doFetchAddress, {data: address, loading: fetchingAddress, error: errorFetchingAddress}] = useLazyQuery(fetchAddress)
this.snackColor = 'success'; const fetchAddressFn = () => {
this.snackText = `${name} har dolts`;
this.snackbar = true;
}
},
toggleIgnoreFailed(name) {
return () => {
this.snackColor = 'error';
this.snackText = `${name} kunde inte döljas`;
this.snackbar = true;
}
},
saveOrigin(origin) {
saveOrigin({origin: origin})
.then(saved => {
if (saved) {
this.origins = [];
this.origin = "";
this.fetchEvents('refreshing');
} else {
this.snackColor = 'error';
this.snackText = `${origin} kunde inte sparas`;
this.snackbar = true;
}
})
.catch(() => {
this.snackColor = 'error';
this.snackText = `${origin} kunde inte sparas`;
this.snackbar = true;
})
},
changeRange(range) {
this.$router.push(`/?range=${range}`);
},
fetchAddress() {
if (window.navigator) { if (window.navigator) {
this.submitInfo = 'Hämtar aktuell position';
this.status = 'submitting';
window.navigator.geolocation.getCurrentPosition(pos => { window.navigator.geolocation.getCurrentPosition(pos => {
fetchAddress({latlng: pos.coords.latitude + "," + pos.coords.longitude}) doFetchAddress({variables: {latlng: pos.coords.latitude + "," + pos.coords.longitude}})
.then(response => { .then(() => {
this.status = 'submitted'; origin.value = address.value.address;
this.origin = response.data.address;
this.fetchEvents('refreshing');
}) })
}) })
} }
} }
} const saveOriginFn = o => doSaveOrigin({variables: { origin: o }}).then(() => {
origin.value = ''
fetchEvents()
})
return {
authLoading,
isAuthenticated,
range,
data,
query: fetchEvents,
submitting,
ranges,
snackbar,
toggleIgnore,
origin,
fetchAddress: fetchAddressFn,
saveOrigin: saveOriginFn
}
},
}; };
</script> </script>
+7 -4
View File
@@ -2,14 +2,17 @@
<v-flex xs12 sm6 md4 lg3> <v-flex xs12 sm6 md4 lg3>
<v-card> <v-card>
<v-card-title> <v-card-title>
<span v-html="title"/> <span v-text="$tc(title, model.length)"/>
<v-spacer/>
<span v-html="model.length"/><span class="ml-1">st</span>
</v-card-title> </v-card-title>
<v-list> <v-list>
<v-list-item v-for="item in model" :key="item"> <v-list-item v-for="item in model" :key="item">
<v-list-item-action v-on:click="toggleIgnore(type, item)"> <v-list-item-action v-on:click="toggleIgnore(type, item)">
<v-icon>mdi-delete-outline</v-icon> <v-tooltip top slot="prepend">
<template v-slot:activator="{ on }">
<v-icon v-on="on">mdi-delete-outline</v-icon>
</template>
<span v-text="$t('filters.remove')"/>
</v-tooltip>
</v-list-item-action> </v-list-item-action>
<v-list-item-title v-html="item"/> <v-list-item-title v-html="item"/>
</v-list-item> </v-list-item>
+74 -101
View File
@@ -1,12 +1,6 @@
<template> <template>
<div> <div :key="isAuthenticated">
<app-loader :show="isLoading" /> <v-container fluid grid-list-md class="app-fade-in">
<app-loader v-if="isSubmitting" overlay>
<p>
{{submitInfo}}
</p>
</app-loader>
<v-container fluid grid-list-md v-if="isReady || isSubmitting || isSubmitted || isRefreshing" class="app-fade-in">
<v-layout row wrap> <v-layout row wrap>
<v-flex xs12> <v-flex xs12>
<v-card> <v-card>
@@ -14,14 +8,14 @@
fluid fluid
grid-list-md grid-list-md
> >
<v-layout row wrap> <v-layout row wrap v-if="!loading && data">
<list :model="bands" title="Band" type="band" :toggleIgnore="toggleIgnore"/> <list :model="data.bands" title="filters.band" type="band" :toggleIgnore="toggleIgnore" v-if="data.bands"/>
<v-flex xs12 sm6 md4 lg3> <v-flex xs12 sm6 md4 lg3>
<v-layout column align-content-start> <v-layout column>
<list :model="states" title="Län" type="state" :toggleIgnore="toggleIgnore"/> <list :model="data.states" title="filters.state" type="state" :toggleIgnore="toggleIgnore" v-if="data.states"/>
<list :model="municipalities" title="Kommun" type="municipality" :toggleIgnore="toggleIgnore"/> <list :model="data.municipalities" title="filters.municipality" type="municipality" :toggleIgnore="toggleIgnore" v-if="data.municipalities"/>
<list :model="cities" title="Stad" type="city" :toggleIgnore="toggleIgnore"/> <list :model="data.cities" title="filters.city" type="city" :toggleIgnore="toggleIgnore" v-if="data.cities"/>
<list :model="danceHalls" title="Danslokal" type="danceHall" :toggleIgnore="toggleIgnore"/> <list :model="data.danceHalls" title="filters.hall" type="danceHall" :toggleIgnore="toggleIgnore" v-if="data.danceHalls"/>
</v-layout> </v-layout>
</v-flex> </v-flex>
</v-layout> </v-layout>
@@ -30,6 +24,13 @@
</v-flex> </v-flex>
</v-layout> </v-layout>
</v-container> </v-container>
<v-snackbar
v-model="snackbar.active"
:color="snackbar.color"
:timeout="5000"
>
{{ snackbar.text }}
</v-snackbar>
</div> </div>
</template> </template>
@@ -44,110 +45,82 @@
} from "~/utils/graph-client"; } from "~/utils/graph-client";
import List from "./List"; import List from "./List";
import { useMutation, useQuery } from '../../../plugins/apollo'
import { ref } from '@vue/composition-api'
import { useAuth } from '../../../plugins/auth'
import { useMutations } from '@u3u/vue-hooks'
import { useTranslation } from '../../../plugins/i18n'
export default { export default {
components: { components: {
List List
}, },
data() { setup() {
return { const { setTitle } = useMutations(['setTitle'])
status: "loading", const { t } = useTranslation();
bands: [], setTitle(t('app.links.filters'))
danceHalls: [], const { loading: authLoading, isAuthenticated } = useAuth()
cities: [], const {data, loading, error, refetch} = useQuery(fetchFilters)
municipalities: [], const snackbar = ref({ active: false, color: 'success', text: '' })
states: [], const [doToggleIgnoreBand, {loading: ignoringBand, error: errorIgnoreBand}] = useMutation(toggleIgnoreBand)
submitInfo: "", const [doToggleIgnoreDanceHall, {loading: ignoringDanceHall, error: errorIgnoreDanceHall}] = useMutation(toggleIgnoreDanceHall)
snackbar: false, const [doToggleIgnoreCity, {loading: ignoringCity, error: errorIgnoreCity}] = useMutation(toggleIgnoreCity)
snackColor: "success", const [doToggleIgnoreMunicipality, {loading: ignoringMunicipality, error: errorIgnoreMunicipality}] = useMutation(toggleIgnoreMunicipality)
snackText: "", const [doToggleIgnoreState, {loading: ignoringState, error: errorIgnoreState}] = useMutation(toggleIgnoreState)
};
}, const toggleIgnoreSuccess = (name) => {
computed: { return () => {
isLoading() { refetch.value();
return this.status === "loading"; snackbar.value.color = 'success';
}, snackbar.value.text = t('filters.success', { name });
isReady() { snackbar.value.active = true;
return this.status === "ready";
},
isSubmitting() {
return this.status === "submitting";
},
isSubmitted() {
return this.status === "submitted";
},
isRefreshing() {
return this.status === "refreshing";
}
},
mounted() {
this.$store.commit('setTitle', 'Filter');
this.fetchFilters();
},
methods: {
fetchFilters(status) {
this.status = status || "loading";
fetchFilters()
.then(this.filtersFetched)
.catch(this.filtersFailed);
},
filtersFetched(response) {
if (response.errors) {
throw new Error("Fetch failed");
} }
this.bands = response.data.bands; }
this.danceHalls = response.data.danceHalls;
this.cities = response.data.cities; const toggleIgnoreFailed = (name) => {
this.municipalities = response.data.municipalities; return () => {
this.states = response.data.states; snackbar.value.color = 'error';
this.status = "ready"; snackbar.value.text = t('filters.failure', { name });
}, snackbar.value.active = true;
filtersFailed() { }
this.status = "load-failed"; }
},
toggleIgnore(type, name) { const toggleIgnore = (type, name) => {
switch (type) { switch (type) {
case 'band': case 'band':
toggleIgnoreBand({name: name}) doToggleIgnoreBand({ variables: { name: name } })
.then(this.toggleIgnoreSuccess(name)) .then(toggleIgnoreSuccess(name))
.catch(this.toggleIgnoreFailed); .catch(toggleIgnoreFailed);
break; break;
case 'danceHall': case 'danceHall':
toggleIgnoreDanceHall({name: name}) doToggleIgnoreDanceHall({ variables: { name: name } })
.then(this.toggleIgnoreSuccess(name)) .then(toggleIgnoreSuccess(name))
.catch(this.toggleIgnoreFailed); .catch(toggleIgnoreFailed);
break; break;
case 'city': case 'city':
toggleIgnoreCity({name: name}) doToggleIgnoreCity({ variables: { name: name } })
.then(this.toggleIgnoreSuccess(name)) .then(toggleIgnoreSuccess(name))
.catch(this.toggleIgnoreFailed); .catch(toggleIgnoreFailed);
break; break;
case 'municipality': case 'municipality':
toggleIgnoreMunicipality({name: name}) doToggleIgnoreMunicipality({ variables: { name: name } })
.then(this.toggleIgnoreSuccess(name)) .then(toggleIgnoreSuccess(name))
.catch(this.toggleIgnoreFailed); .catch(toggleIgnoreFailed);
break; break;
case 'state': case 'state':
toggleIgnoreState({name: name}) doToggleIgnoreState({ variables: { name: name } })
.then(this.toggleIgnoreSuccess(name)) .then(toggleIgnoreSuccess(name))
.catch(this.toggleIgnoreFailed); .catch(toggleIgnoreFailed);
break; break;
} }
}, }
toggleIgnoreSuccess(name) {
return () => { return {
this.fetchFilters('refreshing'); isAuthenticated,
this.snackColor = 'success'; loading,
this.snackText = `${name} har tagits bort`; data,
this.snackbar = true; snackbar,
} toggleIgnore
},
toggleIgnoreFailed(name) {
return () => {
this.snackColor = 'error';
this.snackText = `${name} kunde inte tas bort`;
this.snackbar = true;
}
} }
} }
} }
+58 -114
View File
@@ -1,41 +1,47 @@
<template> <template>
<div> <div :key="isAuthenticated">
<app-loader :show="isLoading" /> <v-container fluid grid-list-md class="app-fade-in">
<app-loader v-if="isSubmitting" overlay>
<p>
{{submitInfo}}
</p>
</app-loader>
<v-container fluid grid-list-md v-if="isReady || isSubmitting || isSubmitted" class="app-fade-in">
<v-layout row wrap> <v-layout row wrap>
<v-flex xs12> <v-flex xs12>
<v-text-field <v-text-field
v-model="origin" v-model="origin"
label="Startpunkt" :label="$t('origins.origin')"
placeholder="Address/geokoordinat" :placeholder="$t('origins.geolocation')"
> >
<v-tooltip top slot="append-outer"> <v-tooltip top slot="append-outer">
<template v-slot:activator="{ on }"> <template v-slot:activator="{ on }">
<v-icon v-on="on" v-on:click="fetchAddress()">mdi-crosshairs-gps</v-icon> <v-icon v-on="on" v-on:click="fetchAddress()">mdi-crosshairs-gps</v-icon>
</template> </template>
<span>Hämta nuvarande position</span> <span v-text="$t('origins.fetchAddress')"/>
</v-tooltip> </v-tooltip>
<v-tooltip top slot="prepend"> <v-tooltip top slot="prepend">
<template v-slot:activator="{ on }"> <template v-slot:activator="{ on }">
<v-icon v-on="on" :disabled="!origin" v-on:click="saveOrigin(origin)">mdi-bookmark-plus-outline</v-icon> <v-icon v-on="on" :disabled="!origin" v-on:click="saveOrigin(origin)">mdi-bookmark-plus-outline</v-icon>
</template> </template>
<span>Spara startpunkt</span> <span v-text="$t('origins.save')"/>
</v-tooltip> </v-tooltip>
</v-text-field> </v-text-field>
</v-flex> </v-flex>
</v-layout> </v-layout>
<v-layout row wrap v-for="origin in origins" :key="origin"> <v-layout row wrap v-for="origin in data.origins" :key="origin" v-if="data && data.origins">
<v-flex xs12> <v-flex xs12>
<v-icon v-on:click="removeOrigin(origin)">mdi-delete-outline</v-icon> <v-tooltip top slot="prepend">
<template v-slot:activator="{ on }">
<v-icon v-on="on" v-on:click="removeOrigin(origin)">mdi-delete-outline</v-icon>
</template>
<span v-text="$t('origins.remove')"/>
</v-tooltip>
<span>{{origin}}</span> <span>{{origin}}</span>
</v-flex> </v-flex>
</v-layout> </v-layout>
</v-container> </v-container>
<v-snackbar
v-model="snackbar.active"
:color="snackbar.color"
:timeout="5000"
>
{{ snackbar.text }}
</v-snackbar>
</div> </div>
</template> </template>
@@ -45,112 +51,50 @@
saveOrigin, saveOrigin,
removeOrigin, removeOrigin,
fetchAddress, fetchAddress,
} from "~/utils/graph-client"; } from "../../../utils/graph-client";
import { useLazyQuery, useMutation, useQuery } from '../../../plugins/apollo'
import { ref } from '@vue/composition-api'
import { useAuth } from '../../../plugins/auth'
import { useMutations } from '@u3u/vue-hooks'
import { useTranslation } from '../../../plugins/i18n'
export default { export default {
data() { setup() {
return { const { setTitle } = useMutations(['setTitle'])
status: "loading", const { t } = useTranslation();
origin: undefined, setTitle(t('app.links.origins'))
submitInfo: "", const { loading: authLoading, isAuthenticated } = useAuth()
snackbar: false, const {data, loading, error, refetch} = useQuery(findOrigins)
snackColor: "success", const snackbar = ref({ active: false, color: 'success', text: '' })
snackText: "", const [doSaveOrigin, {loading: savingOrigin, error: errorSaveOrigin}] = useMutation(saveOrigin)
}; const [doRemoveOrigin, {loading: removingOrigin, error: errorRemoveOrigin}] = useMutation(removeOrigin)
}, const [doFetchAddress, {data: address, loading: fetchingAddress, error: errorFetchingAddress}] = useLazyQuery(fetchAddress)
computed: { const origin = ref('')
isLoading() { const fetchAddressFn = () => {
return this.status === "loading";
},
isReady() {
return this.status === "ready";
},
isSubmitting() {
return this.status === "submitting";
},
isSubmitted() {
return this.status === "submitted";
},
},
mounted() {
this.$store.commit('setTitle', 'Startpunkter');
this.fetchOrigins();
},
methods: {
fetchOrigins() {
this.status = "loading";
findOrigins()
.then(this.originsFetched)
.catch(this.originsFailed);
},
originsFetched(response) {
if (response.errors) {
throw new Error("Fetch failed");
}
this.origins = response.data.origins;
this.status = "ready";
},
originsFailed() {
this.status = "load-failed";
},
saveOrigin(origin) {
this.submitInfo = 'Sparar startpunkt';
this.status = 'submitting';
saveOrigin({origin: origin})
.then(saved => {
this.status = 'submitted';
if (saved) {
this.origins = [];
this.origin = "";
this.fetchOrigins();
} else {
this.snackColor = 'error';
this.snackText = `${origin} kunde inte sparas`;
this.snackbar = true;
}
})
.catch(() => {
this.status = 'submit-failed';
this.snackColor = 'error';
this.snackText = `${origin} kunde inte sparas`;
this.snackbar = true;
})
},
removeOrigin(origin) {
this.submitInfo = 'Tar bort startpunkt';
this.status = 'submitting';
removeOrigin({origin: origin})
.then(removed => {
this.status = 'submitted';
if (removed) {
this.origins = [];
this.fetchOrigins();
} else {
this.snackColor = 'error';
this.snackText = `${origin} kunde inte tas bort`;
this.snackbar = true;
}
})
.catch(() => {
this.status = 'submit-failed';
this.snackColor = 'error';
this.snackText = `${origin} kunde inte tas bort`;
this.snackbar = true;
})
},
fetchAddress() {
if (window.navigator) { if (window.navigator) {
this.submitInfo = 'Hämtar aktuell position';
this.status = 'submitting';
window.navigator.geolocation.getCurrentPosition(pos => { window.navigator.geolocation.getCurrentPosition(pos => {
fetchAddress({latlng: pos.coords.latitude + "," + pos.coords.longitude}) doFetchAddress({variables: {latlng: pos.coords.latitude + "," + pos.coords.longitude}})
.then(response => { .then(() => {
this.status = 'submitted'; origin.value = address.value.address;
this.origin = response.data.address; })
})
}) })
} }
}, }
const saveOriginFn = o => doSaveOrigin({variables: { origin: o }}).then(() => {
refetch.value()
origin.value = ''
})
const removeOriginFn = o => doRemoveOrigin({variables: { origin: o }}).then(() => refetch.value())
return {
isAuthenticated,
loading,
data,
snackbar,
origin,
saveOrigin: saveOriginFn,
removeOrigin: removeOriginFn,
fetchAddress: fetchAddressFn
}
} }
} }
</script> </script>
+15
View File
@@ -0,0 +1,15 @@
<template>
<div>
<slot />
</div>
</template>
<script>
import { getDarkMode } from '../../utils/localStorage'
export default {
setup(props, context) {
context.root.$vuetify.theme.dark = getDarkMode()
}
}
</script>
+146 -105
View File
@@ -1,129 +1,170 @@
<template> <template>
<v-app> <v-app :key="$i18n.locale + isAuthenticated">
<v-navigation-drawer <themed>
v-model="left" <v-navigation-drawer
v-model="nav"
temporary temporary
app app
> >
<v-list dense> <v-list dense>
<v-list-item v-if="!hasUser" @click="() => { doLogin(); }">
<v-list-item> <v-list-item>
<v-list-item>Login</v-list-item> <v-list-item-action>
<v-switch v-model="darkMode"/>
</v-list-item-action>
<v-list-item-title>{{ $t('app.darkMode') }}</v-list-item-title>
</v-list-item> </v-list-item>
</v-list-item> <v-list-item @click="locale = 'en'">
<v-list-item avatar v-if="hasUser"> <v-list-item-title>In English 🇬🇧</v-list-item-title>
<v-list-item-avatar> </v-list-item>
<v-img :src="user.picture" :alt="user.name"></v-img> <v-list-item @click="locale = 'sv'">
</v-list-item-avatar> <v-list-item-title> Svenska 🇸🇪</v-list-item-title>
<v-list-item-content> </v-list-item>
<v-list-item-title v-html="user.name"></v-list-item-title> <v-list-item link v-if="!user" @click="doLogin">
</v-list-item-content> <v-list-item-title>{{ $t('app.login') }}</v-list-item-title>
</v-list-item> </v-list-item>
<v-list-item> <v-list-item v-if="isAuthenticated && user">
<v-list-item-action> <v-list-item-avatar>
<v-icon>mdi-calendar-outline</v-icon> <v-img :src="user.picture" :alt="user.name"/>
</v-list-item-action> </v-list-item-avatar>
<v-list-item-content> <v-list-item-content>
<nuxt-link to="/"><v-list-item-title>Evenemang</v-list-item-title></nuxt-link> <v-list-item-title v-html="user.name"/>
</v-list-item-content> </v-list-item-content>
</v-list-item> </v-list-item>
<v-list-item v-if="hasUser"> <v-list-item link to="/">
<v-list-item-action> <v-list-item-action>
<v-icon>mdi-home</v-icon> <v-icon>mdi-calendar-outline</v-icon>
</v-list-item-action> </v-list-item-action>
<v-list-item-content> <v-list-item-title>{{ $t('app.links.events') }}</v-list-item-title>
<nuxt-link to="/origins/"><v-list-item-title>Hantera startpunkter</v-list-item-title></nuxt-link> </v-list-item>
</v-list-item-content> <v-list-item link to="/origins/" v-if="isAuthenticated">
</v-list-item> <v-list-item-action>
<v-list-item v-if="hasUser"> <v-icon>mdi-home</v-icon>
<v-list-item-action> </v-list-item-action>
<v-icon>mdi-magnify</v-icon> <v-list-item-title>{{ $t('app.links.origins') }}</v-list-item-title>
</v-list-item-action> </v-list-item>
<v-list-item-content> <v-list-item link to="/filters/" v-if="isAuthenticated">
<nuxt-link to="/filters/"><v-list-item-title>Hantera filter</v-list-item-title></nuxt-link> <v-list-item-action>
</v-list-item-content> <v-icon>mdi-magnify</v-icon>
</v-list-item> </v-list-item-action>
<v-list-item v-if="hasUser"> <v-list-item-title>{{ $t('app.links.filters') }}</v-list-item-title>
<v-list-item-action> </v-list-item>
<v-icon>exit_to_app</v-icon> <v-list-item link v-if="isAuthenticated">
</v-list-item-action> <v-list-item-action>
<v-list-item-content> <v-icon>exit_to_app</v-icon>
<nuxt-link to="/logout/"><v-list-item-title>Logga ut</v-list-item-title></nuxt-link> </v-list-item-action>
</v-list-item-content> <v-list-item-content @click="doLogout">
</v-list-item> <v-list-item-title>{{ $t('app.logout') }}</v-list-item-title>
</v-list> </v-list-item-content>
</v-navigation-drawer> </v-list-item>
<v-app-bar app scroll-off-screen> </v-list>
<v-app-bar-nav-icon v-on:click="left = !left"></v-app-bar-nav-icon> </v-navigation-drawer>
<v-toolbar-title v-html="title"></v-toolbar-title> <v-app-bar app scroll-off-screen>
<v-spacer></v-spacer> <v-app-bar-nav-icon v-on:click="nav = !nav"/>
<v-toolbar-items> <v-toolbar-title v-html="title"/>
<v-list-item v-if="hasUser"> <v-spacer/>
<v-list-item-avatar> <v-toolbar-items>
<v-img :src="user.picture" :alt="user.name"></v-img> <v-list-item v-if="isAuthenticated && user">
</v-list-item-avatar> <v-list-item-avatar>
<v-list-item-content> <v-img :src="user.picture" :alt="user.name"/>
<v-list-item-title v-html="user.name"></v-list-item-title> </v-list-item-avatar>
</v-list-item-content> <v-list-item-content>
</v-list-item> <v-list-item-title v-html="user.name"/>
</v-toolbar-items> </v-list-item-content>
</v-app-bar> </v-list-item>
<v-content> </v-toolbar-items>
<v-container fluid> </v-app-bar>
<nuxt /> <v-content>
</v-container> <v-container fluid v-if="!loading">
</v-content> <nuxt/>
</v-container>
</v-content>
</themed>
</v-app> </v-app>
</template> </template>
<style lang="scss"> <style lang="scss">
// We need this line to import all global styling // We need this line to import all global styling
@import "assets/scss/global.scss"; @import "assets/scss/global.scss";
</style> </style>
<style lang="scss" scoped> <style lang="scss" scoped>
.layout { .layout {
background-color: white; background-color: white;
.log-out { .log-out {
cursor: pointer; cursor: pointer;
}
} }
}
</style> </style>
<script> <script>
import auth from "~/utils/auth"; import { useAuth } from '../plugins/auth'
import { computed, ref } from '@vue/composition-api'
import Themed from './components/themed'
import { setDarkMode } from '../utils/localStorage'
import { useRouter, useState } from '@u3u/vue-hooks'
import dayjs from 'dayjs'
import sv from 'dayjs/locale/sv'
export default { export default {
data() { components: {
Themed,
},
setup(props, context) {
const { router } = useRouter()
const { title } = useState(['title'])
const onRedirectCallback = appState => {
router.push(
appState && appState.targetUrl
? appState.targetUrl
: window.location.pathname
);
}
const { loading, isAuthenticated, user, loginWithRedirect, logout } = useAuth(onRedirectCallback)
const doLogin = () => {
loginWithRedirect.value();
}
const doLogout = () => {
logout.value({
returnTo: window.location.origin
});
}
const darkMode = computed({
get: () => context.root.$vuetify.theme.dark,
set: val => {
context.root.$vuetify.theme.dark = val
setDarkMode(context.root.$vuetify.theme.dark)
}
})
const locale = computed({
get: () => context.root.$i18n.locale,
set: locale => {
if (locale === 'en') {
dayjs.locale(locale)
} else if (locale === 'sv') {
dayjs.locale(sv)
}
context.root.$i18n.setLocaleCookie(locale)
context.root.$vuetify.lang.current = locale
context.root.$i18n.locale = locale
}
})
const nav = ref(false)
locale.value = context.root.$i18n.locale
return { return {
user: undefined, title,
left: false loading,
}; isAuthenticated,
}, user,
computed: { doLogin,
hasUser() { doLogout,
return this.user; darkMode,
}, locale,
title() { nav
return this.$store.state.title;
} }
}, },
watch: {
"$route.path"() {
this.fetchUser();
}
},
mounted() {
this.fetchUser();
},
methods: {
fetchUser() {
this.user = auth.getUserInfo();
},
doLogin() {
auth.triggerLogin({ returnUrl: this.$route.fullPath });
}
}
}; };
</script> </script>
-30
View File
@@ -1,30 +0,0 @@
import auth from '~/utils/auth';
// do not add trailing slashes to the excluded paths.
const excludedPaths = ['', '/authorize', '/logout'];
// This function is not SSR-compatible, and works since our code currently only runs on the client.
export default function ({ isHMR, route }) {
// If middleware is called from hot module replacement, ignore it
if (isHMR) {
return;
}
// support both trailing slash and non-trailing slash so it works the same
// when hosted from s3 and not.
let path = route.path;
if (path.endsWith('/')) {
path = path.slice(0, -1);
}
if (!excludedPaths.includes(path)) {
if (auth.isExpired()) {
// if the auth has just expired - try to log in silently.
auth.triggerSilentLogin({ returnUrl: route.fullPath });
}
else if (!auth.isAuthenticated()) {
// otherwise, if the user is not authenticated, trigger a full login.
auth.triggerLogin({ returnUrl: route.fullPath });
}
}
}
+62 -16
View File
@@ -1,7 +1,11 @@
module.exports = { import translations from './translations'
css: [ import numberFormats from './translations/numberFormats'
'vuetify/dist/vuetify.css',
], export default {
env: {
graphqlApi: process.env.GRAPHQL_API,
},
mode: 'spa',
head: { head: {
link: [ link: [
{rel: 'apple-touch-icon', sizes: '180x180', href: '/apple-touch-icon.png'}, {rel: 'apple-touch-icon', sizes: '180x180', href: '/apple-touch-icon.png'},
@@ -18,21 +22,63 @@ module.exports = {
{name: 'viewport', content: 'width=device-width, initial-scale=1, user-scalable=no, minimal-ui'} {name: 'viewport', content: 'width=device-width, initial-scale=1, user-scalable=no, minimal-ui'}
], ],
}, },
mode: 'spa',
buildModules: [
['@nuxtjs/vuetify', { /* module options */ }]
],
modules: [ modules: [
'nuxt-i18n',
'@nuxtjs/vuetify',
['@nuxtjs/moment', { locales: ['sv'], defaultLocale: 'sv' }], ['@nuxtjs/moment', { locales: ['sv'], defaultLocale: 'sv' }],
], ],
plugins: [ i18n: {
{src: '~/plugins/vuetify.js', ssr: false}, strategy: 'prefix_and_default',
{src: '~/plugins/graph-routing.js', ssr: false}, detectBrowserLanguage: {
{src: '~/plugins/app-components.js', ssr: false}, useCookie: true,
{src: '~/plugins/vue-lazyload.js', ssr: false}, cookieKey: 'nuxt_i18n_redirected',
{src: '~/plugins/vue-numeral-filter.js', ssr: false} alwaysRedirect: true,
fallbackLocale: 'sv'
},
locales: [
{
code: 'en',
iso: 'en-US'
},
{
code: 'sv',
iso: 'sv-SE'
}
],
defaultLocale: 'sv',
vueI18n: {
fallbackLocale: 'sv',
messages: translations,
numberFormats
}
},
vuetify: {
optionsPath: './vuetify.options.js'
},
css: [
'vuetify/dist/vuetify.css',
'~/assets/scss/global.scss',
], ],
router: { plugins: [
middleware: ['auth'] '~/plugins/composition',
'~/plugins/hooks',
'~/plugins/i18n',
'~/plugins/vue-numeral-filter.js'
],
build: {
babel: {
presets({ isServer }) {
return [
[
require.resolve('@nuxt/babel-preset-app'),
// require.resolve('@nuxt/babel-preset-app-edge'), // For nuxt-edge users
{
buildTarget: isServer ? 'server' : 'client',
corejs: { version: 3 }
}
]
]
}
}
} }
}; };
+21 -16
View File
@@ -2,38 +2,44 @@
"name": "dancefinder-app", "name": "dancefinder-app",
"version": "1.0.0", "version": "1.0.0",
"engines": { "engines": {
"node": ">=10" "node": ">=12"
}, },
"author": "Joakim Olsson <joakim@unbound.se>", "author": "Joakim Olsson <joakim@unbound.se>",
"private": true, "private": true,
"dependencies": { "dependencies": {
"@auth0/auth0-spa-js": "^1.5.0",
"@nuxtjs/moment": "^1.1.0", "@nuxtjs/moment": "^1.1.0",
"apollo-link": "^1.2.6", "@nuxtjs/vuetify": "^1.9.0",
"apollo-link-error": "^1.1.5", "@u3u/vue-hooks": "^2.0.1",
"apollo-link-http": "^1.5.9", "@vue/composition-api": "^0.3.4",
"auth0-js": "^9.9.0", "apollo": "^2.17.4",
"apollo-cache-inmemory": "^1.6.3",
"apollo-client": "^2.6.4",
"apollo-link": "^1.2.12",
"apollo-link-context": "^1.0.18",
"apollo-link-error": "^1.1.11",
"apollo-link-http": "^1.5.15",
"core-js": "3",
"dayjs": "^1.8.19",
"eslint": "^5.1.0", "eslint": "^5.1.0",
"eslint-plugin-vue": "^4.5.0", "eslint-plugin-vue": "^4.5.0",
"graphql": "^14.1.1", "graphql": "^14.4.2",
"graphql-tag": "^2.10.1", "graphql-tag": "^2.10.1",
"lodash": "^4.17.10",
"mem": "^4.0.0",
"moment": "^2.24.0", "moment": "^2.24.0",
"node-sass": "^4.12.0", "node-sass": "^4.12.0",
"nuxt": "^2.0.0", "nuxt": "^2.8.1",
"s-ago": "^1.3.0", "nuxt-i18n": "^6.0.1",
"sass-loader": "^7.0.3", "sass-loader": "^7.0.3",
"snyk": "^1.258.2", "snyk": "^1.258.2",
"vue": "^2.5.22", "vue": "^2.6.10",
"vue-lazyload": "^1.2.6",
"vue-numeral-filter": "^1.1.1", "vue-numeral-filter": "^1.1.1",
"vuetify": "^2.1.9" "vuetify": "^2.1.9"
}, },
"scripts": { "scripts": {
"dev": "NODE_ENV=development node server/index.js", "dev": "nuxt",
"build": "nuxt build", "build": "nuxt build",
"generate": "nuxt generate", "generate": "nuxt generate",
"lint": "echo NYI", "lint": "eslint --ext .js,.vue --ignore-path .gitignore .",
"precommit": "yarn lint", "precommit": "yarn lint",
"prepush": "yarn test", "prepush": "yarn test",
"start": "node server/index.js", "start": "node server/index.js",
@@ -44,9 +50,8 @@
"prepublish": "yarn run snyk-protect" "prepublish": "yarn run snyk-protect"
}, },
"devDependencies": { "devDependencies": {
"@nuxtjs/vuetify": "^1.9.0", "@babel/runtime-corejs3": "^7.8.3",
"cypress": "^3.1.0", "cypress": "^3.1.0",
"express-http-proxy": "^1.5.0",
"wait-on": "^3.2.0" "wait-on": "^3.2.0"
}, },
"snyk": true "snyk": true
-48
View File
@@ -1,48 +0,0 @@
<template>
<div>
<app-loader v-if="!authFailed" overlay>
<h2>Authenticating...</h2>
</app-loader>
<app-message v-if="authFailed" :description="err.errorDescription" message="Failed to authenticate.">
<template slot="extras">
<Button type="primary" @click="triggerLogin">Log in</Button>
</template>
</app-message>
</div>
</template>
<script>
import auth from "~/utils/auth";
export default {
data() {
return {
authFailed: false,
err: {}
};
},
mounted() {
auth
.handleAuthentication()
.then(this.authorized)
.catch(this.unauthorized);
},
methods: {
authorized({ returnUrl }) {
this.$router.replace(returnUrl);
},
unauthorized(err) {
this.authFailed = true;
this.err = err;
},
triggerLogin() {
const errorState =
this.err && this.err.state ? JSON.parse(this.err.state) : null;
const returnUrl =
errorState && errorState.returnUrl ? errorState.returnUrl : "/";
auth.triggerLogin({ returnUrl });
}
}
};
</script>
+3 -1
View File
@@ -4,14 +4,16 @@
<script> <script>
import Filters from "~/components/pages/filters"; import Filters from "~/components/pages/filters";
import { useTranslation } from '../plugins/i18n'
export default { export default {
components: { components: {
Filters Filters
}, },
head() { head() {
const { t } = useTranslation()
return { return {
title: "Dancefinder - Filter" title: t('filters.title')
}; };
} }
}; };
+3 -1
View File
@@ -4,14 +4,16 @@
<script> <script>
import Events from "~/components/pages/events"; import Events from "~/components/pages/events";
import { useTranslation } from '../plugins/i18n'
export default { export default {
components: { components: {
Events Events
}, },
head() { head() {
const { t } = useTranslation()
return { return {
title: "Dancefinder - Evenemang" title: t('events.title')
}; };
} }
}; };
-17
View File
@@ -1,17 +0,0 @@
<template>
<app-message message="You are logged out.">
<h1 slot="icon">🎉</h1>
</app-message>
</template>
<script>
import auth from "~/utils/auth";
export default {
layout: "default",
mounted() {
auth.logout();
this.$router.replace('/')
}
};
</script>
+3 -1
View File
@@ -4,14 +4,16 @@
<script> <script>
import Origins from "~/components/pages/origins"; import Origins from "~/components/pages/origins";
import { useTranslation } from '../plugins/i18n'
export default { export default {
components: { components: {
Origins Origins
}, },
head() { head() {
const { t } = useTranslation()
return { return {
title: "Dancefinder - Startpunkter" title: t('origins.title')
}; };
} }
}; };
+167
View File
@@ -0,0 +1,167 @@
import { ApolloClient } from 'apollo-client'
import { createHttpLink } from 'apollo-link-http'
import { InMemoryCache } from 'apollo-cache-inmemory';
import { setContext } from 'apollo-link-context'
import { useAuth } from './auth'
import { reactive, toRefs } from '@vue/composition-api'
let instance = null
const apiUrl = process.env.graphqlApi || '/query'
const httpLink = createHttpLink({
uri: apiUrl
})
const getToken = async (options) => {
const { getTokenSilently, isAuthenticated } = useAuth()
if (isAuthenticated.value) {
return await getTokenSilently.value(options)
} else {
return options
}
};
const authLink = setContext(async (_, { headers }) => {
const token = await getToken()
return {
headers: {
...headers,
authorization: token ? `Bearer ${token}` : '',
}
};
})
const client = new ApolloClient({
link: authLink.concat(httpLink),
cache: new InMemoryCache(),
defaultOptions: {
watchQuery: {
fetchPolicy: 'cache-and-network',
},
},
})
instance = client
export const useMutation = (mutation, options) => {
const opts = options;
const doMutate = options => new Promise((resolve, reject) => {
out.loading = true
instance.mutate({
mutation,
...opts,
...options
})
.then(result => {
out.loading = false
out.data = result.data
resolve(result)
})
.catch(e => {
out.loading = false
out.error = e
reject(e)
})
})
const out = reactive({
data: {},
error: null,
loading: false,
})
return [doMutate, toRefs(out)]
}
export const useLazyQuery = (query, options) => {
const opts = options
let watchedQuery = null
const doQuery = options => new Promise((resolve, reject) => {
out.loading = true
let effectiveOptions = {
query,
...(opts || {}),
...(options || {})
}
watchedQuery = instance.watchQuery(effectiveOptions)
watchedQuery.subscribe(({loading, data}) => {
out.loading = loading
out.data = data || {}
out.error = null
resolve(data)
}, error => {
out.loading = false
out.error = error
reject(error)
})
})
const refetch = variables => {
const opts = {}
if (variables) (
opts.variables = variables
)
doQuery(opts)
}
const startPolling = interval => doQuery({pollInterval: interval})
const stopPolling = () => {
if (watchedQuery) {
watchedQuery.stopPolling()
}
}
const out = reactive({
data: {},
error: null,
loading: false,
refetch,
startPolling,
stopPolling
})
return [doQuery, toRefs(out)]
}
export const useQuery = (query, options) => {
const [doQuery, out] = useLazyQuery(query, options)
doQuery()
return out
}
// import { execute, makePromise, ApolloLink, Observable } from 'apollo-link';
// import { HttpLink } from 'apollo-link-http';
// const { includeCredentials } = require('./middleware');
// import { onError } from 'apollo-link-error';
//
// const defaultGraphUri = process.env.graphqlApi || 'https://shiny-gateway.unbound.se';
// const httpLink = new HttpLink({ uri: defaultGraphUri, fetch: includeCredentials, credentials: 'same-origin' });
// const errorLink = onError(({ graphQLErrors, networkError, operation, forward }) => {
// if (graphQLErrors) {
// console.log('GraphQL errors:', graphQLErrors);
// // for (let err of graphQLErrors) {
// // switch (err.extensions.code) {
// // case 'UNAUTHENTICATED':
// // // error code is set to UNAUTHENTICATED
// // // when AuthenticationError thrown in resolver
// //
// // // modify the operation context with a new token
// // }
// // }
// }
// if (networkError) {
// if (networkError.statusCode === 401) {
// return new Observable(observer => {
// // webAuth.checkSession(() => {
// const subscriber = {
// next: observer.next.bind(observer),
// error: observer.error.bind(observer),
// complete: observer.complete.bind(observer)
// };
//
// // Retry last failed request
// forward(operation).subscribe(subscriber);
// // }, (err) => {
// // console.log(err);
// // observer.error(err)
// // });
// });
// }
// }
// }
// );
-8
View File
@@ -1,8 +0,0 @@
import Vue from 'vue';
import AppLoader from '~/components/common/app-loader';
import AppMessage from '~/components/common/app-message';
import AppLazyBackground from '~/components/common/app-lazy-background';
Vue.component('app-loader', AppLoader);
Vue.component('app-message', AppMessage);
Vue.component('app-lazy-background', AppLazyBackground);
+125
View File
@@ -0,0 +1,125 @@
import createAuth0Client from '@auth0/auth0-spa-js'
import { reactive, toRefs } from '@vue/composition-api'
/** Define a default action to perform after authentication */
const DEFAULT_REDIRECT_CALLBACK = () =>
window.history.replaceState({}, document.title, window.location.pathname);
let instance;
const params = (new URL(window.location)).searchParams
const domain = params.get('domain') || 'unbound.eu.auth0.com'
export const useAuth = (onRedirectCallback = DEFAULT_REDIRECT_CALLBACK) => {
if (instance) {
return toRefs(instance)
}
const options = {
domain: domain,
client_id: 'orQfnvCPUR5C3mJkKoiWLQHOVQsBn60e',
audience: 'http://dancefinder.unbound.se',
redirect_uri: window.location.origin,
}
instance = reactive({
loading: false,
isAuthenticated: false,
user: {},
auth0Client: null,
popupOpen: false,
error: null,
/** Authenticates the user using a popup window */
loginWithPopup: async o => {
this.popupOpen = true;
try {
await instance.auth0Client.loginWithPopup(o);
} catch (e) {
// eslint-disable-next-line
console.error(e);
} finally {
instance.popupOpen = false;
}
instance.user = await instance.auth0Client.getUser();
instance.isAuthenticated = true;
},
/** Handles the callback when logging in using a redirect */
handleRedirectCallback: async () => {
instance.loading = true;
try {
await instance.auth0Client.handleRedirectCallback();
instance.user = await instance.auth0Client.getUser();
instance.isAuthenticated = true;
} catch (e) {
instance.error = e;
} finally {
instance.loading = false;
}
},
/** Authenticates the user using the redirect method */
loginWithRedirect: o => {
return instance.auth0Client.loginWithRedirect(o);
},
/** Returns all the claims present in the ID token */
getIdTokenClaims: o => {
return instance.auth0Client.getIdTokenClaims(o);
},
/** Returns the access token. If the token is invalid or missing, a new one is retrieved */
getTokenSilently: o => {
return instance.auth0Client.getTokenSilently(o);
},
/** Gets the access token using a popup window */
getTokenWithPopup: o => {
return instance.auth0Client.getTokenWithPopup(o);
},
/** Logs the user out and removes their session on the authorization server */
logout: o => {
return instance.auth0Client.logout(o);
}
})
const fetchUser = () => {
instance.auth0Client.isAuthenticated()
.then(a => {
instance.isAuthenticated = a
instance.auth0Client.getUser()
.then(u => {
instance.user = u
instance.loading = false;
});
});
}
// Create a new instance of the SDK client using members of the given options object
createAuth0Client(options)
.then(client => {
instance.loading = true
instance.auth0Client = client
try {
// If the user is returning to the app after authentication..
if (
window.location.search.includes('code=') &&
window.location.search.includes('state=')
) {
// handle the redirect and retrieve tokens
instance.auth0Client.handleRedirectCallback()
.then(appState => {
// Notify subscribers that the redirect callback has happened, passing the appState
// (useful for retrieving any pre-authentication state)
onRedirectCallback(appState);
// Initialize our internal authentication state
fetchUser()
})
} else {
fetchUser()
}
} catch (e) {
instance.error = e;
} finally {
instance.loading = false
}
})
return toRefs(instance)
}
+4
View File
@@ -0,0 +1,4 @@
import Vue from 'vue';
import VueCompositionApi from '@vue/composition-api';
Vue.use(VueCompositionApi);
-15
View File
@@ -1,15 +0,0 @@
export default ({ app }) => {
app.router.beforeEach((to, from, next) => {
// keep the graphql api url variable on all navigation,
// if it is actually present.
let target;
if (!to.query.graph && from.query.graph) {
target = {
path: to.path,
query: { ...to.query, graph: from.query.graph },
};
}
next(target);
});
}
+4
View File
@@ -0,0 +1,4 @@
import Vue from 'vue';
import hooks from '@u3u/vue-hooks';
Vue.use(hooks);
+14
View File
@@ -0,0 +1,14 @@
let i18n = null
let localePath = null
export const useTranslation = () => {
return {
t: i18n.t.bind(i18n),
localePath
}
}
export default ({app}) => {
i18n = app.i18n
localePath = app.localePath
}
-6
View File
@@ -1,6 +0,0 @@
import Vue from 'vue';
import VueLazyload from 'vue-lazyload';
Vue.use(VueLazyload, {
lazyComponent: true,
});
-39
View File
@@ -1,39 +0,0 @@
const express = require('express');
const proxy = require('express-http-proxy');
const { Nuxt, Builder } = require('nuxt');
//const { addGraphMiddleware } = require('graph-mock');
const app = express();
const host = process.env.HOST || '127.0.0.1';
const port = process.env.PORT || 3000;
// Import and set Nuxt.js options
let config = require('../nuxt.config.js');
config.dev = process.env.NODE_ENV !== 'production';
const nuxt = new Nuxt(config);
// Start build process in dev mode
if (config.dev) {
const builder = new Builder(nuxt);
builder.build();
}
// Add graphql mocking middleware
//addGraphMiddleware(app);
app.use('/query', proxy('localhost:6080', {
proxyReqPathResolver: function (req) {
return '/query';
}
}
)
);
// Give nuxt middleware to express
app.use(nuxt.render);
// Start express server
app.listen(port, host);
console.log(`Server is listening on http://${host}:${port}`); // eslint-disable-line
+24
View File
@@ -0,0 +1,24 @@
export default {
en: {
title: 'Dancefinder',
login: 'Login',
logout: 'Log out',
darkMode: 'Dark mode',
links: {
events: "Events",
filters: "Filters",
origins: "Origins",
}
},
sv: {
title: 'Dancefinder',
login: 'Logga in',
logout: 'Logga ut',
darkMode: 'Mörkt läge',
links: {
events: "Evenemang",
filters: "Hantera filter",
origins: "Hantera startpunkter",
},
}
}
+38
View File
@@ -0,0 +1,38 @@
export default {
en: {
title: 'Dancefinder - Events',
login: 'NB! Login to handle filtering of the list',
range: {
ONE_WEEK: '1 week',
TWO_WEEKS: '2 weeks',
ONE_MONTH: '1 month',
ONE_QUARTER: '1 quarter',
ONE_YEAR: '1 year'
},
date: 'Date:',
time: 'Time:',
hall: 'Where:',
city: 'City:',
municipality: 'Municipality:',
state: 'State:',
hide: 'Hide'
},
sv: {
title: 'Dancefinder - Evenemang',
login: 'OBS! Logga in för att kunna filtrera listan',
range: {
ONE_WEEK: '1 vecka',
TWO_WEEKS: '2 veckor',
ONE_MONTH: '1 månad',
ONE_QUARTER: '1 kvartal',
ONE_YEAR: '1 år'
},
date: 'Datum:',
time: 'Tid:',
hall: 'Var:',
city: 'Stad:',
municipality: 'Kommun:',
state: 'Län:',
hide: 'Dölj'
}
}
+24
View File
@@ -0,0 +1,24 @@
export default {
en: {
title: 'Dancefinder - Filters',
band: 'No bands | 1 band | {count} bands',
hall: 'No dance halls | 1 dance hall | {count} dance halls',
city: 'No cities | 1 city | {count} cities',
municipality: 'No municipalities | 1 municipality | {count} municipalities',
state: 'No states | 1 state | {count} states',
remove: 'Remove filter',
success: 'Filter for {name} is removed',
failure: 'Filter for {name} could not be removed'
},
sv: {
title: 'Dancefinder - Filter',
band: 'Inga band | 1 band | {count} band',
hall: 'Inga danslokaler | 1 danslokal | {count} danslokaler',
city: 'Ingen stad | 1 stad | {count} städer',
municipality: 'Ingen kommun | 1 kommun | {count} kommuner',
state: 'Inget län | 1 län | {count} län',
remove: 'Ta bort filter',
success: 'Filtrering av {name} har tagits bort',
failure: 'Filtrering av {name} kunde inte tas bort'
}
}
+19
View File
@@ -0,0 +1,19 @@
import app from './app'
import events from './events'
import filters from './filters'
import origins from './origins'
export default {
en: {
app: app.en,
events: events.en,
filters: filters.en,
origins: origins.en,
},
sv: {
app: app.sv,
events: events.sv,
filters: filters.sv,
origins: origins.sv,
},
}
+12
View File
@@ -0,0 +1,12 @@
export default {
'en': {
currency: {
style: 'currency', currency: 'USD'
}
},
'sv': {
currency: {
style: 'currency', currency: 'SEK'
}
}
}
+18
View File
@@ -0,0 +1,18 @@
export default {
en: {
title: 'Dancefinder - Origins',
origin: 'Origin',
geolocation: 'Address/geolocation (WGS84)',
fetchAddress: 'Fetch current position',
save: 'Save origin',
remove: 'Remove origin'
},
sv: {
title: 'Dancefinder - Startpunkter',
origin: 'Startpunkt',
geolocation: 'Address/geokoordinat (WGS84)',
fetchAddress: 'Hämta nuvarande position',
save: 'Spara startpunkt',
remove: 'Ta bort startpunkt'
}
}
+37
View File
@@ -0,0 +1,37 @@
export default {
sv: {
close: 'Stäng',
dataIterator: {
pageText: '{0}-{1} av {2}',
noResultsText: 'Inga matchande resultat',
loadingText: 'Laddar...'
},
dataTable: {
itemsPerPageText: 'Rader per sida:',
ariaLabel: {
sortDescending: ': Sorterad minskande. Aktivera för att ta bort sortering.',
sortAscending: ': Sorterad ökande. Aktivera för att sortera minskande.',
sortNone: ': Ej sorterad. Aktivera för att sortera ökande.'
}
},
dataFooter: {
itemsPerPageText: 'Per sida:',
itemsPerPageAll: 'Alla',
nextPage: 'Nästa sida',
prevPage: 'Föregående sida',
firstPage: 'Första sidan',
lastPage: 'Sista sidan'
},
datePicker: {
itemsSelected: '{0} valda'
},
noDataText: 'Ingen data tillgänglig',
carousel: {
prev: 'Föregående yta',
next: 'Nästa yta'
},
calendar: {
moreEvents: '{0} fler'
}
}
}
-124
View File
@@ -1,124 +0,0 @@
import auth0 from 'auth0-js';
import {
storeStateAndNonce,
clearStateAndNonce,
getStateAndNonce,
storeAuth,
getIdToken,
getUserInfo,
clear,
getExpiresAt,
getAccessToken,
} from './storage';
export default class AuthenticationClient {
/**
* Instantiates an authentication client.
* You should only need one per app and authentication method.
* @param {Object} auth0Config An auth0 configuration object, as per their API.
*/
constructor(auth0Config) {
this.config = auth0Config;
this.webAuth = new auth0.WebAuth(auth0Config);
}
logout() {
clear();
}
getUserInfo() {
return getUserInfo();
}
isAuthenticated() {
return !!getIdToken();
}
isExpired() {
const expiresAt = getExpiresAt();
return new Date().getTime() > expiresAt && this.isAuthenticated();
}
/**
* Triggers a login by redirecting to auth0.
*/
triggerLogin(state) {
// clear the state and nonce when triggering a login - otherwise hash parsing wont work.
clearStateAndNonce();
this.webAuth.authorize({ state: JSON.stringify(state) });
}
triggerSilentLogin(state) {
if (!state) {
throw new Error('You must specify a state.');
}
const nonce = new Date().getTime().toString();
// before we trigger the silent login - store the state and nonce in localstorage.
storeStateAndNonce(state, nonce);
let url = this.webAuth.client.buildAuthorizeUrl({
...this.config,
nonce,
state: JSON.stringify(state),
});
url += '&prompt=none';
window.location.href = url;
}
checkSession(resolve, reject) {
const state = {returnUrl: window.location.href};
const nonce = new Date().getTime().toString();
storeStateAndNonce(state, nonce);
this.webAuth.checkSession({state: JSON.stringify(state), nonce: nonce}, (err, result) => {
if (err) {
return reject(err || 'Re-authentication failed');
} else {
return this.storeSession(result, resolve, reject);
}
});
}
storeSession(authResult, resolve, reject) {
this.webAuth.client.userInfo(authResult.accessToken, (err, user) => {
if (err) {
// if any error happens when fetching user info - nuke all session info
this.logout();
return reject('Authentication failed');
}
const expiresAt = JSON.stringify(authResult.expiresIn * 1000 + new Date().getTime());
storeAuth({ ...authResult, user, expiresAt });
return resolve(JSON.parse(authResult.state));
});
}
handleAuthentication() {
return new Promise((resolve, reject) => {
// retrieve stored state and nonce from localstorage
const { state, nonce } = getStateAndNonce();
// however, if there is no state and nonce stored - do not provide any param to parseHash.
// Otherwise, the non-silent logins will fail due to invalid state.
const parseHashParam = state && nonce ? { state, nonce } : undefined;
this.webAuth.parseHash(parseHashParam, (err, authResult) => {
if (authResult && authResult.accessToken && authResult.idToken) {
// If we fail to either set the session or store user info - reject and clear the session.
try {
return this.storeSession(authResult, resolve, reject);
} catch (error) {
return reject(error || 'Authentication failed');
}
} else {
return reject(err || 'Authentication failed');
}
});
});
}
idToken() {
return getIdToken();
}
accessToken() {
return getAccessToken();
}
}
-152
View File
@@ -1,152 +0,0 @@
import AuthClient, { __RewireAPI__ as rewire } from '.'; // eslint-disable-line
import { __RewireAPI__ as storageRewire } from './storage'; // eslint-disable-line
describe('Auth module', () => {
const STORAGE = {
ACCESS: storageRewire.__get__('STORAGE_ACCESS'),
ID: storageRewire.__get__('STORAGE_ID'),
EXPIRES: storageRewire.__get__('STORAGE_EXPIRES'),
USER: storageRewire.__get__('STORAGE_USER'),
STATE: storageRewire.__get__('STORAGE_STATE'),
NONCE: storageRewire.__get__('STORAGE_NONCE'),
};
let authZero;
let TEST_DATE;
let webAuth;
// populates the storage with some mock data to mimic a login
const populateStorage = () => {
localStorage.setItem(STORAGE.ACCESS, 'foo');
localStorage.setItem(STORAGE.ID, 'foo');
localStorage.setItem(STORAGE.EXPIRES, 'foo');
localStorage.setItem(STORAGE.USER, '{"foo": "bar"}');
localStorage.setItem(STORAGE.STATE, 'foo');
localStorage.setItem(STORAGE.NONCE, 'foo');
};
beforeEach(() => {
// freeze time for this test suite - we're going back to the 90s!
TEST_DATE = new Date('1999');
global.Date = jest.fn(() => TEST_DATE);
// mop up current localStorage before each run - it will not clean itself
localStorage.clear();
authZero = {
authorize: jest.fn(),
parseHash: jest.fn(),
client: {
buildAuthorizeUrl: jest.fn(() => 'https://example.com'),
userInfo: jest.fn(),
},
};
rewire.__set__('auth0', { WebAuth: () => authZero });
webAuth = new AuthClient();
});
it('stores user info on successful login', (done) => {
const state = { returnUrl: '/foo' };
const authResult = {
accessToken: 'foo',
idToken: 'bar',
expiresIn: 86400,
state: JSON.stringify(state),
};
const userInfo = { foo: 'bar' };
authZero.parseHash = jest.fn((options, cb) => {
cb(null, authResult);
});
authZero.client.userInfo = jest.fn((token, cb) => {
cb(null, userInfo);
});
return webAuth.handleAuthentication()
.then((result) => {
const expiresAt = JSON.stringify(authResult.expiresIn * 1000 + TEST_DATE.getTime());
expect(authZero.parseHash).toBeCalled();
expect(localStorage.getItem(STORAGE.ACCESS)).toEqual(authResult.accessToken);
expect(localStorage.getItem(STORAGE.ID)).toEqual(authResult.idToken);
expect(localStorage.getItem(STORAGE.EXPIRES)).toEqual(expiresAt);
expect(localStorage.getItem(STORAGE.USER)).toEqual(JSON.stringify(userInfo));
// verify that the state we sent in is what we get back
expect(result).toEqual(state);
done();
});
});
it('clears state and nonce from storage on normal login attempt', () => {
populateStorage();
webAuth.triggerLogin({ returnUrl: '/' });
expect(localStorage.getItem(STORAGE.NONCE)).toBeFalsy();
expect(localStorage.getItem(STORAGE.STATE)).toBeFalsy();
});
it('sets correct localstorage content on silent login attempt', () => {
const state = { returnUrl: '/some-path/', foo: 'bar', ham: 34 };
const serializedState = JSON.stringify(state);
webAuth.triggerSilentLogin(state);
expect(localStorage.getItem(STORAGE.NONCE)).toEqual(TEST_DATE.getTime().toString());
expect(localStorage.getItem(STORAGE.STATE)).toEqual(serializedState);
});
it('rejects failed logins', (done) => {
const authResult = {
error: 'failed to authenticate',
};
authZero.parseHash = jest.fn((options, cb) => {
cb(null, authResult);
});
return webAuth.handleAuthentication()
.catch(() => {
expect(authZero.parseHash).toBeCalled();
expect(localStorage.length).toBe(0);
done();
});
});
it('rejects logins followed by failed user fetches', (done) => {
const authResult = {
accessToken: 'foo',
idToken: 'bar',
expiresIn: 86400,
};
authZero.parseHash = jest.fn((options, cb) => {
cb(null, authResult);
});
authZero.client.userInfo = jest.fn((token, cb) => {
cb('Credentials invalid');
});
return webAuth.handleAuthentication()
.catch(() => {
expect(authZero.parseHash).toBeCalled();
expect(authZero.client.userInfo).toBeCalled();
expect(localStorage.length).toBe(0);
done();
});
});
it('clears session on logout', () => {
populateStorage();
webAuth.logout();
expect(localStorage.length).toBe(0);
});
it('indicates authenticated and expired users based on expiry time', () => {
populateStorage();
localStorage.setItem(STORAGE.EXPIRES, global.Date().getTime() - 1000);
expect(webAuth.isAuthenticated()).toBe(true);
expect(webAuth.isExpired()).toBe(true);
localStorage.setItem(STORAGE.EXPIRES, global.Date().getTime());
expect(webAuth.isAuthenticated()).toBe(true);
expect(webAuth.isExpired()).toBe(false);
localStorage.setItem(STORAGE.EXPIRES, global.Date().getTime() + 1000);
expect(webAuth.isAuthenticated()).toBe(true);
expect(webAuth.isExpired()).toBe(false);
});
});
-47
View File
@@ -1,47 +0,0 @@
const STORAGE_ACCESS = 'access_token';
const STORAGE_ID = 'id_token';
const STORAGE_EXPIRES = 'expires_at';
const STORAGE_USER = 'user_info';
const STORAGE_STATE = 'auth-state';
const STORAGE_NONCE = 'auth-nonce';
/**
* Removes all auth specific items from storage.
*/
export const clear = () => {
localStorage.removeItem(STORAGE_ACCESS);
localStorage.removeItem(STORAGE_ID);
localStorage.removeItem(STORAGE_EXPIRES);
localStorage.removeItem(STORAGE_USER);
localStorage.removeItem(STORAGE_STATE);
localStorage.removeItem(STORAGE_NONCE);
};
export const storeAuth = ({
accessToken, idToken, user, expiresAt,
}) => {
localStorage.setItem(STORAGE_ACCESS, accessToken);
localStorage.setItem(STORAGE_ID, idToken);
localStorage.setItem(STORAGE_EXPIRES, expiresAt);
localStorage.setItem(STORAGE_USER, JSON.stringify(user));
};
export const storeStateAndNonce = (state, nonce) => {
localStorage.setItem(STORAGE_STATE, JSON.stringify(state));
localStorage.setItem(STORAGE_NONCE, nonce);
};
export const getStateAndNonce = () => ({
state: localStorage.getItem(STORAGE_STATE),
nonce: localStorage.getItem(STORAGE_NONCE),
});
export const clearStateAndNonce = () => {
localStorage.removeItem(STORAGE_STATE);
localStorage.removeItem(STORAGE_NONCE);
};
export const getUserInfo = () => JSON.parse(localStorage.getItem(STORAGE_USER));
export const getIdToken = () => localStorage.getItem(STORAGE_ID);
export const getAccessToken = () => localStorage.getItem(STORAGE_ACCESS);
export const getExpiresAt = () => JSON.parse(localStorage.getItem(STORAGE_EXPIRES));
-18
View File
@@ -1,18 +0,0 @@
import AuthClient from './auth-client';
const getRedirectUri = () => {
return location ? `${location.origin}/authorize/` : "http://localhost:3000/authorize/";
};
const auth0Config = {
domain: "unbound.eu.auth0.com",
clientID: "orQfnvCPUR5C3mJkKoiWLQHOVQsBn60e",
redirectUri: getRedirectUri(),
audience: "http://dancefinder.unbound.se",
responseType: "token id_token",
scope: "openid profile email readwrite:settings"
};
const webAuth = new AuthClient(auth0Config);
export default webAuth;
-11
View File
@@ -1,11 +0,0 @@
const webAuth = require("../auth").default;
module.exports = {
includeCredentials: (uri, options) => {
const token = webAuth.accessToken();
if (token) {
options.headers['Authorization'] = 'Bearer ' + token;
}
return fetch(uri, options);
}
};
-37
View File
@@ -1,37 +0,0 @@
module.exports = {
toggleIgnoreBandMutation: `
mutation ToggleIgnoreBand($name: String!) {
ignore: ToggleIgnoreBand(name: $name)
}
`,
toggleIgnoreDanceHallMutation: `
mutation ToggleIgnoreDanceHall($name: String!) {
ignore: ToggleIgnoreDanceHall(name: $name)
}
`,
toggleIgnoreCityMutation: `
mutation ToggleIgnoreCity($name: String!) {
ignore: ToggleIgnoreCity(name: $name)
}
`,
toggleIgnoreMunicipalityMutation: `
mutation ToggleIgnoreMunicipality($name: String!) {
ignore: ToggleIgnoreMunicipality(name: $name)
}
`,
toggleIgnoreStateMutation: `
mutation ToggleIgnoreState($name: String!) {
ignore: ToggleIgnoreState(name: $name)
}
`,
saveOriginMutation: `
mutation SaveOrigin($origin: String!) {
saved: SaveOrigin(origin: $origin)
}
`,
removeOriginMutation: `
mutation RemoveOrigin($origin: String!) {
removed: RemoveOrigin(origin: $origin)
}
`
};
+36 -33
View File
@@ -1,34 +1,37 @@
import { createQuery } from './utils'; import gql from 'graphql-tag'
import {
toggleIgnoreBandMutation,
toggleIgnoreDanceHallMutation,
toggleIgnoreCityMutation,
toggleIgnoreMunicipalityMutation,
toggleIgnoreStateMutation,
saveOriginMutation,
removeOriginMutation
} from './mutationStrings';
/* eslint-disable max-len */ export const toggleIgnoreBand = gql`
export const toggleIgnoreBand = variables => { mutation ToggleIgnoreBand($name: String!) {
return createQuery(toggleIgnoreBandMutation, variables) ignore: ToggleIgnoreBand(name: $name)
}; }
export const toggleIgnoreDanceHall = variables => { `
return createQuery(toggleIgnoreDanceHallMutation, variables) export const toggleIgnoreDanceHall = gql`
}; mutation ToggleIgnoreDanceHall($name: String!) {
export const toggleIgnoreCity = variables => { ignore: ToggleIgnoreDanceHall(name: $name)
return createQuery(toggleIgnoreCityMutation, variables) }
}; `
export const toggleIgnoreMunicipality = variables => { export const toggleIgnoreCity = gql`
return createQuery(toggleIgnoreMunicipalityMutation, variables) mutation ToggleIgnoreCity($name: String!) {
}; ignore: ToggleIgnoreCity(name: $name)
export const toggleIgnoreState = variables => { }
return createQuery(toggleIgnoreStateMutation, variables) `
}; export const toggleIgnoreMunicipality = gql`
export const saveOrigin = variables => { mutation ToggleIgnoreMunicipality($name: String!) {
return createQuery(saveOriginMutation, variables) ignore: ToggleIgnoreMunicipality(name: $name)
}; }
export const removeOrigin = variables => { `
return createQuery(removeOriginMutation, variables) export const toggleIgnoreState = gql`
}; mutation ToggleIgnoreState($name: String!) {
/* eslint-enable max-len */ ignore: ToggleIgnoreState(name: $name)
}
`
export const saveOrigin = gql`
mutation SaveOrigin($origin: String!) {
saved: SaveOrigin(origin: $origin)
}
`
export const removeOrigin = gql`
mutation RemoveOrigin($origin: String!) {
removed: RemoveOrigin(origin: $origin)
}
`
+47 -13
View File
@@ -1,14 +1,48 @@
import { createQuery } from './utils'; import gql from 'graphql-tag'
import {
eventsQuery,
originsQuery,
addressFromLatLngQuery,
filtersQuery,
} from './queryStrings';
/* eslint-disable max-len */ export const findEvents = gql`
export const findEvents = variables => createQuery(eventsQuery, variables); query events($range: Range $origins: [String!] $includeOrigins: Boolean!) {
export const findOrigins = () => createQuery(originsQuery); events: Events(range: $range origins: $origins) {
export const fetchAddress = variables => createQuery(addressFromLatLngQuery, variables); date
export const fetchFilters = () => createQuery(filtersQuery); time
/* eslint-enable max-len */ band {
name
}
danceHall {
name
city
municipality
state
}
extraInfo
distances {
origin
distance
duration
}
}
origins: Origins @include(if: $includeOrigins)
}
`;
export const findOrigins = gql`
query origins {
origins: Origins
}
`;
export const fetchAddress = gql`
query adressFromLatLng($latlng: String!) {
address: AddressFromLatLng(latlng: $latlng)
}
`;
export const fetchFilters = gql`
query {
bands: IgnoredBands
cities: IgnoredCities
states: IgnoredStates
danceHalls: IgnoredDanceHalls
municipalities: IgnoredMunicipalities
}
`;
-50
View File
@@ -1,50 +0,0 @@
export const eventQuery = `
events: Events(range: $range origins: $origins) {
date
time
band {
name
}
danceHall {
name
city
municipality
state
}
extraInfo
distances {
origin
distance
duration
}
}
`;
export const eventsQuery = `
query events($range: Range $origins: [String!] $includeOrigins: Boolean!) {
${eventQuery}
origins: Origins @include(if: $includeOrigins)
}
`;
export const originsQuery = `
query origins {
origins: Origins
}
`;
export const addressFromLatLngQuery = `
query adressFromLatLng($latlng: String!) {
address: AddressFromLatLng(latlng: $latlng)
}
`;
export const filtersQuery = `
query {
bands: IgnoredBands
cities: IgnoredCities
states: IgnoredStates
danceHalls: IgnoredDanceHalls
municipalities: IgnoredMunicipalities
}
`;
-51
View File
@@ -1,51 +0,0 @@
import { execute, makePromise, ApolloLink, Observable } from 'apollo-link';
import { HttpLink } from 'apollo-link-http';
import gql from 'graphql-tag';
const { includeCredentials } = require('./middleware');
import { onError } from 'apollo-link-error';
import { default as webAuth} from '../auth';
const defaultGraphUri = '/query';
const httpLink = new HttpLink({ uri: defaultGraphUri, fetch: includeCredentials, credentials: 'same-origin' });
const errorLink = onError(({ graphQLErrors, networkError, operation, forward }) => {
if (graphQLErrors) {
console.log('GraphQL errors:', graphQLErrors);
// for (let err of graphQLErrors) {
// switch (err.extensions.code) {
// case 'UNAUTHENTICATED':
// // error code is set to UNAUTHENTICATED
// // when AuthenticationError thrown in resolver
//
// // modify the operation context with a new token
// }
// }
}
if (networkError) {
if (networkError.statusCode === 401) {
return new Observable(observer => {
webAuth.checkSession(() => {
const subscriber = {
next: observer.next.bind(observer),
error: observer.error.bind(observer),
complete: observer.complete.bind(observer)
};
// Retry last failed request
forward(operation).subscribe(subscriber);
}, (err) => {
console.log(err);
observer.error(err)
});
});
}
}
}
);
export const createQuery = (query, variables) => { // eslint-disable-line
const operation = {
query: gql(query),
variables: variables
};
return makePromise(execute(ApolloLink.from([errorLink, httpLink]), operation));
};
+9
View File
@@ -0,0 +1,9 @@
const DARK_MODE_KEY = 'dancefinder-dark-mode'
const LOCALE_KEY = 'dancefinder-locale'
const getDarkMode = () => localStorage.getItem(DARK_MODE_KEY)
const setDarkMode = (mode) => localStorage.setItem(DARK_MODE_KEY, mode)
const getLocale = () => localStorage.getItem(LOCALE_KEY)
const setLocale = (locale) => localStorage.setItem(LOCALE_KEY, locale)
export { getDarkMode, setDarkMode, getLocale, setLocale };
+1334 -334
View File
File diff suppressed because it is too large Load Diff