Mostly UI stuff

Signed-off-by: Thomas Citharel <tcit@tcit.fr>
This commit is contained in:
Thomas Citharel 2018-07-09 14:19:24 +02:00
parent b60e8b7647
commit e170aa7f66
16 changed files with 450 additions and 296 deletions

2
.gitignore vendored
View file

@ -17,3 +17,5 @@ erl_crash.dump
.elixir_ls
/doc
priv/static/GeoLite2-City.mmdb
.vscode/

View file

@ -48,3 +48,12 @@ config :guardian, Guardian.DB,
schema_name: "guardian_tokens", # default
token_types: ["refresh_token"], # store all token types if not set
sweep_interval: 60 # default: 60 minutes
config :geolix,
databases: [
%{
id: :city,
adapter: Geolix.Adapter.MMDB2,
source: "priv/static/GeoLite2-City.mmdb"
}
]

View file

@ -9,6 +9,22 @@
enable-resize-watcher
>
<v-list dense>
<v-list-tile avatar v-if="$store.state.user">
<v-list-tile-avatar>
<img v-if="!getUser().actor.avatar_url"
class="img-circle elevation-7 mb-1"
src="https://picsum.photos/125/125/"
>
<img v-else
class="img-circle elevation-7 mb-1"
:src="getUser().actor.avatar_url"
>
</v-list-tile-avatar>
<v-list-tile-content @click="$router.push({name: 'Account', params: { name: getUser().actor.username }})">
<v-list-tile-title>{{ this.displayed_name }}</v-list-tile-title>
</v-list-tile-content>
</v-list-tile>
<template v-for="(item, i) in items" v-if="showMenuItem(item.role)">
<v-layout
row
@ -40,8 +56,8 @@
</v-navigation-drawer>
<NavBar v-bind="{toggleDrawer}"></NavBar>
<v-content>
<v-container fluid fill-height>
<v-layout xs-12>
<v-container fluid fill-height :class="{'px-0': $vuetify.breakpoint.xsOnly }">
<v-layout xs12>
<transition name="router">
<router-view></router-view>
</transition>
@ -140,8 +156,13 @@ export default {
},
toggleDrawer() {
this.drawer = !this.drawer;
}
},
},
computed: {
displayed_name() {
return this.$store.state.user.actor.display_name === null ? this.$store.state.user.actor.username : this.$store.state.user.actor.display_name
},
}
};
</script>

View file

@ -1,5 +1,4 @@
<template>
<v-container>
<v-layout row>
<v-flex xs12 sm6 offset-sm3>
<v-progress-circular v-if="loading" indeterminate color="primary"></v-progress-circular>
@ -171,7 +170,6 @@
</v-card>
</v-flex>
</v-layout>
</v-container>
</template>
<script>

View file

