4 | <%= @inner_content %>
5 |
6 |
--------------------------------------------------------------------------------
/scripts/dev.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | set -ex
4 |
5 | mix deps.get
6 |
7 | npm install --prefix assets
8 |
9 | mix compile
10 | mix do ecto.create, ecto.migrate
11 |
12 | echo "run: docker exec -it ${HOSTNAME} sh in another console to jump into the container!"
13 |
14 | tail -f /dev/null
15 |
--------------------------------------------------------------------------------
/lib/live_map_app.ex:
--------------------------------------------------------------------------------
1 | defmodule LiveMapApp do
2 | @moduledoc """
3 | LiveMapApp keeps the contexts that define your domain
4 | and business logic.
5 |
6 | Contexts are also responsible for managing your data, regardless
7 | if it comes from the database, an external API or others.
8 | """
9 | end
10 |
--------------------------------------------------------------------------------
/test/live_map_app_web/views/layout_view_test.exs:
--------------------------------------------------------------------------------
1 | defmodule LiveMapAppWeb.LayoutViewTest do
2 | use LiveMapAppWeb.ConnCase, async: true
3 |
4 | # When testing helpers, you may want to import Phoenix.HTML and
5 | # use functions such as safe_to_string() to convert the helper
6 | # result into an HTML string.
7 | # import Phoenix.HTML
8 | end
9 |
--------------------------------------------------------------------------------
/priv/repo/migrations/20201007231121_create_apps.exs:
--------------------------------------------------------------------------------
1 | defmodule LiveMapApp.Repo.Migrations.CreateApps do
2 | use Ecto.Migration
3 |
4 | def change do
5 | create table(:apps) do
6 | add :longitude, :decimal
7 | add :latitude, :decimal
8 | add :app_id, :string
9 | add :download_at, :utc_datetime
10 |
11 | timestamps()
12 | end
13 |
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/priv/repo/seeds.exs:
--------------------------------------------------------------------------------
1 | # Script for populating the database. You can run it as:
2 | #
3 | # mix run priv/repo/seeds.exs
4 | #
5 | # Inside the script, you can read and write to any of your
6 | # repositories directly:
7 | #
8 | # LiveMapApp.Repo.insert!(%LiveMapApp.SomeSchema{})
9 | #
10 | # We recommend using the bang functions (`insert!`, `update!`
11 | # and so on) as they will fail if something goes wrong.
12 |
--------------------------------------------------------------------------------
/lib/live_map_app_web/templates/layout/live.html.leex:
--------------------------------------------------------------------------------
1 |
2 |
<%= live_flash(@flash, :info) %>
5 |
6 |
<%= live_flash(@flash, :error) %>
9 |
10 | <%= @inner_content %>
11 |
12 |
--------------------------------------------------------------------------------
/test/live_map_app_web/views/error_view_test.exs:
--------------------------------------------------------------------------------
1 | defmodule LiveMapAppWeb.ErrorViewTest do
2 | use LiveMapAppWeb.ConnCase, async: true
3 |
4 | # Bring render/3 and render_to_string/3 for testing custom views
5 | import Phoenix.View
6 |
7 | test "renders 404.html" do
8 | assert render_to_string(LiveMapAppWeb.ErrorView, "404.html", []) == "Not Found"
9 | end
10 |
11 | test "renders 500.html" do
12 | assert render_to_string(LiveMapAppWeb.ErrorView, "500.html", []) == "Internal Server Error"
13 | end
14 | end
15 |
--------------------------------------------------------------------------------
/lib/live_map_app_web/views/error_view.ex:
--------------------------------------------------------------------------------
1 | defmodule LiveMapAppWeb.ErrorView do
2 | use LiveMapAppWeb, :view
3 |
4 | # If you want to customize a particular status code
5 | # for a certain format, you may uncomment below.
6 | # def render("500.html", _assigns) do
7 | # "Internal Server Error"
8 | # end
9 |
10 | # By default, Phoenix returns the status message from
11 | # the template name. For example, "404.html" becomes
12 | # "Not Found".
13 | def template_not_found(template, _assigns) do
14 | Phoenix.Controller.status_message_from_template(template)
15 | end
16 | end
17 |
--------------------------------------------------------------------------------
/lib/live_map_app/dashboard/app.ex:
--------------------------------------------------------------------------------
1 | defmodule LiveMapApp.Dashboard.App do
2 | use Ecto.Schema
3 | import Ecto.Changeset
4 |
5 | schema "apps" do
6 | field :app_id, :string
7 | field :download_at, :utc_datetime
8 | field :latitude, :decimal
9 | field :longitude, :decimal
10 | field :country, :string
11 |
12 | timestamps()
13 | end
14 |
15 | @doc false
16 | def changeset(app, attrs) do
17 | app
18 | |> cast(attrs, [:longitude, :latitude, :app_id, :download_at, :country])
19 | |> validate_required([:longitude, :latitude, :app_id, :download_at])
20 | end
21 | end
22 |
--------------------------------------------------------------------------------
/lib/live_map_app_web/live/live_helpers.ex:
--------------------------------------------------------------------------------
1 | defmodule LiveMapAppWeb.LiveHelpers do
2 | @doc """
3 | Renders a component inside the `LiveMapAppWeb.ModalComponent` component.
4 |
5 | The rendered modal receives a `:return_to` option to properly update
6 | the URL when the modal is closed.
7 |
8 | ## Examples
9 |
10 | <%= live_modal @socket, LiveMapAppWeb.AppLive.FormComponent,
11 | id: @app.id || :new,
12 | action: @live_action,
13 | app: @app,
14 | return_to: Routes.app_index_path(@socket, :index) %>
15 | """
16 | def live_modal(_socket, _component, _opts) do
17 | end
18 | end
19 |
--------------------------------------------------------------------------------
/lib/live_map_app_web/gettext.ex:
--------------------------------------------------------------------------------
1 | defmodule LiveMapAppWeb.Gettext do
2 | @moduledoc """
3 | A module providing Internationalization with a gettext-based API.
4 |
5 | By using [Gettext](https://hexdocs.pm/gettext),
6 | your module gains a set of macros for translations, for example:
7 |
8 | import LiveMapAppWeb.Gettext
9 |
10 | # Simple translation
11 | gettext("Here is the string to translate")
12 |
13 | # Plural translation
14 | ngettext("Here is the string to translate",
15 | "Here are the strings to translate",
16 | 3)
17 |
18 | # Domain-based translation
19 | dgettext("errors", "Here is the error message to translate")
20 |
21 | See the [Gettext Docs](https://hexdocs.pm/gettext) for detailed usage.
22 | """
23 | use Gettext, otp_app: :live_map_app
24 | end
25 |
--------------------------------------------------------------------------------
/config/test.exs:
--------------------------------------------------------------------------------
1 | use Mix.Config
2 |
3 | # Configure your database
4 | #
5 | # The MIX_TEST_PARTITION environment variable can be used
6 | # to provide built-in test partitioning in CI environment.
7 | # Run `mix help test` for more information.
8 | config :live_map_app, LiveMapApp.Repo,
9 | username: System.get_env("POSTGRES_USER"),
10 | password: System.get_env("POSTGRES_PASSWORD"),
11 | database: "live_map_app_test#{System.get_env("MIX_TEST_PARTITION")}",
12 | hostname: System.get_env("POSTGRES_HOSTNAME"),
13 | pool: Ecto.Adapters.SQL.Sandbox
14 |
15 | # We don't run a server during test. If one is required,
16 | # you can enable the server option below.
17 | config :live_map_app, LiveMapAppWeb.Endpoint,
18 | http: [port: 4002],
19 | server: false
20 |
21 | # Print only warnings and errors during test
22 | config :logger, level: :warn
23 |
24 | config :tesla, adapter: Tesla.Mock
25 |
--------------------------------------------------------------------------------
/assets/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "repository": {},
3 | "description": " ",
4 | "license": "MIT",
5 | "scripts": {
6 | "deploy": "webpack --mode production",
7 | "watch": "webpack --mode development --watch"
8 | },
9 | "dependencies": {
10 | "phoenix": "file:../deps/phoenix",
11 | "phoenix_html": "file:../deps/phoenix_html",
12 | "phoenix_live_view": "file:../deps/phoenix_live_view",
13 | "nprogress": "^0.2.0"
14 | },
15 | "devDependencies": {
16 | "@babel/core": "^7.0.0",
17 | "@babel/preset-env": "^7.0.0",
18 | "babel-loader": "^8.0.0",
19 | "copy-webpack-plugin": "^5.1.1",
20 | "css-loader": "^3.4.2",
21 | "sass-loader": "^8.0.2",
22 | "node-sass": "^4.13.1",
23 | "hard-source-webpack-plugin": "^0.13.1",
24 | "mini-css-extract-plugin": "^0.9.0",
25 | "optimize-css-assets-webpack-plugin": "^5.0.1",
26 | "terser-webpack-plugin": "^2.3.2",
27 | "webpack": "4.41.5",
28 | "webpack-cli": "^3.3.2"
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # The directory Mix will write compiled artifacts to.
2 | /_build/
3 |
4 | # If you run "mix test --cover", coverage assets end up here.
5 | /cover/
6 |
7 | # The directory Mix downloads your dependencies sources to.
8 | /deps/
9 |
10 | # Where 3rd-party dependencies like ExDoc output generated docs.
11 | /doc/
12 |
13 | # Ignore .fetch files in case you like to edit your project deps locally.
14 | /.fetch
15 |
16 | # If the VM crashes, it generates a dump, let's ignore it too.
17 | erl_crash.dump
18 |
19 | # Also ignore archive artifacts (built via "mix archive.build").
20 | *.ez
21 |
22 | # Ignore package tarball (built via "mix hex.build").
23 | live_map_app-*.tar
24 |
25 | # If NPM crashes, it generates a log, let's ignore it too.
26 | npm-debug.log
27 |
28 | # The directory NPM downloads your dependencies sources to.
29 | /assets/node_modules/
30 |
31 | # Since we are building assets from assets/,
32 | # we ignore priv/static. You may want to comment
33 | # this depending on your deployment strategy.
34 | /priv/static/
35 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '2.2'
2 | services:
3 | service:
4 | environment:
5 | - UID
6 | build:
7 | context: .
8 | dockerfile: Dockerfile.local
9 | args:
10 | uid: 501
11 | # Comment the following line if you want `docker-compose up` to start the phoenix server automatically
12 | command: bash scripts/dev.sh
13 | env_file: ./env/dev.env
14 | ports:
15 | - 4005:4000
16 | volumes:
17 | - .:/home/app/service/
18 | - elixir-artifacts:/home/app/elixir-artifacts
19 | depends_on:
20 | db:
21 | condition: service_healthy
22 | networks:
23 | - shared
24 | - default
25 |
26 | db:
27 | environment:
28 | POSTGRES_USER: postgres
29 | POSTGRES_PASSWORD: "postgres"
30 | PSQL_TRUST_LOCALNET: 'true'
31 | ENCODING: UTF8
32 | image: postgres:9.6
33 | healthcheck:
34 | test: ["CMD", "pg_isready", "-d", "postgres", "-U", "postgres"]
35 | interval: 10s
36 | timeout: 3s
37 | retries: 10
38 |
39 | networks:
40 | shared:
41 | external: true
42 |
43 | volumes:
44 | elixir-artifacts: {}
45 |
--------------------------------------------------------------------------------
/config/config.exs:
--------------------------------------------------------------------------------
1 | # This file is responsible for configuring your application
2 | # and its dependencies with the aid of the Mix.Config module.
3 | #
4 | # This configuration file is loaded before any dependency and
5 | # is restricted to this project.
6 |
7 | # General application configuration
8 | use Mix.Config
9 |
10 | config :live_map_app,
11 | ecto_repos: [LiveMapApp.Repo],
12 | api_token: System.get_env("API_TOKEN")
13 |
14 | # Configures the endpoint
15 | config :live_map_app, LiveMapAppWeb.Endpoint,
16 | url: [host: "localhost"],
17 | secret_key_base: System.get_env("PHOENIX_SECRET"),
18 | render_errors: [view: LiveMapAppWeb.ErrorView, accepts: ~w(html json), layout: false],
19 | pubsub_server: LiveMapApp.PubSub,
20 | live_view: [signing_salt: "ifCTTFiF"]
21 |
22 | # Configures Elixir's Logger
23 | config :logger, :console,
24 | format: "$time $metadata[$level] $message\n",
25 | metadata: [:request_id]
26 |
27 | # Use Jason for JSON parsing in Phoenix
28 | config :phoenix, :json_library, Jason
29 |
30 | # Import environment specific config. This must remain at the bottom
31 | # of this file so it overrides the configuration defined above.
32 | import_config "#{Mix.env()}.exs"
33 |
--------------------------------------------------------------------------------
/lib/live_map_app/application.ex:
--------------------------------------------------------------------------------
1 | defmodule LiveMapApp.Application do
2 | # See https://hexdocs.pm/elixir/Application.html
3 | # for more information on OTP Applications
4 | @moduledoc false
5 |
6 | use Application
7 |
8 | def start(_type, _args) do
9 | children = [
10 | # Start the Ecto repository
11 | LiveMapApp.Repo,
12 | # Start the Telemetry supervisor
13 | LiveMapAppWeb.Telemetry,
14 | # Start the PubSub system
15 | {Phoenix.PubSub, name: LiveMapApp.PubSub},
16 | # Start the Endpoint (http/https)
17 | LiveMapAppWeb.Endpoint
18 | # Start a worker by calling: LiveMapApp.Worker.start_link(arg)
19 | # {LiveMapApp.Worker, arg}
20 | ]
21 |
22 | # See https://hexdocs.pm/elixir/Supervisor.html
23 | # for other strategies and supported options
24 | opts = [strategy: :one_for_one, name: LiveMapApp.Supervisor]
25 | Supervisor.start_link(children, opts)
26 | end
27 |
28 | # Tell Phoenix to update the endpoint configuration
29 | # whenever the application is updated.
30 | def config_change(changed, _new, removed) do
31 | LiveMapAppWeb.Endpoint.config_change(changed, removed)
32 | :ok
33 | end
34 | end
35 |
--------------------------------------------------------------------------------
/lib/live_map_app_web/templates/layout/root.html.leex:
--------------------------------------------------------------------------------
1 |
2 |
3 |
20 |
21 | <%= @inner_content %>
22 |
23 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/lib/live_map_app_web/channels/user_socket.ex:
--------------------------------------------------------------------------------
1 | defmodule LiveMapAppWeb.UserSocket do
2 | use Phoenix.Socket
3 |
4 | ## Channels
5 | # channel "room:*", LiveMapAppWeb.RoomChannel
6 |
7 | # Socket params are passed from the client and can
8 | # be used to verify and authenticate a user. After
9 | # verification, you can put default assigns into
10 | # the socket that will be set for all channels, ie
11 | #
12 | # {:ok, assign(socket, :user_id, verified_user_id)}
13 | #
14 | # To deny connection, return `:error`.
15 | #
16 | # See `Phoenix.Token` documentation for examples in
17 | # performing token verification on connect.
18 | @impl true
19 | def connect(_params, socket, _connect_info) do
20 | {:ok, socket}
21 | end
22 |
23 | # Socket id's are topics that allow you to identify all sockets for a given user:
24 | #
25 | # def id(socket), do: "user_socket:#{socket.assigns.user_id}"
26 | #
27 | # Would allow you to broadcast a "disconnect" event and terminate
28 | # all active sockets and channels for a given user:
29 | #
30 | # LiveMapAppWeb.Endpoint.broadcast("user_socket:#{user.id}", "disconnect", %{})
31 | #
32 | # Returning `nil` makes this socket anonymous.
33 | @impl true
34 | def id(_socket), do: nil
35 | end
36 |
--------------------------------------------------------------------------------
/test/support/channel_case.ex:
--------------------------------------------------------------------------------
1 | defmodule LiveMapAppWeb.ChannelCase do
2 | @moduledoc """
3 | This module defines the test case to be used by
4 | channel tests.
5 |
6 | Such tests rely on `Phoenix.ChannelTest` and also
7 | import other functionality to make it easier
8 | to build common data structures and query the data layer.
9 |
10 | Finally, if the test case interacts with the database,
11 | we enable the SQL sandbox, so changes done to the database
12 | are reverted at the end of every test. If you are using
13 | PostgreSQL, you can even run database tests asynchronously
14 | by setting `use LiveMapAppWeb.ChannelCase, async: true`, although
15 | this option is not recommended for other databases.
16 | """
17 |
18 | use ExUnit.CaseTemplate
19 |
20 | using do
21 | quote do
22 | # Import conveniences for testing with channels
23 | import Phoenix.ChannelTest
24 | import LiveMapAppWeb.ChannelCase
25 |
26 | # The default endpoint for testing
27 | @endpoint LiveMapAppWeb.Endpoint
28 | end
29 | end
30 |
31 | setup tags do
32 | :ok = Ecto.Adapters.SQL.Sandbox.checkout(LiveMapApp.Repo)
33 |
34 | unless tags[:async] do
35 | Ecto.Adapters.SQL.Sandbox.mode(LiveMapApp.Repo, {:shared, self()})
36 | end
37 |
38 | :ok
39 | end
40 | end
41 |
--------------------------------------------------------------------------------
/Dockerfile.local:
--------------------------------------------------------------------------------
1 | # Elixir 1.10 with Erlang/OTP 22
2 | FROM elixir@sha256:ba981350b63eb016427d12d90dad643eea8e2bfed37e0f2e4f2bce5aa5303eae
3 |
4 | LABEL maintainer="francesco.zanoli@gmail.com"
5 |
6 | ARG run_deps="inotify-tools"
7 |
8 | ARG mix_env="dev"
9 | ARG http_port="4000"
10 | ARG app_path="/home/app/service"
11 | ARG uid="1000"
12 |
13 | USER root
14 |
15 | ENV TERM xterm
16 |
17 | ENV HOME /home/app
18 | ENV APP_PATH ${app_path}
19 | ENV HTTP_PORT ${http_port}
20 | ENV MIX_ENV ${mix_env}
21 | ENV ERL_AFLAGS="-kernel shell_history enabled"
22 |
23 | ENV REFRESHED_AT 2020-10-08
24 |
25 | RUN apt-get -q update && apt-get -qy --no-install-recommends install ${run_deps}
26 | RUN curl -sL https://deb.nodesource.com/setup_14.x
27 | RUN apt-get install -y nodejs npm erlang-dev erlang-parsetools
28 |
29 | RUN adduser --disabled-password --gecos '' app --uid ${uid}
30 |
31 | RUN mkdir -p /home/app/elixir-artifacts
32 | RUN chown -R app:app /home/app/elixir-artifacts
33 |
34 | USER app:app
35 |
36 | RUN /usr/local/bin/mix local.hex --force && \
37 | /usr/local/bin/mix local.rebar --force && \
38 | /usr/local/bin/mix hex.info
39 |
40 | RUN echo "PS1=\"\[$(tput setaf 3)$(tput bold)[\]\\u@\\h$:\\w]$ \"" >> /home/app/.bashrc
41 |
42 | COPY --chown=app:app . ${APP_PATH}
43 |
44 | WORKDIR ${APP_PATH}
45 |
46 | EXPOSE ${HTTP_PORT}
47 |
48 | CMD ["sh", "script/start.sh"]
49 |
--------------------------------------------------------------------------------
/config/prod.secret.exs:
--------------------------------------------------------------------------------
1 | # In this file, we load production configuration and secrets
2 | # from environment variables. You can also hardcode secrets,
3 | # although such is generally not recommended and you have to
4 | # remember to add this file to your .gitignore.
5 | use Mix.Config
6 |
7 | database_url =
8 | System.get_env("DATABASE_URL") ||
9 | raise """
10 | environment variable DATABASE_URL is missing.
11 | For example: ecto://USER:PASS@HOST/DATABASE
12 | """
13 |
14 | config :live_map_app, LiveMapApp.Repo,
15 | # ssl: true,
16 | url: database_url,
17 | pool_size: String.to_integer(System.get_env("POOL_SIZE") || "10")
18 |
19 | secret_key_base =
20 | System.get_env("SECRET_KEY_BASE") ||
21 | raise """
22 | environment variable SECRET_KEY_BASE is missing.
23 | You can generate one by calling: mix phx.gen.secret
24 | """
25 |
26 | config :live_map_app, LiveMapAppWeb.Endpoint,
27 | http: [
28 | port: String.to_integer(System.get_env("PORT") || "4000"),
29 | transport_options: [socket_opts: [:inet6]]
30 | ],
31 | secret_key_base: secret_key_base
32 |
33 | # ## Using releases (Elixir v1.9+)
34 | #
35 | # If you are doing OTP releases, you need to instruct Phoenix
36 | # to start each relevant endpoint:
37 | #
38 | # config :live_map_app, LiveMapAppWeb.Endpoint, server: true
39 | #
40 | # Then you can assemble a release by calling `mix release`.
41 | # See `mix help release` for more information.
42 |
--------------------------------------------------------------------------------
/test/support/conn_case.ex:
--------------------------------------------------------------------------------
1 | defmodule LiveMapAppWeb.ConnCase do
2 | @moduledoc """
3 | This module defines the test case to be used by
4 | tests that require setting up a connection.
5 |
6 | Such tests rely on `Phoenix.ConnTest` and also
7 | import other functionality to make it easier
8 | to build common data structures and query the data layer.
9 |
10 | Finally, if the test case interacts with the database,
11 | we enable the SQL sandbox, so changes done to the database
12 | are reverted at the end of every test. If you are using
13 | PostgreSQL, you can even run database tests asynchronously
14 | by setting `use LiveMapAppWeb.ConnCase, async: true`, although
15 | this option is not recommended for other databases.
16 | """
17 |
18 | use ExUnit.CaseTemplate
19 |
20 | using do
21 | quote do
22 | # Import conveniences for testing with connections
23 | import Plug.Conn
24 | import Phoenix.ConnTest
25 | import LiveMapAppWeb.ConnCase
26 |
27 | alias LiveMapAppWeb.Router.Helpers, as: Routes
28 |
29 | # The default endpoint for testing
30 | @endpoint LiveMapAppWeb.Endpoint
31 | end
32 | end
33 |
34 | setup tags do
35 | :ok = Ecto.Adapters.SQL.Sandbox.checkout(LiveMapApp.Repo)
36 |
37 | unless tags[:async] do
38 | Ecto.Adapters.SQL.Sandbox.mode(LiveMapApp.Repo, {:shared, self()})
39 | end
40 |
41 | {:ok, conn: Phoenix.ConnTest.build_conn()}
42 | end
43 | end
44 |
--------------------------------------------------------------------------------
/lib/live_map_app_web/router.ex:
--------------------------------------------------------------------------------
1 | defmodule LiveMapAppWeb.Router do
2 | use LiveMapAppWeb, :router
3 |
4 | pipeline :browser do
5 | plug :accepts, ["html"]
6 | plug :fetch_session
7 | plug :fetch_live_flash
8 | plug :put_root_layout, {LiveMapAppWeb.LayoutView, :root}
9 | plug :protect_from_forgery
10 | plug :put_secure_browser_headers
11 | end
12 |
13 | pipeline :api do
14 | plug :accepts, ["json"]
15 | end
16 |
17 | scope "/", LiveMapAppWeb do
18 | post "/add_downaload", DownloadAppController, :add_download
19 | pipe_through :browser
20 |
21 | live "/", AppLive.Index, :index
22 | live "/apps", AppLive.Index, :index
23 | live "/apps/new", AppLive.Index, :new
24 | live "/apps/:id/edit", AppLive.Index, :edit
25 |
26 | live "/apps/:id", AppLive.Show, :show
27 | live "/apps/:id/show/edit", AppLive.Show, :edit
28 | end
29 |
30 | # Other scopes may use custom stacks.
31 | # scope "/api", LiveMapAppWeb do
32 | # pipe_through :api
33 | # end
34 |
35 | # Enables LiveDashboard only for development
36 | #
37 | # If you want to use the LiveDashboard in production, you should put
38 | # it behind authentication and allow only admins to access it.
39 | # If your application does not have an admins-only section yet,
40 | # you can use Plug.BasicAuth to set up some basic authentication
41 | # as long as you are also using SSL (which you should anyway).
42 | if Mix.env() in [:dev, :test] do
43 | import Phoenix.LiveDashboard.Router
44 |
45 | scope "/" do
46 | pipe_through :browser
47 | live_dashboard "/dashboard", metrics: LiveMapAppWeb.Telemetry
48 | end
49 | end
50 | end
51 |
--------------------------------------------------------------------------------
/lib/live_map_app_web/live/app_live/bar_chart.ex:
--------------------------------------------------------------------------------
1 | defmodule LiveMapAppWeb.AppLive.BarChart do
2 | use Phoenix.LiveComponent
3 |
4 | @default %{
5 | id: nil,
6 | title: "Dashboard",
7 | list: [],
8 | total: 0,
9 | percentage: false
10 | }
11 |
12 | @doc """
13 | Renders a BarChart live_component .
14 | id: the id used from the components,
15 | title: Title showed on top of the graph,
16 | list: a list of structure which contains the category
17 | name and the number of them, each element is a bar in
18 | the graph
19 | total: the total number element analysed ,
20 | percentage: if true it calculate the percentage of
21 | elements in each group,
22 | ## Examples
23 |
24 | <%= live_component @socket, LiveMapAppWeb.AppLive.BarChart,
25 | id: :new,
26 | title: "Title to show"
27 | list: [{"category", 1},{"categor2", 2}]
28 | total: 3
29 | percentage: false
30 |
31 | """
32 | def render(assigns) do
33 | ~L"""
34 |
35 |
<%= @title %>
36 | <%= for {countryName, count} <- @list do %>
37 |
38 |
39 | <%= countryName %>: <%= if @percentage do Float.round((count/@total)*100,2) else count end %>
40 |
41 |
42 | <% end %>
43 |
44 | """
45 | end
46 |
47 | def mount(_assigns, socket) do
48 | {:ok, socket}
49 | end
50 |
51 | def update(assigns, socket) do
52 | {:ok, assign(socket, Map.merge(@default, assigns))}
53 | end
54 | end
55 |
--------------------------------------------------------------------------------
/assets/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const glob = require('glob');
3 | const HardSourceWebpackPlugin = require('hard-source-webpack-plugin');
4 | const MiniCssExtractPlugin = require('mini-css-extract-plugin');
5 | const TerserPlugin = require('terser-webpack-plugin');
6 | const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');
7 | const CopyWebpackPlugin = require('copy-webpack-plugin');
8 |
9 | module.exports = (env, options) => {
10 | const devMode = options.mode !== 'production';
11 |
12 | return {
13 | optimization: {
14 | minimizer: [
15 | new TerserPlugin({ cache: true, parallel: true, sourceMap: devMode }),
16 | new OptimizeCSSAssetsPlugin({})
17 | ]
18 | },
19 | entry: {
20 | 'app': glob.sync('./vendor/**/*.js').concat(['./js/app.js'])
21 | },
22 | output: {
23 | filename: '[name].js',
24 | path: path.resolve(__dirname, '../priv/static/js'),
25 | publicPath: '/js/'
26 | },
27 | devtool: devMode ? 'eval-cheap-module-source-map' : undefined,
28 | module: {
29 | rules: [
30 | {
31 | test: /\.js$/,
32 | exclude: /node_modules/,
33 | use: {
34 | loader: 'babel-loader'
35 | }
36 | },
37 | {
38 | test: /\.[s]?css$/,
39 | use: [
40 | MiniCssExtractPlugin.loader,
41 | 'css-loader',
42 | 'sass-loader',
43 | ],
44 | }
45 | ]
46 | },
47 | plugins: [
48 | new MiniCssExtractPlugin({ filename: '../css/app.css' }),
49 | new CopyWebpackPlugin([{ from: 'static/', to: '../' }])
50 | ]
51 | .concat(devMode ? [new HardSourceWebpackPlugin()] : [])
52 | }
53 | };
54 |
--------------------------------------------------------------------------------
/lib/live_map_app_web/views/error_helpers.ex:
--------------------------------------------------------------------------------
1 | defmodule LiveMapAppWeb.ErrorHelpers do
2 | @moduledoc """
3 | Conveniences for translating and building error messages.
4 | """
5 |
6 | use Phoenix.HTML
7 |
8 | @doc """
9 | Generates tag for inlined form input errors.
10 | """
11 | def error_tag(form, field) do
12 | Enum.map(Keyword.get_values(form.errors, field), fn error ->
13 | content_tag(:span, translate_error(error),
14 | class: "invalid-feedback",
15 | phx_feedback_for: input_id(form, field)
16 | )
17 | end)
18 | end
19 |
20 | @doc """
21 | Translates an error message using gettext.
22 | """
23 | def translate_error({msg, opts}) do
24 | # When using gettext, we typically pass the strings we want
25 | # to translate as a static argument:
26 | #
27 | # # Translate "is invalid" in the "errors" domain
28 | # dgettext("errors", "is invalid")
29 | #
30 | # # Translate the number of files with plural rules
31 | # dngettext("errors", "1 file", "%{count} files", count)
32 | #
33 | # Because the error messages we show in our forms and APIs
34 | # are defined inside Ecto, we need to translate them dynamically.
35 | # This requires us to call the Gettext module passing our gettext
36 | # backend as first argument.
37 | #
38 | # Note we use the "errors" domain, which means translations
39 | # should be written to the errors.po file. The :count option is
40 | # set by Ecto and indicates we should also apply plural rules.
41 | if count = opts[:count] do
42 | Gettext.dngettext(LiveMapAppWeb.Gettext, "errors", msg, msg, count, opts)
43 | else
44 | Gettext.dgettext(LiveMapAppWeb.Gettext, "errors", msg, opts)
45 | end
46 | end
47 | end
48 |
--------------------------------------------------------------------------------
/test/support/data_case.ex:
--------------------------------------------------------------------------------
1 | defmodule LiveMapApp.DataCase do
2 | @moduledoc """
3 | This module defines the setup for tests requiring
4 | access to the application's data layer.
5 |
6 | You may define functions here to be used as helpers in
7 | your tests.
8 |
9 | Finally, if the test case interacts with the database,
10 | we enable the SQL sandbox, so changes done to the database
11 | are reverted at the end of every test. If you are using
12 | PostgreSQL, you can even run database tests asynchronously
13 | by setting `use LiveMapApp.DataCase, async: true`, although
14 | this option is not recommended for other databases.
15 | """
16 |
17 | use ExUnit.CaseTemplate
18 |
19 | using do
20 | quote do
21 | alias LiveMapApp.Repo
22 |
23 | import Ecto
24 | import Ecto.Changeset
25 | import Ecto.Query
26 | import LiveMapApp.DataCase
27 | end
28 | end
29 |
30 | setup tags do
31 | :ok = Ecto.Adapters.SQL.Sandbox.checkout(LiveMapApp.Repo)
32 |
33 | unless tags[:async] do
34 | Ecto.Adapters.SQL.Sandbox.mode(LiveMapApp.Repo, {:shared, self()})
35 | end
36 |
37 | :ok
38 | end
39 |
40 | @doc """
41 | A helper that transforms changeset errors into a map of messages.
42 |
43 | assert {:error, changeset} = Accounts.create_user(%{password: "short"})
44 | assert "password is too short" in errors_on(changeset).password
45 | assert %{password: ["password is too short"]} = errors_on(changeset)
46 |
47 | """
48 | def errors_on(changeset) do
49 | Ecto.Changeset.traverse_errors(changeset, fn {message, opts} ->
50 | Regex.replace(~r"%{(\w+)}", message, fn _, key ->
51 | opts |> Keyword.get(String.to_existing_atom(key), key) |> to_string()
52 | end)
53 | end)
54 | end
55 | end
56 |
--------------------------------------------------------------------------------
/lib/live_map_app_web/endpoint.ex:
--------------------------------------------------------------------------------
1 | defmodule LiveMapAppWeb.Endpoint do
2 | use Phoenix.Endpoint, otp_app: :live_map_app
3 |
4 | # The session will be stored in the cookie and signed,
5 | # this means its contents can be read but not tampered with.
6 | # Set :encryption_salt if you would also like to encrypt it.
7 | @session_options [
8 | store: :cookie,
9 | key: "_live_map_app_key",
10 | signing_salt: "QrYfiS+Z"
11 | ]
12 |
13 | socket "/socket", LiveMapAppWeb.UserSocket,
14 | websocket: true,
15 | longpoll: false
16 |
17 | socket "/live", Phoenix.LiveView.Socket, websocket: [connect_info: [session: @session_options]]
18 |
19 | # Serve at "/" the static files from "priv/static" directory.
20 | #
21 | # You should set gzip to true if you are running phx.digest
22 | # when deploying your static files in production.
23 | plug Plug.Static,
24 | at: "/",
25 | from: :live_map_app,
26 | gzip: false,
27 | only: ~w(css fonts images js favicon.ico robots.txt)
28 |
29 | # Code reloading can be explicitly enabled under the
30 | # :code_reloader configuration of your endpoint.
31 | if code_reloading? do
32 | socket "/phoenix/live_reload/socket", Phoenix.LiveReloader.Socket
33 | plug Phoenix.LiveReloader
34 | plug Phoenix.CodeReloader
35 | plug Phoenix.Ecto.CheckRepoStatus, otp_app: :live_map_app
36 | end
37 |
38 | plug Phoenix.LiveDashboard.RequestLogger,
39 | param_key: "request_logger",
40 | cookie_key: "request_logger"
41 |
42 | plug Plug.RequestId
43 | plug Plug.Telemetry, event_prefix: [:phoenix, :endpoint]
44 |
45 | plug Plug.Parsers,
46 | parsers: [:urlencoded, :multipart, :json],
47 | pass: ["*/*"],
48 | json_decoder: Phoenix.json_library()
49 |
50 | plug Plug.MethodOverride
51 | plug Plug.Head
52 | plug Plug.Session, @session_options
53 | plug LiveMapAppWeb.Router
54 | end
55 |
--------------------------------------------------------------------------------
/assets/css/app.scss:
--------------------------------------------------------------------------------
1 | /* This file is for your main application css. */
2 | @import "./phoenix.css";
3 | @import "../node_modules/nprogress/nprogress.css";
4 |
5 | /* LiveView specific classes for your customizations */
6 | .phx-no-feedback.invalid-feedback,
7 | .phx-no-feedback .invalid-feedback {
8 | display: none;
9 | }
10 |
11 | .phx-click-loading {
12 | opacity: 0.5;
13 | transition: opacity 1s ease-out;
14 | }
15 |
16 | .phx-disconnected{
17 | cursor: wait;
18 | }
19 | .phx-disconnected *{
20 | pointer-events: none;
21 | }
22 |
23 | .phx-modal {
24 | opacity: 1!important;
25 | position: fixed;
26 | z-index: 1;
27 | left: 0;
28 | top: 0;
29 | width: 100%;
30 | height: 100%;
31 | overflow: auto;
32 | background-color: rgb(0,0,0);
33 | background-color: rgba(0,0,0,0.4);
34 | }
35 |
36 | .phx-modal-content {
37 | background-color: #fefefe;
38 | margin: 15% auto;
39 | padding: 20px;
40 | border: 1px solid #888;
41 | width: 80%;
42 | }
43 |
44 | .phx-modal-close {
45 | color: #aaa;
46 | float: right;
47 | font-size: 28px;
48 | font-weight: bold;
49 | }
50 |
51 | .phx-modal-close:hover,
52 | .phx-modal-close:focus {
53 | color: black;
54 | text-decoration: none;
55 | cursor: pointer;
56 | }
57 |
58 |
59 | /* Alerts and form errors */
60 | .alert {
61 | padding: 15px;
62 | margin-bottom: 20px;
63 | border: 1px solid transparent;
64 | border-radius: 4px;
65 | }
66 | .alert-info {
67 | color: #31708f;
68 | background-color: #d9edf7;
69 | border-color: #bce8f1;
70 | }
71 | .alert-warning {
72 | color: #8a6d3b;
73 | background-color: #fcf8e3;
74 | border-color: #faebcc;
75 | }
76 | .alert-danger {
77 | color: #a94442;
78 | background-color: #f2dede;
79 | border-color: #ebccd1;
80 | }
81 | .alert p {
82 | margin-bottom: 0;
83 | }
84 | .alert:empty {
85 | display: none;
86 | }
87 | .invalid-feedback {
88 | color: #a94442;
89 | display: block;
90 | margin: -1rem 0 2rem;
91 | }
92 |
--------------------------------------------------------------------------------
/test/live_map_app/dashboard_test.exs:
--------------------------------------------------------------------------------
1 | defmodule LiveMapApp.DashboardTest do
2 | use LiveMapApp.DataCase
3 |
4 | alias LiveMapApp.Dashboard
5 |
6 | describe "apps" do
7 | alias LiveMapApp.Dashboard.App
8 |
9 | @valid_attrs %{
10 | app_id: "some app_id",
11 | download_at: "2010-04-17T14:00:00Z",
12 | latitude: "120.5",
13 | longitude: "120.5",
14 | country: "Unknown"
15 | }
16 | @invalid_attrs %{app_id: nil, download_at: nil, latitude: nil, longitude: nil}
17 |
18 | def app_fixture(attrs \\ %{}) do
19 | {:ok, app} =
20 | attrs
21 | |> Enum.into(@valid_attrs)
22 | |> Dashboard.create_app()
23 |
24 | app
25 | end
26 |
27 | test "list_apps/0 returns all apps" do
28 | app = app_fixture()
29 | assert Dashboard.list_apps() == [app]
30 | end
31 |
32 | test "get_app!/1 returns the app with given id" do
33 | app = app_fixture()
34 | assert Dashboard.get_app!(app.id) == app
35 | end
36 |
37 | test "create_app/1 with valid data creates a app" do
38 | assert {:ok, %App{} = app} = Dashboard.create_app(@valid_attrs)
39 | assert app.app_id == "some app_id"
40 | assert app.download_at == DateTime.from_naive!(~N[2010-04-17T14:00:00Z], "Etc/UTC")
41 | assert app.latitude == Decimal.new("120.5")
42 | assert app.longitude == Decimal.new("120.5")
43 | end
44 |
45 | test "create_app/1 with invalid data returns error changeset" do
46 | assert {:error, %Ecto.Changeset{}} = Dashboard.create_app(@invalid_attrs)
47 | end
48 |
49 | test "delete_app/1 deletes the app" do
50 | app = app_fixture()
51 | assert {:ok, %App{}} = Dashboard.delete_app(app)
52 | assert_raise Ecto.NoResultsError, fn -> Dashboard.get_app!(app.id) end
53 | end
54 |
55 | test "change_app/1 returns a app changeset" do
56 | app = app_fixture()
57 | assert %Ecto.Changeset{} = Dashboard.change_app(app)
58 | end
59 | end
60 | end
61 |
--------------------------------------------------------------------------------
/assets/js/app.js:
--------------------------------------------------------------------------------
1 | // We need to import the CSS so that webpack will load it.
2 | // The MiniCssExtractPlugin is used to separate it out into
3 | // its own CSS file.
4 | import "../css/app.scss"
5 | import "../css/main.scss"
6 | // webpack automatically bundles all modules in your
7 | // entry points. Those entry points can be configured
8 | // in "webpack.config.js".
9 | //
10 | // Import deps with the dep name or local files with a relative path, for example:
11 | //
12 | // import {Socket} from "phoenix"
13 | // import socket from "./socket"
14 | //
15 | import "phoenix_html"
16 | import {Socket} from "phoenix"
17 | import NProgress from "nprogress"
18 | import {LiveSocket} from "phoenix_live_view"
19 |
20 | let Hooks = {}
21 |
22 | Hooks.MapMarkerHandler = {
23 | mounted() {
24 |
25 | this.handleEvent("new_marker", ({ marker }) => {
26 |
27 | var markerPosition = { lat: parseFloat(marker.latitude), lng: parseFloat(marker.longitude) }
28 |
29 | const mapMarker = new google.maps.Marker({
30 | position: markerPosition,
31 | animation: google.maps.Animation.DROP,
32 | title: marker.app_id
33 | })
34 | mapMarker.setMap(window.map)
35 | });
36 | }
37 | }
38 |
39 | let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content")
40 | let liveSocket = new LiveSocket("/live", Socket, {hooks: Hooks, params: {_csrf_token: csrfToken}})
41 |
42 | // Show progress bar on live navigation and form submits
43 | window.addEventListener("phx:page-loading-start", info => NProgress.start())
44 | window.addEventListener("phx:page-loading-stop", info => NProgress.done())
45 |
46 | // connect if there are any LiveViews on the page
47 | liveSocket.connect()
48 |
49 | // expose liveSocket on window for web console debug logs and latency simulation:
50 | // >> liveSocket.enableDebug()
51 | // >> liveSocket.enableLatencySim(1000) // enabled for duration of browser session
52 | // >> liveSocket.disableLatencySim()
53 | window.liveSocket = liveSocket
54 |
55 |
--------------------------------------------------------------------------------
/lib/live_map_app_web/telemetry.ex:
--------------------------------------------------------------------------------
1 | defmodule LiveMapAppWeb.Telemetry do
2 | use Supervisor
3 | import Telemetry.Metrics
4 |
5 | def start_link(arg) do
6 | Supervisor.start_link(__MODULE__, arg, name: __MODULE__)
7 | end
8 |
9 | @impl true
10 | def init(_arg) do
11 | children = [
12 | # Telemetry poller will execute the given period measurements
13 | # every 10_000ms. Learn more here: https://hexdocs.pm/telemetry_metrics
14 | {:telemetry_poller, measurements: periodic_measurements(), period: 10_000}
15 | # Add reporters as children of your supervision tree.
16 | # {Telemetry.Metrics.ConsoleReporter, metrics: metrics()}
17 | ]
18 |
19 | Supervisor.init(children, strategy: :one_for_one)
20 | end
21 |
22 | def metrics do
23 | [
24 | # Phoenix Metrics
25 | summary("phoenix.endpoint.stop.duration",
26 | unit: {:native, :millisecond}
27 | ),
28 | summary("phoenix.router_dispatch.stop.duration",
29 | tags: [:route],
30 | unit: {:native, :millisecond}
31 | ),
32 |
33 | # Database Metrics
34 | summary("live_map_app.repo.query.total_time", unit: {:native, :millisecond}),
35 | summary("live_map_app.repo.query.decode_time", unit: {:native, :millisecond}),
36 | summary("live_map_app.repo.query.query_time", unit: {:native, :millisecond}),
37 | summary("live_map_app.repo.query.queue_time", unit: {:native, :millisecond}),
38 | summary("live_map_app.repo.query.idle_time", unit: {:native, :millisecond}),
39 |
40 | # VM Metrics
41 | summary("vm.memory.total", unit: {:byte, :kilobyte}),
42 | summary("vm.total_run_queue_lengths.total"),
43 | summary("vm.total_run_queue_lengths.cpu"),
44 | summary("vm.total_run_queue_lengths.io")
45 | ]
46 | end
47 |
48 | defp periodic_measurements do
49 | [
50 | # A module, function and arguments to be invoked periodically.
51 | # This function must call :telemetry.execute/3 and a metric must be added above.
52 | # {LiveMapAppWeb, :count_users, []}
53 | ]
54 | end
55 | end
56 |
--------------------------------------------------------------------------------
/mix.exs:
--------------------------------------------------------------------------------
1 | defmodule LiveMapApp.MixProject do
2 | use Mix.Project
3 |
4 | def project do
5 | [
6 | app: :live_map_app,
7 | version: "0.1.0",
8 | elixir: "~> 1.7",
9 | elixirc_paths: elixirc_paths(Mix.env()),
10 | compilers: [:phoenix, :gettext] ++ Mix.compilers(),
11 | start_permanent: Mix.env() == :prod,
12 | aliases: aliases(),
13 | deps: deps()
14 | ]
15 | end
16 |
17 | # Configuration for the OTP application.
18 | #
19 | # Type `mix help compile.app` for more information.
20 | def application do
21 | [
22 | mod: {LiveMapApp.Application, []},
23 | extra_applications: [:logger, :runtime_tools]
24 | ]
25 | end
26 |
27 | # Specifies which paths to compile per environment.
28 | defp elixirc_paths(:test), do: ["lib", "test/support"]
29 | defp elixirc_paths(_), do: ["lib"]
30 |
31 | # Specifies your project dependencies.
32 | #
33 | # Type `mix help deps` for examples and options.
34 | defp deps do
35 | [
36 | {:phoenix, "~> 1.5.5"},
37 | {:phoenix_ecto, "~> 4.1"},
38 | {:ecto_sql, "~> 3.4"},
39 | {:postgrex, ">= 0.0.0"},
40 | {:phoenix_live_view, "~> 0.14.6"},
41 | {:phoenix_html, "~> 2.11"},
42 | {:phoenix_live_reload, "~> 1.2", only: :dev},
43 | {:phoenix_live_dashboard, "~> 0.2"},
44 | {:telemetry_metrics, "~> 0.4"},
45 | {:telemetry_poller, "~> 0.4"},
46 | {:gettext, "~> 0.11"},
47 | {:jason, "~> 1.0"},
48 | {:plug_cowboy, "~> 2.0"},
49 | {:tesla, "~> 1.3.0"},
50 | {:mox, "~> 1.0", only: :test}
51 | ]
52 | end
53 |
54 | # Aliases are shortcuts or tasks specific to the current project.
55 | # For example, to install project dependencies and perform other setup tasks, run:
56 | #
57 | # $ mix setup
58 | #
59 | # See the documentation for `Mix` for more info on aliases.
60 | defp aliases do
61 | [
62 | setup: ["deps.get", "ecto.setup", "cmd npm install --prefix assets"],
63 | "ecto.setup": ["ecto.create", "ecto.migrate", "run priv/repo/seeds.exs"],
64 | "ecto.reset": ["ecto.drop", "ecto.setup"],
65 | test: ["ecto.create --quiet", "ecto.migrate --quiet", "test"]
66 | ]
67 | end
68 | end
69 |
--------------------------------------------------------------------------------
/config/prod.exs:
--------------------------------------------------------------------------------
1 | use Mix.Config
2 |
3 | # For production, don't forget to configure the url host
4 | # to something meaningful, Phoenix uses this information
5 | # when generating URLs.
6 | #
7 | # Note we also include the path to a cache manifest
8 | # containing the digested version of static files. This
9 | # manifest is generated by the `mix phx.digest` task,
10 | # which you should run after static files are built and
11 | # before starting your production server.
12 | config :live_map_app, LiveMapAppWeb.Endpoint,
13 | url: [host: "example.com", port: 80],
14 | cache_static_manifest: "priv/static/cache_manifest.json"
15 |
16 | # Do not print debug messages in production
17 | config :logger, level: :info
18 |
19 | # ## SSL Support
20 | #
21 | # To get SSL working, you will need to add the `https` key
22 | # to the previous section and set your `:url` port to 443:
23 | #
24 | # config :live_map_app, LiveMapAppWeb.Endpoint,
25 | # ...
26 | # url: [host: "example.com", port: 443],
27 | # https: [
28 | # port: 443,
29 | # cipher_suite: :strong,
30 | # keyfile: System.get_env("SOME_APP_SSL_KEY_PATH"),
31 | # certfile: System.get_env("SOME_APP_SSL_CERT_PATH"),
32 | # transport_options: [socket_opts: [:inet6]]
33 | # ]
34 | #
35 | # The `cipher_suite` is set to `:strong` to support only the
36 | # latest and more secure SSL ciphers. This means old browsers
37 | # and clients may not be supported. You can set it to
38 | # `:compatible` for wider support.
39 | #
40 | # `:keyfile` and `:certfile` expect an absolute path to the key
41 | # and cert in disk or a relative path inside priv, for example
42 | # "priv/ssl/server.key". For all supported SSL configuration
43 | # options, see https://hexdocs.pm/plug/Plug.SSL.html#configure/1
44 | #
45 | # We also recommend setting `force_ssl` in your endpoint, ensuring
46 | # no data is ever sent via http, always redirecting to https:
47 | #
48 | # config :live_map_app, LiveMapAppWeb.Endpoint,
49 | # force_ssl: [hsts: true]
50 | #
51 | # Check `Plug.SSL` for all available options in `force_ssl`.
52 |
53 | # Finally import the config/prod.secret.exs which loads secrets
54 | # and configuration from environment variables.
55 | import_config "prod.secret.exs"
56 |
--------------------------------------------------------------------------------
/lib/live_map_app_web/live/app_live/index.ex:
--------------------------------------------------------------------------------
1 | defmodule LiveMapAppWeb.AppLive.Index do
2 | use LiveMapAppWeb, :live_view
3 |
4 | alias LiveMapAppWeb.AppLive.BarChart
5 | alias LiveMapApp.Dashboard
6 |
7 | @impl true
8 | def mount(_params, _session, socket) do
9 | if connected?(socket), do: Dashboard.subscribe()
10 | {:ok, assign(socket, :downloaded_apps, list_apps())}
11 | end
12 |
13 | @impl true
14 | def handle_params(params, _url, socket) do
15 | {:noreply, apply_action(socket, socket.assigns.live_action, params)}
16 | end
17 |
18 | defp apply_action(socket, :index, _params) do
19 | socket
20 | |> assign(:page_title, "Dashboards")
21 | |> assign(:app, nil)
22 | end
23 |
24 | @doc """
25 | Handle the :download_added event generated from the database
26 | It dispatches an update to the app lists to immediately show the result
27 | """
28 | @impl true
29 | def handle_info({:download_added, app}, socket) do
30 | {:noreply, update(socket, :downloaded_apps, fn apps -> [app | apps] end)}
31 | end
32 |
33 | @doc """
34 | Handle the :new_marker event generated from the database module everytime a new
35 | download is added
36 | """
37 | @impl true
38 | def handle_info({:new_marker, app}, socket) do
39 | {:noreply,
40 | push_event(socket, "new_marker", %{
41 | marker: %{latitude: app.latitude, longitude: app.longitude, app_id: app.app_id}
42 | })}
43 | end
44 |
45 | defp list_apps do
46 | Dashboard.list_apps()
47 | end
48 |
49 | defp get_day_name(day) do
50 | case day do
51 | 1 -> "Monday"
52 | 2 -> "Tuesday"
53 | 3 -> "Wednesday"
54 | 4 -> "Thursday"
55 | 5 -> "Friday"
56 | 6 -> "Saturday"
57 | 7 -> "Sunday"
58 | _ -> "Unknown"
59 | end
60 | end
61 |
62 | defp get_month_name(month) do
63 | case month do
64 | 1 -> "Jan"
65 | 2 -> "Feb"
66 | 3 -> "Mar"
67 | 4 -> "Apr"
68 | 5 -> "May"
69 | 6 -> "Jun"
70 | 7 -> "Jul"
71 | 8 -> "Aug"
72 | 9 -> "Sep"
73 | 10 -> "Oct"
74 | 11 -> "Nov"
75 | 12 -> "Dec"
76 | _ -> "Unknown"
77 | end
78 | end
79 |
80 | defp get_day_time(datetime) do
81 | cond do
82 | datetime.hour < 12 -> "Morning"
83 | datetime.hour < 18 -> "Afternoon"
84 | datetime.hour < 22 -> "Evening"
85 | true -> "Night"
86 | end
87 | end
88 | end
89 |
--------------------------------------------------------------------------------
/lib/live_map_app/dashboard.ex:
--------------------------------------------------------------------------------
1 | defmodule LiveMapApp.Dashboard do
2 | @moduledoc """
3 | The Dashboard context.
4 | """
5 |
6 | import Ecto.Query, warn: false
7 | alias LiveMapApp.Repo
8 |
9 | alias LiveMapApp.Dashboard.App
10 |
11 | @doc """
12 | Returns the list of apps.
13 |
14 | ## Examples
15 |
16 | iex> list_apps()
17 | [%App{}, ...]
18 |
19 | """
20 | def list_apps do
21 | Repo.all(App)
22 | end
23 |
24 | @doc """
25 | Gets a single app.
26 |
27 | Raises `Ecto.NoResultsError` if the App does not exist.
28 |
29 | ## Examples
30 |
31 | iex> get_app!(123)
32 | %App{}
33 |
34 | iex> get_app!(456)
35 | ** (Ecto.NoResultsError)
36 |
37 | """
38 | def get_app!(id), do: Repo.get!(App, id)
39 |
40 | @doc """
41 | Creates a app.
42 |
43 | ## Examples
44 |
45 | iex> create_app(%{field: value})
46 | {:ok, %App{}}
47 |
48 | iex> create_app(%{field: bad_value})
49 | {:error, %Ecto.Changeset{}}
50 |
51 | """
52 | def create_app(attrs \\ %{}) do
53 | %App{}
54 | |> App.changeset(attrs)
55 | |> Repo.insert()
56 | |> broadcast(:download_added)
57 | end
58 |
59 | @doc """
60 | Deletes a app.
61 |
62 | ## Examples
63 |
64 | iex> delete_app(app)
65 | {:ok, %App{}}
66 |
67 | iex> delete_app(app)
68 | {:error, %Ecto.Changeset{}}
69 |
70 | """
71 | def delete_app(%App{} = app) do
72 | Repo.delete(app)
73 | end
74 |
75 | @doc """
76 | Returns an `%Ecto.Changeset{}` for tracking app changes.
77 |
78 | ## Examples
79 |
80 | iex> change_app(app)
81 | %Ecto.Changeset{data: %App{}}
82 |
83 | """
84 | def change_app(%App{} = app, attrs \\ %{}) do
85 | App.changeset(app, attrs)
86 | end
87 |
88 | def subscribe do
89 | Phoenix.PubSub.subscribe(LiveMapApp.PubSub, "apps")
90 | end
91 |
92 | defp broadcast({:error, _reason} = error, _event), do: error
93 |
94 | defp broadcast({:ok, app}, :download_added = event) do
95 | Phoenix.PubSub.broadcast(LiveMapApp.PubSub, "apps", {event, app})
96 | Phoenix.PubSub.broadcast(LiveMapApp.PubSub, "apps", {:new_marker, app})
97 | {:ok, app}
98 | end
99 |
100 | defp broadcast({:ok, app}, event) do
101 | Phoenix.PubSub.broadcast(LiveMapApp.PubSub, "apps", {event, app})
102 | {:ok, app}
103 | end
104 | end
105 |
--------------------------------------------------------------------------------
/config/dev.exs:
--------------------------------------------------------------------------------
1 | use Mix.Config
2 |
3 | # Configure your database
4 | config :live_map_app, LiveMapApp.Repo,
5 | username: System.get_env("POSTGRES_USER"),
6 | password: System.get_env("POSTGRES_PASSWORD"),
7 | database: System.get_env("POSTGRES_DB"),
8 | hostname: System.get_env("POSTGRES_HOSTNAME"),
9 | show_sensitive_data_on_connection_error: true,
10 | pool_size: 10
11 |
12 | # For development, we disable any cache and enable
13 | # debugging and code reloading.
14 | #
15 | # The watchers configuration can be used to run external
16 | # watchers to your application. For example, we use it
17 | # with webpack to recompile .js and .css sources.
18 | config :live_map_app, LiveMapAppWeb.Endpoint,
19 | http: [port: 4000],
20 | debug_errors: true,
21 | code_reloader: true,
22 | check_origin: false,
23 | watchers: [
24 | node: [
25 | "node_modules/webpack/bin/webpack.js",
26 | "--mode",
27 | "development",
28 | "--watch-stdin",
29 | cd: Path.expand("../assets", __DIR__)
30 | ]
31 | ]
32 |
33 | # ## SSL Support
34 | #
35 | # In order to use HTTPS in development, a self-signed
36 | # certificate can be generated by running the following
37 | # Mix task:
38 | #
39 | # mix phx.gen.cert
40 | #
41 | # Note that this task requires Erlang/OTP 20 or later.
42 | # Run `mix help phx.gen.cert` for more information.
43 | #
44 | # The `http:` config above can be replaced with:
45 | #
46 | # https: [
47 | # port: 4001,
48 | # cipher_suite: :strong,
49 | # keyfile: "priv/cert/selfsigned_key.pem",
50 | # certfile: "priv/cert/selfsigned.pem"
51 | # ],
52 | #
53 | # If desired, both `http:` and `https:` keys can be
54 | # configured to run both http and https servers on
55 | # different ports.
56 |
57 | # Watch static and templates for browser reloading.
58 | config :live_map_app, LiveMapAppWeb.Endpoint,
59 | live_reload: [
60 | patterns: [
61 | ~r"priv/static/.*(js|css|png|jpeg|jpg|gif|svg)$",
62 | ~r"priv/gettext/.*(po)$",
63 | ~r"lib/live_map_app_web/(live|views)/.*(ex)$",
64 | ~r"lib/live_map_app_web/templates/.*(eex)$"
65 | ]
66 | ]
67 |
68 | # Do not include metadata nor timestamps in development logs
69 | config :logger, :console, format: "[$level] $message\n"
70 |
71 | # Set a higher stacktrace during development. Avoid configuring such
72 | # in production as building large stacktraces may be expensive.
73 | config :phoenix, :stacktrace_depth, 20
74 |
75 | # Initialize plugs at runtime for faster development compilation
76 | config :phoenix, :plug_init_mode, :runtime
77 |
--------------------------------------------------------------------------------
/priv/gettext/en/LC_MESSAGES/errors.po:
--------------------------------------------------------------------------------
1 | ## `msgid`s in this file come from POT (.pot) files.
2 | ##
3 | ## Do not add, change, or remove `msgid`s manually here as
4 | ## they're tied to the ones in the corresponding POT file
5 | ## (with the same domain).
6 | ##
7 | ## Use `mix gettext.extract --merge` or `mix gettext.merge`
8 | ## to merge POT files into PO files.
9 | msgid ""
10 | msgstr ""
11 | "Language: en\n"
12 |
13 | ## From Ecto.Changeset.cast/4
14 | msgid "can't be blank"
15 | msgstr ""
16 |
17 | ## From Ecto.Changeset.unique_constraint/3
18 | msgid "has already been taken"
19 | msgstr ""
20 |
21 | ## From Ecto.Changeset.put_change/3
22 | msgid "is invalid"
23 | msgstr ""
24 |
25 | ## From Ecto.Changeset.validate_acceptance/3
26 | msgid "must be accepted"
27 | msgstr ""
28 |
29 | ## From Ecto.Changeset.validate_format/3
30 | msgid "has invalid format"
31 | msgstr ""
32 |
33 | ## From Ecto.Changeset.validate_subset/3
34 | msgid "has an invalid entry"
35 | msgstr ""
36 |
37 | ## From Ecto.Changeset.validate_exclusion/3
38 | msgid "is reserved"
39 | msgstr ""
40 |
41 | ## From Ecto.Changeset.validate_confirmation/3
42 | msgid "does not match confirmation"
43 | msgstr ""
44 |
45 | ## From Ecto.Changeset.no_assoc_constraint/3
46 | msgid "is still associated with this entry"
47 | msgstr ""
48 |
49 | msgid "are still associated with this entry"
50 | msgstr ""
51 |
52 | ## From Ecto.Changeset.validate_length/3
53 | msgid "should be %{count} character(s)"
54 | msgid_plural "should be %{count} character(s)"
55 | msgstr[0] ""
56 | msgstr[1] ""
57 |
58 | msgid "should have %{count} item(s)"
59 | msgid_plural "should have %{count} item(s)"
60 | msgstr[0] ""
61 | msgstr[1] ""
62 |
63 | msgid "should be at least %{count} character(s)"
64 | msgid_plural "should be at least %{count} character(s)"
65 | msgstr[0] ""
66 | msgstr[1] ""
67 |
68 | msgid "should have at least %{count} item(s)"
69 | msgid_plural "should have at least %{count} item(s)"
70 | msgstr[0] ""
71 | msgstr[1] ""
72 |
73 | msgid "should be at most %{count} character(s)"
74 | msgid_plural "should be at most %{count} character(s)"
75 | msgstr[0] ""
76 | msgstr[1] ""
77 |
78 | msgid "should have at most %{count} item(s)"
79 | msgid_plural "should have at most %{count} item(s)"
80 | msgstr[0] ""
81 | msgstr[1] ""
82 |
83 | ## From Ecto.Changeset.validate_number/3
84 | msgid "must be less than %{number}"
85 | msgstr ""
86 |
87 | msgid "must be greater than %{number}"
88 | msgstr ""
89 |
90 | msgid "must be less than or equal to %{number}"
91 | msgstr ""
92 |
93 | msgid "must be greater than or equal to %{number}"
94 | msgstr ""
95 |
96 | msgid "must be equal to %{number}"
97 | msgstr ""
98 |
--------------------------------------------------------------------------------
/priv/gettext/errors.pot:
--------------------------------------------------------------------------------
1 | ## This is a PO Template file.
2 | ##
3 | ## `msgid`s here are often extracted from source code.
4 | ## Add new translations manually only if they're dynamic
5 | ## translations that can't be statically extracted.
6 | ##
7 | ## Run `mix gettext.extract` to bring this file up to
8 | ## date. Leave `msgstr`s empty as changing them here has no
9 | ## effect: edit them in PO (`.po`) files instead.
10 |
11 | ## From Ecto.Changeset.cast/4
12 | msgid "can't be blank"
13 | msgstr ""
14 |
15 | ## From Ecto.Changeset.unique_constraint/3
16 | msgid "has already been taken"
17 | msgstr ""
18 |
19 | ## From Ecto.Changeset.put_change/3
20 | msgid "is invalid"
21 | msgstr ""
22 |
23 | ## From Ecto.Changeset.validate_acceptance/3
24 | msgid "must be accepted"
25 | msgstr ""
26 |
27 | ## From Ecto.Changeset.validate_format/3
28 | msgid "has invalid format"
29 | msgstr ""
30 |
31 | ## From Ecto.Changeset.validate_subset/3
32 | msgid "has an invalid entry"
33 | msgstr ""
34 |
35 | ## From Ecto.Changeset.validate_exclusion/3
36 | msgid "is reserved"
37 | msgstr ""
38 |
39 | ## From Ecto.Changeset.validate_confirmation/3
40 | msgid "does not match confirmation"
41 | msgstr ""
42 |
43 | ## From Ecto.Changeset.no_assoc_constraint/3
44 | msgid "is still associated with this entry"
45 | msgstr ""
46 |
47 | msgid "are still associated with this entry"
48 | msgstr ""
49 |
50 | ## From Ecto.Changeset.validate_length/3
51 | msgid "should be %{count} character(s)"
52 | msgid_plural "should be %{count} character(s)"
53 | msgstr[0] ""
54 | msgstr[1] ""
55 |
56 | msgid "should have %{count} item(s)"
57 | msgid_plural "should have %{count} item(s)"
58 | msgstr[0] ""
59 | msgstr[1] ""
60 |
61 | msgid "should be at least %{count} character(s)"
62 | msgid_plural "should be at least %{count} character(s)"
63 | msgstr[0] ""
64 | msgstr[1] ""
65 |
66 | msgid "should have at least %{count} item(s)"
67 | msgid_plural "should have at least %{count} item(s)"
68 | msgstr[0] ""
69 | msgstr[1] ""
70 |
71 | msgid "should be at most %{count} character(s)"
72 | msgid_plural "should be at most %{count} character(s)"
73 | msgstr[0] ""
74 | msgstr[1] ""
75 |
76 | msgid "should have at most %{count} item(s)"
77 | msgid_plural "should have at most %{count} item(s)"
78 | msgstr[0] ""
79 | msgstr[1] ""
80 |
81 | ## From Ecto.Changeset.validate_number/3
82 | msgid "must be less than %{number}"
83 | msgstr ""
84 |
85 | msgid "must be greater than %{number}"
86 | msgstr ""
87 |
88 | msgid "must be less than or equal to %{number}"
89 | msgstr ""
90 |
91 | msgid "must be greater than or equal to %{number}"
92 | msgstr ""
93 |
94 | msgid "must be equal to %{number}"
95 | msgstr ""
96 |
--------------------------------------------------------------------------------
/lib/live_map_app_web.ex:
--------------------------------------------------------------------------------
1 | defmodule LiveMapAppWeb do
2 | @moduledoc """
3 | The entrypoint for defining your web interface, such
4 | as controllers, views, channels and so on.
5 |
6 | This can be used in your application as:
7 |
8 | use LiveMapAppWeb, :controller
9 | use LiveMapAppWeb, :view
10 |
11 | The definitions below will be executed for every view,
12 | controller, etc, so keep them short and clean, focused
13 | on imports, uses and aliases.
14 |
15 | Do NOT define functions inside the quoted expressions
16 | below. Instead, define any helper function in modules
17 | and import those modules here.
18 | """
19 |
20 | def controller do
21 | quote do
22 | use Phoenix.Controller, namespace: LiveMapAppWeb
23 |
24 | import Plug.Conn
25 | import LiveMapAppWeb.Gettext
26 | alias LiveMapAppWeb.Router.Helpers, as: Routes
27 | end
28 | end
29 |
30 | def view do
31 | quote do
32 | use Phoenix.View,
33 | root: "lib/live_map_app_web/templates",
34 | namespace: LiveMapAppWeb
35 |
36 | # Import convenience functions from controllers
37 | import Phoenix.Controller,
38 | only: [get_flash: 1, get_flash: 2, view_module: 1, view_template: 1]
39 |
40 | # Include shared imports and aliases for views
41 | unquote(view_helpers())
42 | end
43 | end
44 |
45 | def live_view do
46 | quote do
47 | use Phoenix.LiveView,
48 | layout: {LiveMapAppWeb.LayoutView, "live.html"}
49 |
50 | unquote(view_helpers())
51 | end
52 | end
53 |
54 | def live_component do
55 | quote do
56 | use Phoenix.LiveComponent
57 |
58 | unquote(view_helpers())
59 | end
60 | end
61 |
62 | def router do
63 | quote do
64 | use Phoenix.Router
65 |
66 | import Plug.Conn
67 | import Phoenix.Controller
68 | import Phoenix.LiveView.Router
69 | end
70 | end
71 |
72 | def channel do
73 | quote do
74 | use Phoenix.Channel
75 | import LiveMapAppWeb.Gettext
76 | end
77 | end
78 |
79 | defp view_helpers do
80 | quote do
81 | # Use all HTML functionality (forms, tags, etc)
82 | use Phoenix.HTML
83 |
84 | # Import LiveView helpers (live_render, live_component, live_patch, etc)
85 | import Phoenix.LiveView.Helpers
86 | import LiveMapAppWeb.LiveHelpers
87 |
88 | # Import basic rendering functionality (render, render_layout, etc)
89 | import Phoenix.View
90 |
91 | import LiveMapAppWeb.ErrorHelpers
92 | import LiveMapAppWeb.Gettext
93 | alias LiveMapAppWeb.Router.Helpers, as: Routes
94 | end
95 | end
96 |
97 | @doc """
98 | When used, dispatch to the appropriate controller/view/etc.
99 | """
100 | defmacro __using__(which) when is_atom(which) do
101 | apply(__MODULE__, which, [])
102 | end
103 | end
104 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # LiveMapApp
2 |
3 | This is a really simple [Phoenix LiveView]() Application to get real time information about app downloads using a map and different dashboards.
4 |
5 | The creation of this project is described in [this article](https://medium.com/@francescozanoli/how-to-use-google-maps-with-ecto-and-phoenix-liveview-2b81bed570a9)
6 |
7 | ## Development
8 | ### Requirements
9 | The solution is using docker so be sure to have installed `docker-compose` and `Docker` on your machine before starting.
10 |
11 | :warning: Important Step :warning:
12 |
13 | You will need a GoogleApiKey, you can get one by following [this tutorial](https://developers.google.com/maps/documentation/javascript/get-api-key). You need to enable it for:
14 | - Geocoding API
15 | - Maps JavaScript API
16 |
17 | Once you have it you have to copy it in the `API_TOKEN` environment variable in the `/env/dev.env` file.
18 |
19 | ### How to launch it
20 | Once you have clone the repo you just need to go into the folder and run:
21 | > docker-compose up --build
22 |
23 | Note: the first time it may take a while
24 |
25 | As default the container is created, dependencies are installed, database is created and migrated but no application is started. This allow you to jump into the container and run test if you want to.
26 | To jump into the container you can copy the output of the `docker-compose up` into another console or run this:
27 | > docker exec -it HOSTNAME sh
28 |
29 | replacing HOSTNAME with the container id.
30 | To start the server you need to go run:
31 | > mix phx.server
32 |
33 | The server will start listening on the port `4000`, which is forwared from the container at the port `4005`
34 |
35 | ### How to run tests
36 | Inside the container you can run unit test using:
37 | > MIX_ENV=test mix test
38 |
39 | ### Troubleshooting
40 | If the process doesn't work try deleting all the following images from docker if presents:
41 | - live-map-app_service
42 | - postgres
43 | - elixir
44 | Try deleting also the folder _build, deps and node_module then start the process again
45 |
46 | ## Usage
47 | To start using the service you need to start it using the instruction above.
48 |
49 | In order send information to the service you have to hit the endpoint with the correct parameters:
50 | - longitude, must be a number
51 | - latitude, must be a number
52 | - downloaded_at, must be a date in the ISO format (i.e T22:31:32.223Z)
53 |
54 | An example:
55 | > curl --location --request POST 'localhost:4005/add_downaload' \
56 | --form 'longitude=-14.00' \
57 | --form 'latitude=-50.0' \
58 | --form 'downloaded_at=2021-10-11T22:31:32.223Z' \
59 | --form 'app_id=test23'
60 |
61 | To interact with the application you can go to [localhost:4005](http://localhost:4005/)
62 |
63 | ## Future Improvements and welcome MRs
64 | - More unit tests, especially in the View part and hooks, using this
65 | - More explicit error message in the endpoint
66 | - Service level tests
67 | - Heatmap Layer usint [this](https://developers.google.com/maps/documentation/javascript/heatmaplayer)
68 |
--------------------------------------------------------------------------------
/lib/live_map_app_web/controllers/download_app_controller.ex:
--------------------------------------------------------------------------------
1 | defmodule LiveMapAppWeb.DownloadAppController do
2 | use LiveMapAppWeb, :controller
3 | alias LiveMapApp.Dashboard
4 |
5 | @unknown "Unknown"
6 |
7 | @doc """
8 | Stores a new app object into the database
9 |
10 | it receives:
11 | longitude, has to be a float
12 | latitude, has to be a float
13 | downloaded_at, has to be a date in the iso format
14 | app_id
15 |
16 | it validates the attributes time and it save the value in
17 | the database
18 | Return:
19 | created if it was successfull
20 | invalid_paramters in case of validation error
21 | error in case of storage error
22 | """
23 | def add_download(conn, %{
24 | "longitude" => long,
25 | "latitude" => lat,
26 | "downloaded_at" => download_time,
27 | "app_id" => app_name
28 | }) do
29 | with {:ok, date, _} <- DateTime.from_iso8601(download_time),
30 | {longitude, ""} <- Float.parse(long),
31 | {latitude, ""} <- Float.parse(lat),
32 | country <-
33 | Tesla.get("https://maps.googleapis.com/maps/api/geocode/json",
34 | query: [
35 | latlng: "#{latitude},#{longitude}",
36 | key: Application.get_env(:live_map_app, :api_token)
37 | ]
38 | )
39 | |> handle_tesla_response() do
40 | case Dashboard.create_app(%{
41 | latitude: lat,
42 | longitude: long,
43 | app_id: app_name,
44 | download_at: date,
45 | country: country
46 | }) do
47 | {:ok, _result} ->
48 | conn
49 | |> put_status(:created)
50 | |> json(:created)
51 |
52 | {:error, error} ->
53 | conn
54 | |> put_status(500)
55 | |> json(inspect(error))
56 | end
57 | else
58 | _ ->
59 | conn
60 | |> put_status(400)
61 | |> json(:invalid_paramters)
62 | end
63 | end
64 |
65 | def add_download(conn, _params) do
66 | conn
67 | |> put_status(400)
68 | |> json(:invalid_json_body)
69 | end
70 |
71 | defp handle_tesla_response({:ok, %Tesla.Env{status: 200, body: body}}),
72 | do:
73 | body
74 | |> Jason.decode()
75 | |> parse_results()
76 |
77 | defp handle_tesla_response(_response), do: @unknown
78 |
79 | defp parse_results({:ok, %{"results" => nil}}), do: @unknown
80 |
81 | defp parse_results({:ok, %{"results" => result}}),
82 | do:
83 | result
84 | |> List.first()
85 | |> parse_address()
86 |
87 | defp parse_results(_), do: @unknown
88 |
89 | defp parse_address(nil), do: @unknown
90 |
91 | defp parse_address(components),
92 | do:
93 | components
94 | |> Map.get("address_components")
95 | |> Enum.reduce("", fn %{"types" => types, "long_name" => name}, acc ->
96 | if Enum.member?(types, "country") do
97 | acc <> name
98 | else
99 | acc
100 | end
101 | end)
102 | |> final_country()
103 |
104 | defp final_country(""), do: @unknown
105 | defp final_country(country), do: country
106 | end
107 |
--------------------------------------------------------------------------------
/assets/css/main.scss:
--------------------------------------------------------------------------------
1 | .header {
2 | padding: 1rem;
3 | text-align: center;
4 | font-weight: bolder;
5 | }
6 |
7 | .footer {
8 | grid-row-start: 2;
9 | grid-row-end: 3;
10 | text-align: center;
11 | color: grey;
12 | }
13 |
14 | .block{
15 | box-shadow: 5px 5px 5px 5px #888888;
16 | border-radius: 2em;
17 | margin-bottom: 2em;
18 | }
19 |
20 | .column-name{
21 | padding: 3rem;
22 | }
23 |
24 |
25 | #map {
26 | height: 400px;
27 | border-radius: 0em 2em 2em 0em;
28 | }
29 |
30 | dl {
31 | display: flex;
32 | background-color: white;
33 | flex-direction: column;
34 | width: 100%;
35 | max-width: 700px;
36 | position: relative;
37 | padding: 20px;
38 | }
39 |
40 | dt {
41 | align-self: flex-start;
42 | width: 100%;
43 | font-weight: 700;
44 | display: block;
45 | text-align: center;
46 | font-size: 1.2em;
47 | text-transform: uppercase;
48 | font-weight: 700;
49 | margin-bottom: 20px;
50 | margin-left: 130px;
51 | }
52 |
53 | .text {
54 | font-weight: 600;
55 | display: flex;
56 | align-items: center;
57 | height: 40px;
58 | width: 130px;
59 | background-color: white;
60 | position: absolute;
61 | left: 0;
62 | justify-content: flex-end;
63 | }
64 |
65 | .percentage {
66 | font-size: .8em;
67 | line-height: 1;
68 | text-transform: uppercase;
69 | width: 100%;
70 | height: 40px;
71 | margin-left: 130px;
72 | background: repeating-linear-gradient(
73 | to right,
74 | #ddd,
75 | #ddd 1px,
76 | #fff 1px,
77 | #fff 5%
78 | );
79 |
80 | &:after {
81 | content: "";
82 | display: block;
83 | background-color: #3d9970;
84 | width: 50px;
85 | margin-bottom: 10px;
86 | height: 90%;
87 | position: relative;
88 | top: 50%;
89 | transform: translateY(-50%);
90 | transition: background-color .3s ease;
91 | cursor: pointer;
92 | }
93 | &:hover,
94 | &:focus {
95 | &:after {
96 | background-color: #aaa;
97 | }
98 | }
99 | }
100 |
101 | @for $i from 1 through 100 {
102 | .percentage-#{$i}:after {
103 | $value: ($i * 1%);
104 | width: $value;
105 | }
106 | }
107 |
108 | input[type="radio"] {
109 | position: absolute;
110 | left: -9999px;
111 | }
112 |
113 | :root {
114 | --black: #1a1a1a;
115 | --white: #fff;
116 | --green: #49b293;
117 | }
118 |
119 | .filters {
120 | text-align: center;
121 | display: inline-block;
122 | margin-bottom: 2rem;
123 | padding: 0.5rem 1rem;
124 | margin-bottom: 0.25rem;
125 | border-radius: 2rem;
126 | min-width: 50px;
127 | line-height: normal;
128 | cursor: pointer;
129 | transition: all 0.1s;
130 | }
131 |
132 | .filters:hover {
133 | background: var(--green);
134 | color: var(--white);
135 | }
136 |
137 | .tag{
138 | display: inline;
139 | font-weight: lighter;
140 | }
141 |
142 | .dashboard-container{
143 | display: table-caption;
144 | margin: 2em;
145 | }
146 |
147 | [value="All"]:checked ~ .dashboard [data-category] {
148 | display: block;
149 | }
150 | [value="List"]:checked ~ .dashboard:not([data-category*="List"]),
151 | [value="ByCountry"]:checked ~ .dashboard:not([data-category*="ByCountry"]),
152 | [value="ByYear"]:checked ~ .dashboard:not([data-category*="ByYear"]),
153 | [value="ByMonth"]:checked ~ .dashboard:not([data-category*="ByMonth"]),
154 | [value="ByDay"]:checked ~ .dashboard:not([data-category*="ByDay"]),
155 | [value="ByDayTime"]:checked ~ .dashboard:not([data-category*="ByDayTime"]),
156 | [value="ByApp"]:checked ~ .dashboard:not([data-category*="ByApp"]) {
157 | display: none;
158 | }
159 |
--------------------------------------------------------------------------------
/lib/live_map_app_web/live/app_live/index.html.leex:
--------------------------------------------------------------------------------
1 |
2 |