@ -129,7 +129,7 @@
.then(response => response.json())
.then((data) => {
this.loading = false;
this.$router.push({name: 'Event', params: {uuid: data.uuid}});
this.$router.push({name: 'Event', params: {uuid: data.data.uuid}});
}).catch((err) => {
Promise.resolve(err).then((err) => {
console.log('err creation', err);

View file

@ -1,21 +1,26 @@
<template>
<v-container>
<v-layout row>
<v-flex xs12 sm6 offset-sm3>
<span v-if="error">Error : event not found</span>
<v-progress-circular v-if="loading" indeterminate color="primary"></v-progress-circular>
<v-card v-if="!loading && !error">
<v-layout column class="media">
<v-card-media
src="https://picsum.photos/600/400/"
height="200px"
>
<v-container fill-height fluid>
<v-layout fill-height>
<v-flex xs12 align-end flexbox>
<v-card-title>
<v-btn icon @click="$router.go(-1)">
<v-btn icon @click="$router.go(-1)" class="white--text">
<v-icon>chevron_left</v-icon>
</v-btn>
<v-spacer></v-spacer>
<v-btn icon class="mr-3" v-if="event.organizer.id === $store.state.user.actor.id" :to="{ name: 'EditEvent', params: {id: event.id}}">
<v-btn icon class="mr-3 white--text" v-if="event.organizer.id === $store.state.user.actor.id" :to="{ name: 'EditEvent', params: {id: event.id}}">
<v-icon>edit</v-icon>
</v-btn>
<v-menu bottom left>
<v-btn icon slot="activator">
<v-btn icon slot="activator" class="white--text">
<v-icon>more_vert</v-icon>
</v-btn>
<v-list>
@ -28,18 +33,16 @@
</v-list>
</v-menu>
</v-card-title>
<v-container grid-list-md text-xs-center>
<v-card-media
src="https://picsum.photos/600/400/"
height="200px"
>
</v-flex>
</v-layout>
</v-container>
</v-card-media>
<v-container grid-list-md>
<v-layout row wrap>
<v-flex xs6>
<v-flex md10>
<v-spacer></v-spacer>
<span class="subheading grey--text">{{ event.begins_on | formatDay }}</span>
<h1 class="display-2">{{ event.title }}</h1>
<h1 class="display-1">{{ event.title }}</h1>
<div>
<router-link :to="{name: 'Account', params: { name: event.organizer.username } }">
<v-avatar size="25px">
@ -50,24 +53,67 @@
</router-link>
<span v-if="event.organizer">Organisé par {{ event.organizer.display_name ? event.organizer.display_name : event.organizer.username }}</span>
</div>
<p>
<vue-markdown :source="event.description" v-if="event.description" />
</p>
<!--<p><router-link :to="{ name: 'Account', params: {id: event.organizer.id} }"><span class="grey&#45;&#45;text">{{ event.organizer.username }}</span></router-link> organises {{ event.title }} <span v-if="event.address.addressLocality">in {{ event.address.addressLocality }}</span> on the {{ event.startDate | formatDate }}.</p>
<v-card-text v-if="event.description"><vue-markdown :source="event.description"></vue-markdown></v-card-text>-->
</v-flex>
<v-flex xs6>
<v-flex md2>
<p v-if="!actorIsParticipant()">Vous y allez ?
<span class="text--darken-2 grey--text">{{ event.participants.length }} personnes y vont.</span>
</p>
<p v-else>
Vous avez annoncé aller à cet événement.
</p>
<v-card-actions>
<v-btn color="success" v-if="!event.participants.map(participant => participant.id).includes($store.state.user.actor.id)" @click="joinEvent" class="btn btn-primary"><v-icon>check</v-icon> Join</v-btn>
<v-btn v-if="event.participants.map(participant => participant.id).includes($store.state.user.actor.id)" @click="leaveEvent" class="btn btn-primary">Leave</v-btn>
<v-btn v-if="!actorIsParticipant()" @click="joinEvent" color="success"><v-icon>check</v-icon> Join</v-btn>
<v-btn v-if="actorIsParticipant()" @click="leaveEvent" color="error">Leave</v-btn>
</v-card-actions>
</v-flex>
</v-layout>
</v-container>
<v-container fluid grid-list-md>
<v-subheader>Membres</v-subheader>
<v-layout row>
<v-flex xs2 v-for="actor in event.participants" :key="actor.uuid">
<v-divider></v-divider>
<v-container>
<v-layout row wrap>
<v-flex xs12 md4 order-md1>
<v-layout
column
fill-height
>
<v-list two-line>
<v-list-tile>
<v-list-tile-action>
<v-icon color="indigo">access_time</v-icon>
</v-list-tile-action>
<v-list-tile-content>
<v-list-tile-title>{{ event.begins_on | formatDate }}</v-list-tile-title>
<v-list-tile-sub-title>{{ event.ends_on | formatDate }}</v-list-tile-sub-title>
</v-list-tile-content>
</v-list-tile>
<v-divider inset></v-divider>
<v-list-tile>
<v-list-tile-action>
<v-icon color="indigo">place</v-icon>
</v-list-tile-action>
<v-list-tile-content>
<v-list-tile-title><span v-if="event.address_type === 'physical'">
{{ event.physical_address.streetAddress }}
</span></v-list-tile-title>
<v-list-tile-sub-title>Mobile</v-list-tile-sub-title>
</v-list-tile-content>
</v-list-tile>
</v-list>
</v-layout>
</v-flex>
<v-flex md8 xs12>
<p>
<h2>Details</h2>
<vue-markdown :source="event.description" v-if="event.description" :toc-first-level="3" />
</p>
<v-subheader>Participants</v-subheader>
<v-flex md2 v-for="actor in event.participants" :key="actor.uuid">
<router-link :to="{name: 'Account', params: { name: actor.username }}">
<v-avatar size="75px">
<img v-if="!actor.avatar"
@ -82,14 +128,13 @@
</router-link>
<span>{{ actor.username }}</span>
</v-flex>
</v-flex>
<span v-if="event.participants.length === 0">No participants yet.</span>
</v-layout>
</v-container>
</v-layout>
</v-card>
</v-flex>
</v-layout>
</v-container>
</template>
<script>
@ -136,11 +181,11 @@
console.log(data);
this.error = true;
this.loading = false;
})
});
});
},
joinEvent() {
eventFetch(`/events/${this.uuid}/join`, this.$store)
eventFetch(`/events/${this.uuid}/join`, this.$store, { method: 'POST' })
.then(response => response.json())
.then((data) => {
console.log(data);
@ -166,6 +211,10 @@
document.body.removeChild(link);
})
},
actorIsParticipant() {
const actorId = this.$store.state.user.actor.id;
return this.event.participants.map(participant => participant.id).includes(actorId) || actorId === this.event.organizer.id;
}
},
props: {
uuid: {
@ -180,6 +229,8 @@
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
<style>
.v-card__media__background {
filter: contrast(0.4);
}
</style>

View file

@ -18,9 +18,23 @@
</v-jumbotron>
<v-layout>
<v-flex xs12 sm8 offset-sm2>
<v-card>
<v-layout row wrap>
<v-flex xs4 v-for="event in events" :key="event.uuid">
<v-flex xs12 sm6>
<h1>Welcome back {{ $store.state.user.actor.username }}</h1>
</v-flex>
<v-flex xs12 sm6>
<v-layout align-center>
<span class="events-nearby title">Events nearby </span><v-text-field
solo
append-icon="place"
:value="ipLocation()"
></v-text-field>
</v-layout>
</v-flex>
</v-layout>
<v-card v-if="events.length > 0">
<v-layout row wrap>
<v-flex md4 v-for="event in events" :key="event.uuid">
<v-card :to="{ name: 'Event', params:{ uuid: event.uuid } }">
<v-card-media v-if="!event.image"
class="white--text"
@ -45,30 +59,16 @@
>
</v-avatar>
</router-link>
<span v-if="event.organizer">Organisé par {{ event.organizer.display_name }}</span>
<span v-if="event.organizer">Organisé par {{ event.organizer.display_name ? event.organizer.display_name : event.organizer.username }}</span>
</div>
</v-card-title>
</v-card>
</v-flex>
</v-layout>
</v-card>
</v-flex>
</v-layout>
<v-layout row>
<v-flex xs6>
<v-btn large @click="geoLocalize"><v-icon>my_location</v-icon>Me géolocaliser</v-btn>
</v-flex>
<v-flex xs6>
<vuetify-google-autocomplete
id="map"
append-icon="search"
classname="form-control"
placeholder="Start typing"
enable-geolocation
types="(cities)"
v-on:placechanged="getAddressData"
>
</vuetify-google-autocomplete>
<v-alert v-else :value="true" type="info">
No events found nearby {{ ipLocation() }}
</v-alert>
</v-flex>
</v-layout>
</v-container>
@ -76,12 +76,10 @@
<script>
import VuetifyGoogleAutocomplete from 'vuetify-google-autocomplete';
import ngeohash from 'ngeohash';
import eventFetch from "../api/eventFetch";
export default {
components: { VuetifyGoogleAutocomplete },
name: 'Home',
data() {
return {
@ -94,6 +92,8 @@ export default {
},
locations: [],
events: [],
city: {name: null},
country: {name: null},
};
},
created() {
@ -118,6 +118,8 @@ export default {
.then((response) => {
this.loading = false;
this.events = response.data;
this.city = response.city;
this.country = response.country;
});
},
geoLocalize() {
@ -147,6 +149,9 @@ export default {
viewEvent(event) {
this.$router.push({ name: 'Event', params: { uuid: event.uuid } })
},
ipLocation() {
return this.city.name ? this.city.name : this.country.name;
}
},
};
</script>
@ -157,4 +162,8 @@ export default {
border: 1px solid #dbdbdb;
color: rgba(0,0,0,.87);
}
.events-nearby {
margin-bottom: 25px;
}
</style>

View file

@ -23,6 +23,7 @@
:items="searchElement.items"
:search-input.sync="search"
v-model="searchSelect"
return-object
>
<template slot="item" slot-scope="data">
<template v-if="typeof data.item !== 'object'">
@ -30,7 +31,8 @@
</template>
<template v-else>
<v-list-tile-avatar>
<img :src="data.item.avatar">
<img :src="data.item.avatar" v-if="data.item.avatar">
<v-icon v-else>event</v-icon>
</v-list-tile-avatar>
<v-list-tile-content>
<v-list-tile-title v-html="username_with_domain(data.item)"></v-list-tile-title>
@ -45,6 +47,7 @@
:close-on-content-click="false"
:nudge-width="200"
v-model="notificationMenu"
v-if="getUser()"
>
<v-btn icon slot="activator">
<v-badge left color="red">
@ -73,6 +76,7 @@
</v-card>
</v-menu>
<v-btn flat @click="$router.push({name: 'Account', params: { name: getUser().actor.username }})" v-if="$store.state.user">{{ this.displayed_name }}</v-btn>
<v-btn v-else :to="{ name: 'Login' }">Se connecter</v-btn>
</v-toolbar>
</template>
@ -107,9 +111,9 @@
val && this.querySelections(val)
},
searchSelect(val) {
console.log(val);
console.log('searchSelect', val);
if (val.type === 'Event') {
this.$router.push({name: 'Event', params: { name: val.organizer.username, slug: val.slug }});
this.$router.push({name: 'Event', params: { uuid: val.uuid }});
} else if (val.type === 'Locality') {
this.$router.push({name: 'EventList', params: {location: val.geohash}});
} else {

View file

@ -9,6 +9,7 @@ defmodule Eventos.Events do
alias Eventos.Events.Event
alias Eventos.Events.Comment
alias Eventos.Actors.Actor
alias Eventos.Addresses.Address
@doc """
Returns the list of events.
@ -54,6 +55,19 @@ defmodule Eventos.Events do
)
end
import Geo.PostGIS
def find_close_events(lon, lat, radius \\ 50000) do # 50 000 meters -> 50 kms
ip_point = Geo.WKT.decode("SRID=4326;POINT(#{lon} #{lat})")
Repo.all(
from e in Event,
join: a in Address,
on: a.id == e.physical_address_id,
where: st_dwithin_in_meters(^ip_point, a.geom, ^radius),
preload: :organizer_actor
)
end
@doc """
Gets a single event.

View file

@ -7,15 +7,24 @@ defmodule EventosWeb.EventController do
alias Eventos.Events
alias Eventos.Events.Event
alias Eventos.Export.ICalendar
alias Eventos.Addresses
import Logger
require Logger
action_fallback EventosWeb.FallbackController
def index(conn, _params) do
events = Events.list_events()
render(conn, "index.json", events: events)
ip = "88.161.154.97"
Logger.debug(inspect Geolix.lookup(ip), pretty: true)
with %{city: %Geolix.Result.City{city: city, country: country, location: %Geolix.Record.Location{latitude: latitude, longitude: longitude}}} <- Geolix.lookup(ip) do
Logger.debug(inspect city)
Logger.debug(inspect [latitude, longitude])
distance = case city do
nil -> 500000
_ -> 50000
end
events = Events.find_close_events(longitude, latitude, distance)
render(conn, "index.json", events: events, coord: %{longitude: longitude, latitude: latitude, distance: distance}, city: city, country: country)
end
end
def create(conn, %{"event" => event_params}) do

View file

@ -0,0 +1,16 @@
defmodule EventosWeb.ParticipantController do
@moduledoc """
Controller for participants to an event
"""
use EventosWeb, :controller
alias Eventos.Events
def join(conn, %{"uuid" => uuid}) do
with event <- Events.get_event_by_uuid(uuid),
%{actor: actor} <- Guardian.Plug.current_resource(conn) do
participant = Events.create_participant(%{"event_id" => event.id, "actor_id" => actor.id, "role" => 1})
render(conn, "participant.json", %{participant: participant})
end
end
end

View file

@ -81,14 +81,12 @@ defmodule EventosWeb.Router do
patch "/events/:uuid", EventController, :update
put "/events/:uuid", EventController, :update
delete "/events/:uuid", EventController, :delete
post "/events/:uuid/join", ParticipantController, :join
post "/comments", CommentController, :create
patch "/comments/:uuid", CommentController, :update
put "/comments/:uuid", CommentController, :update
delete "/comments/:uuid", CommentController, :delete
#post "/events/:id/request", EventRequestController, :create_for_event
resources "/participant", ParticipantController
resources "/bots", BotController, except: [:new, :edit, :show, :index]
#resources "/requests", EventRequestController
post "/groups", GroupController, :create
post "/groups/:name/join", GroupController, :join
resources "/members", MemberController

View file

@ -5,8 +5,13 @@ defmodule EventosWeb.EventView do
use EventosWeb, :view
alias EventosWeb.{EventView, ActorView, GroupView, AddressView}
def render("index.json", %{events: events}) do
%{data: render_many(events, EventView, "event_simple.json")}
def render("index.json", %{events: events, coord: coord, city: city, country: country}) do
%{
data: render_many(events, EventView, "event_simple.json"),
coord: coord,
city: city,
country: country,
}
end
def render("show_simple.json", %{event: event}) do
@ -49,7 +54,7 @@ defmodule EventosWeb.EventView do
ends_on: event.ends_on,
uuid: event.uuid,
organizer: render_one(event.organizer_actor, ActorView, "acccount_basic.json"),
participants: render_many(event.participants, ActorView, "show_basic.json"),
participants: render_many(event.participants, ActorView, "acccount_basic.json"),
physical_address: render_one(event.physical_address, AddressView, "address.json"),
type: "Event",
address_type: event.address_type,

View file

@ -26,7 +26,7 @@ defmodule Eventos.Mixfile do
def application do
[
mod: {Eventos.Application, []},
extra_applications: [:logger, :runtime_tools, :guardian, :bamboo]
extra_applications: [:logger, :runtime_tools, :guardian, :bamboo, :geolix]
]
end
@ -69,6 +69,7 @@ defmodule Eventos.Mixfile do
{:ex_ical, github: "fazibear/ex_ical"},
{:bamboo, "~> 1.0"},
{:bamboo_smtp, "~> 1.5.0"},
{:geolix, "~> 0.16"},
# Dev and test dependencies
{:phoenix_live_reload, "~> 1.0", only: :dev},
{:ex_machina, "~> 2.1", only: :test},

View file

@ -31,6 +31,7 @@
"gen_smtp": {:hex, :gen_smtp, "0.12.0", "97d44903f5ca18ca85cb39aee7d9c77e98d79804bbdef56078adcf905cb2ef00", [:rebar3], [], "hexpm"},
"geo": {:hex, :geo, "2.1.0", "f9a7a1403dde669c4e3f1885aeb4f3b3fb4e51cd28ada6d9f97463e5da65c04a", [:mix], [{:ecto, "~> 2.1", [hex: :ecto, repo: "hexpm", optional: true]}, {:poison, "~> 3.0", [hex: :poison, repo: "hexpm", optional: true]}], "hexpm"},
"geo_postgis": {:hex, :geo_postgis, "1.1.0", "4c9efc082a8b625c335967fec9f5671c2bc8a0a686f9c5130445ebbcca989740", [:mix], [{:geo, "~> 2.0", [hex: :geo, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.13", [hex: :postgrex, repo: "hexpm", optional: false]}], "hexpm"},
"geolix": {:hex, :geolix, "0.16.0", "0095f5e828c0b46596216efaf6a4d46aba3f1a4da3bdfec735b2e49c8cd908fb", [:mix], [{:mmdb2_decoder, "~> 0.2.0", [hex: :mmdb2_decoder, repo: "hexpm", optional: false]}, {:poolboy, "~> 1.0", [hex: :poolboy, repo: "hexpm", optional: false]}], "hexpm"},
"gettext": {:hex, :gettext, "0.15.0", "40a2b8ce33a80ced7727e36768499fc9286881c43ebafccae6bab731e2b2b8ce", [:mix], [], "hexpm"},
"guardian": {:hex, :guardian, "1.0.1", "db0fbaf571c3b874785818b7272eaf5f1ed97a2f9b1f8bc5dc8b0fb8f8f7bb06", [:mix], [{:jose, "~> 1.8", [hex: :jose, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.0 or ~> 1.2 or ~> 1.3", [hex: :phoenix, repo: "hexpm", optional: true]}, {:plug, "~> 1.3.3 or ~> 1.4", [hex: :plug, repo: "hexpm", optional: true]}, {:poison, "~> 2.2 or ~> 3.0", [hex: :poison, repo: "hexpm", optional: false]}, {:uuid, ">= 1.1.1", [hex: :uuid, repo: "hexpm", optional: false]}], "hexpm"},
"guardian_db": {:hex, :guardian_db, "1.1.0", "45ab94206cce38f7443dc27de6dc52966ccbdeff65ca1b1f11a6d8f3daceb556", [:mix], [{:ecto, "~> 2.2", [hex: :ecto, repo: "hexpm", optional: false]}, {:guardian, "~> 1.0", [hex: :guardian, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.13", [hex: :postgrex, repo: "hexpm", optional: true]}], "hexpm"},
@ -48,6 +49,7 @@
"mime": {:hex, :mime, "1.3.0", "5e8d45a39e95c650900d03f897fbf99ae04f60ab1daa4a34c7a20a5151b7a5fe", [:mix], [], "hexpm"},
"mimerl": {:hex, :mimerl, "1.0.2", "993f9b0e084083405ed8252b99460c4f0563e41729ab42d9074fd5e52439be88", [:rebar3], [], "hexpm"},
"mix_test_watch": {:hex, :mix_test_watch, "0.6.0", "5e206ed04860555a455de2983937efd3ce79f42bd8536fc6b900cc286f5bb830", [:mix], [{:file_system, "~> 0.2.1 or ~> 0.3", [hex: :file_system, repo: "hexpm", optional: false]}], "hexpm"},
"mmdb2_decoder": {:hex, :mmdb2_decoder, "0.2.0", "34346c43bb1860d38d5505e5edf3d5e31a0d9fce4ee48f0c8c762875bcd05bb2", [:mix], [], "hexpm"},
"parse_trans": {:hex, :parse_trans, "3.2.0", "2adfa4daf80c14dc36f522cf190eb5c4ee3e28008fc6394397c16f62a26258c2", [:rebar3], [], "hexpm"},
"phoenix": {:hex, :phoenix, "1.3.2", "2a00d751f51670ea6bc3f2ba4e6eb27ecb8a2c71e7978d9cd3e5de5ccf7378bd", [:mix], [{:cowboy, "~> 1.0", [hex: :cowboy, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 1.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:plug, "~> 1.3.3 or ~> 1.4", [hex: :plug, repo: "hexpm", optional: false]}, {:poison, "~> 2.2 or ~> 3.0", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm"},
"phoenix_ecto": {:hex, :phoenix_ecto, "3.3.0", "702f6e164512853d29f9d20763493f2b3bcfcb44f118af2bc37bb95d0801b480", [:mix], [{:ecto, "~> 2.1", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.9", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"},

View file

@ -0,0 +1,15 @@
defmodule Eventos.Repo.Migrations.AlterEventsSetDescriptionAsText do
use Ecto.Migration
def up do
alter table(:events) do
modify :description, :text
end
end
def down do
alter table(:events) do
modify :description, :string
end
end
end