Commit 25ad8396 authored by Mikko Ahlroth's avatar Mikko Ahlroth

Refactor to RE:DOM & Concise CSS

* Initial version of front page ready with updating world map graph
* Added GeoIP functionality for world map locations
* Using MBU for frontend building instead of Brunch
parent 8993e173
rules:
no-ids: 0
property-sort-order: 0
no-qualifying-elements: 0
force-element-nesting: 0
force-attribute-nesting: 0
function-name-format: 0
leading-zero: 0
no-color-literals: 0
File added
exports.config = {
// See http://brunch.io/#documentation for docs.
files: {
javascripts: {
joinTo: {
'js/app.js': /^(web\/static\/vendor)|(node_modules)|(web\/static\/js\/)|(web\/static\/elm-bin\/)/
}
// To use a separate vendor.js bundle, specify two files path
// https://github.com/brunch/brunch/blob/stable/docs/config.md#files
// joinTo: {
// "js/app.js": /^(web\/static\/js)/,
// "js/vendor.js": /^(web\/static\/vendor)|(deps)/
// }
//
// To change the order of concatenation of files, explicitly mention here
// https://github.com/brunch/brunch/tree/master/docs#concatenation
// order: {
// before: [
// "web/static/vendor/js/jquery-2.1.1.js",
// "web/static/vendor/js/bootstrap.min.js"
// ]
// }
},
stylesheets: {
joinTo: {
'css/app.css': /^web\/static\/css\/app\.scss$/,
}
},
templates: {
joinTo: 'js/app.js'
}
},
conventions: {
// This option sets where we should place non-css and non-js assets in.
// By default, we set this to "/web/static/assets". Files in this directory
// will be copied to `paths.public`, which is "priv/static" by default.
assets: /^(web\/static\/assets)/
},
// Phoenix paths configuration
paths: {
// Dependencies and current project directories to watch
watched: [
'web/static',
'test/static',
'web/static/elm'
],
// Where to compile files to
public: 'priv/static'
},
// Configure your plugins
plugins: {
babel: {
// Do not use ES6 compiler in vendor or elm code
ignore: [/web\/static\/vendor/, /web\/static\/elm-bin/],
presets: ['es2015', 'es2016']
},
sass: {
mode: 'native',
// Bootstrap needs higher precision to match precompiled pixel values
precision: 8,
options: {
includePaths: [
'node_modules/bootstrap-sass/assets/stylesheets'
]
}
},
elmBrunch: {
// This is relative to the `elmFolder` below
executablePath: '../../../node_modules/elm/binwrappers',
elmFolder: 'web/static/elm',
mainModules: ['IndexPage/Updater.elm', 'Profile/MainUpdater.elm', 'Profile/TotalUpdater.elm'],
// Compile elm files into elm-bin where they can be picked up from by the
// javascript compiler
outputFolder: '../elm-bin',
// Compile all Elm main files into single JS file
outputFile: 'elm-app.js'
}
},
modules: {
autoRequire: {
'js/app.js': ['web/static/js/app']
}
},
npm: {
enabled: true,
// Whitelist the npm deps to be pulled in as front-end assets.
// All other deps in package.json will be excluded from the bundle.
whitelist: ['phoenix', 'phoenix_html']
},
conventions: {
// Don't scan for javascript files inside elm-stuff folders
ignored: [/elm-stuff/]
}
};
......@@ -90,6 +90,25 @@ config :number,
separator: "."
]
config :geolix,
databases: [
%{
id: :city,
adapter: Geolix.Adapter.MMDB2,
source: "#{__DIR__}/geoip-cities.gz"
},
%{
id: :country,
adapter: Geolix.Adapter.MMDB2,
source: "#{__DIR__}/geoip-countries.gz"
}
]
config :geolite2data,
geolix_updater: true,
logger: true
# Appsignal configuration
config :code_stats, CodeStats.Endpoint,
......
......@@ -4,6 +4,7 @@ defmodule CodeStats.Endpoint do
socket "/live_update_socket", CodeStats.LiveUpdateSocket
plug CodeStats.RequestTime
plug RemoteIp
# Serve at "/" the static files from "priv/static" directory.
#
......
......@@ -7,12 +7,11 @@ defmodule Mix.Tasks.Frontend.Build.Assets do
@deps []
def in_path(), do: Path.join([src_path(), "assets"])
def out_path(), do: Path.join([dist_path(), "assets"])
task _ do
# Ensure target path exists
File.mkdir_p!(out_path())
File.mkdir_p!(dist_path())
File.cp_r!(in_path(), out_path())
File.cp_r!(in_path(), dist_path())
end
end
......@@ -12,25 +12,26 @@ defmodule Mix.Tasks.Frontend.Build.Js.Bundle do
def bin(), do: node_bin("rollup")
def out_path(), do: Path.join([tmp_path(), "bundled", "js"])
def out_file(), do: Path.join([out_path(), "app.js"])
def args() do
op = out_path()
[
"--config",
"rollup.config.js",
"--input",
Path.join([Mix.Tasks.Frontend.Build.Js.Transpile.out_path(), "app.js"]),
"--output",
Path.join([op, "app.js"]),
out_file(),
"--format",
"cjs",
"iife",
"--sourcemap",
Path.join([op, "app.js.map"])
Path.join([out_path(), "app.js.map"])
]
end
task _ do
bin() |> exec(args()) |> listen()
print_size(out_file())
end
end
......@@ -19,7 +19,7 @@ defmodule CodeStats.Mixfile do
def application do
[mod: {CodeStats, []},
applications: [:phoenix, :phoenix_pubsub, :phoenix_html, :cowboy, :logger, :gettext,
:phoenix_ecto, :postgrex, :comeonin, :calendar, :bamboo, :appsignal]]
:phoenix_ecto, :postgrex, :comeonin, :calendar, :bamboo, :appsignal, :geolix, :geolite2data]]
end
# Specifies which paths to compile per environment.
......@@ -48,8 +48,11 @@ defmodule CodeStats.Mixfile do
{:bamboo, "1.0.0-rc.1"},
{:corsica, "~> 1.0.0"},
{:appsignal, "~> 1.3"},
{:mbu, "~> 0.2.4"},
{:fs, "~> 2.12.0", override: true}
{:mbu, "~> 0.3.0"},
{:fs, "~> 2.12.0", override: true},
{:geolix, "~> 0.13.0"},
{:geolite2data, "~> 0.0.3"},
{:remote_ip, "~> 0.1.3"}
]
end
......
......@@ -4,23 +4,24 @@
"repository": {},
"dependencies": {
"babel-polyfill": "~6.23.0",
"concise-ui": "~0.2.1",
"concise.css": "~4.1.2",
"phoenix": "file:deps/phoenix",
"phoenix_html": "file:deps/phoenix_html",
"redom": "~2.2.1"
"redom": "~2.4.1"
},
"devDependencies": {
"babel-cli": "~6.24.0",
"babel-core": "~6.24.0",
"babel-preset-es2015": "~6.24.0",
"babel-preset-es2016": "~6.22.0",
"babel-preset-es2017": "~6.22.0",
"babel-cli": "~6.24.1",
"babel-core": "~6.24.1",
"babel-preset-es2015": "~6.24.1",
"babel-preset-es2016": "~6.24.1",
"babel-preset-es2017": "~6.24.1",
"concise-cli": "~0.4.1",
"cssnano": "~3.10.0",
"rollup": "~0.41.6",
"rollup-plugin-commonjs": "~8.0.2",
"rollup-plugin-node-resolve": "~2.1.0",
"rollup-plugin-sourcemaps": "~0.4.1",
"rollup-plugin-node-resolve": "~3.0.0",
"rollup-plugin-sourcemaps": "~0.4.2",
"uglifyjs": "~2.4.10"
}
}
......@@ -32,23 +32,26 @@ defmodule CodeStats.FrontpageChannel do
The given pulse must have xps preloaded, xps must have language preloaded.
"""
def send_pulse(%User{} = user, %Pulse{xps: xps})
def send_pulse(%User{private_profile: false} = user, coords, %Pulse{xps: xps})
when not is_nil(xps) do
# Don't show username for private profiles
username = case user.private_profile do
true -> "Private user"
false -> user.username
end
formatted_xps = for xp <- xps do
%{
xp: xp.amount,
language: xp.language.name,
username: username
language: xp.language.name
}
end
CodeStats.Endpoint.broadcast("frontpage", "new_pulse", %{xps: formatted_xps})
CodeStats.Endpoint.broadcast(
"frontpage",
"new_pulse",
%{
xps: formatted_xps,
username: user.username,
coords: coords
}
)
end
def send_pulse(_, _, _), do: nil
end
......@@ -16,9 +16,12 @@ defmodule CodeStats.PulseController do
XP,
CacheService,
ProfileChannel,
FrontpageChannel
FrontpageChannel,
GeoIP
}
plug GeoIP
def add(conn, %{"coded_at" => timestamp, "xps" => xps}) when is_list(xps) do
{user, machine} = AuthUtils.get_api_details(conn)
......@@ -31,24 +34,27 @@ defmodule CodeStats.PulseController do
:ok <- update_caches(inserted_xps)
do
# Broadcast XP data to possible viewers on profile page and frontpage
coords = GeoIP.get_coords(conn)
ProfileChannel.send_pulse(user, %{pulse | xps: inserted_xps})
FrontpageChannel.send_pulse(user, %{pulse | xps: inserted_xps})
FrontpageChannel.send_pulse(user, coords, %{pulse | xps: inserted_xps})
conn |> put_status(201) |> json(%{"ok" => "Great success!"})
conn |> put_status(201) |> json(%{ok: "Great success!"})
else
{:error, :not_found, reason} ->
conn |> put_status(404) |> json(%{"error" => reason})
conn |> put_status(404) |> json(%{error: reason})
{:error, :generic, reason} ->
conn |> put_status(400) |> json(%{"error" => reason})
conn |> put_status(400) |> json(%{error: reason})
{:error, :internal, reason} ->
conn |> put_status(500) |> json(%{"error" => reason})
conn |> put_status(500) |> json(%{error: reason})
end
end
def add(conn, _params) do
resp(conn, 400, %{"error" => "Invalid xps format."})
conn
|> put_status(400)
|> json(%{error: "Invalid xps format."})
end
defp parse_timestamp(timestamp) do
......
defmodule CodeStats.GeoIP do
@moduledoc """
Plug for adding GeoIP information about user to conn.
Contains also utility functions for dealing with the GeoIP information.
"""
@geoip_key :_codestats_geoip_data
# Round to this many decimal places to prevent being too accurate and preserve privacy
@accuracy 1
import Plug.Conn
def init(opts) do
opts
end
def call(conn, _opts) do
put_private(conn, @geoip_key, Geolix.lookup(conn.remote_ip))
end
@doc """
Get user's geoip coordinates from conn, if available, nil otherwise.
"""
def get_coords(conn) do
case conn.private[@geoip_key] do
%{
city: %Geolix.Result.City{
location: %Geolix.Record.Location{
latitude: lat,
longitude: lon
}
}
} ->
%{
lat: Float.round(lat, @accuracy),
lon: Float.round(lon, @accuracy)
}
_ ->
nil
end
end
end
......@@ -6,7 +6,7 @@ defmodule CodeStats.SetSessionUser do
this plug.
"""
@private_info_key :codestats_session_user
@private_info_key :_codestats_session_user
import Plug.Conn
import Ecto.Query, only: [from: 2]
......@@ -26,7 +26,7 @@ defmodule CodeStats.SetSessionUser do
id = AuthUtils.get_current_user_id(conn)
query = from u in User,
where: u.id == ^id
put_private(conn, @private_info_key, Repo.one(query))
else
put_private(conn, @private_info_key, nil)
......
@import '../../../node_modules/concise.css/concise';
// Space out content a bit
body, form, ul, table {
margin-top: 20px;
margin-bottom: 20px;
}
@media (min-width: 1200px) {
.container{
max-width: 800px;
}
}
// Phoenix flash messages
.alert:empty { display: none; }
// Phoenix inline forms in links and buttons
form {
&.link, &.button {
display: inline;
}
}
// Custom page header
.header {
border-bottom: 1px solid #e5e5e5;
}
.logo {
height: 60px;
display: block;
margin-bottom: 0;
background-size: 519px 71px;
>a {
text-decoration: none;
color: inherit;
}
}
// Everything but the jumbotron gets side spacing for mobile first views
.header,
.marketing {
padding-right: 15px;
padding-left: 15px;
}
// Main marketing message
.jumbotron {
text-align: center;
border-bottom: 1px solid #e5e5e5;
}
// Supporting marketing content
.marketing {
margin: 35px 0;
}
footer {
.footer-info {
text-align: center;
}
ul.footer-info {
list-style-type: none;
display: block;
margin: 0.7em;
padding: 0;
@charset 'UTF-8';
li {
display: inline;
margin: 0;
padding: 0;
}
@import 'settings';
li:after {
content: " – ";
}
li:first-of-type:before {
content: ""
}
li:last-of-type:after {
content: "";
}
}
}
// Responsive: Portrait tablets and up
@media screen and (min-width: 768px) {
// Remove the padding we set earlier
.header,
.marketing {
padding-right: 0;
padding-left: 0;
}
// Space out the masthead
.header {
margin-bottom: 30px;
}
// Remove the bottom border on the jumbotron for visual effect
.jumbotron {
border-bottom: 0;
}
}
.machine-action {
margin-right: 5px;
}
#profile-username {
display: inline;
}
.profile-detail-list {
list-style: none;
margin-left: 0;
padding-left: 0;
//display: inline;
li {
//display: inline;
//margin-left: 10px;
&:before {
content: "☞ ";
}
}
}
// Ticker in the live update for index page
ul.ticker {
list-style: none;
font-family: monospace;
&:after {
position: absolute;
bottom: 0;
height: 100%;
width: 100%;
content: "";
background: linear-gradient(to top, rgba(255, 255, 255, 1) 5%, rgba(255, 255, 255, 0) 30%);
pointer-events: none;
}
}
@import '../../../node_modules/concise.css/concise';
@import '../../../node_modules/concise-ui/concise-ui';
// Plugin page editor icons
.media {
.media-object {
width: 8em;
}
}
@import 'layout';
@import 'ui';
@import 'graph';
.hidden-fadeout {
opacity: 0;
transition: opacity 1s ease-in-out;
}
.world-map {
position: relative;
>img {
max-width: 100%;
}
>.pulses {
>.pulse-indicator {
position: absolute;
border-radius: 50%;
background-color: #fff;
}
}
}
main {
margin: 0;
}
#nav-stripe {
// Eat flash-stripe's additional space from the bottom
margin-bottom: -1lh;
img#header-logo {
display: block;
max-height: 4lh;
width: auto;
height: auto;
}
nav[role='navigation'] {
>div {
text-align: right;
}
}
}
#footer-stripe {
text-align: center;
}
// Stripe that is the width of the page but has padding for the content
.stripe {
width: 100%;
margin: 0;
padding: 0.5lh 0;
background-color: getColor(background, body);
// Stripe contents that take the size of a container and are centered
>.stripe-inner {
margin: 0 auto;
max-width: $container-width;
padding-left: $gutter / 2;
padding-right: $gutter / 2;
overflow: visible;
}
&.stripe-dark {
background-color: getColor(background, dark);
color: getColor(text, inverted);
h1, h2, h3, h4, h5, h6 {
color: getColor(text, inverted);
}
a {
color: getColor(text, inverted_link);
&:hover {
color: getColor(text, inverted_link_hover);
}
}
}
}
//
// Base
// --------------------------------------------------
// Font size for small devices
$font-size: 16;
// Font size for big devices
$font-size-secondary: 18;
// Tracking
$letter-spacing: .05em;
// Font families
$font-primary: 'Helvetica', 'Arial', sans-serif;
$font-secondary: 'Helvetica', 'Arial', sans-serif;
$font-mono: 'Consolas', monospace;
$font-print-primary: 'Georgia', 'Times New Roman', 'Times', serif;
$font-print-secondary: 'Georgia', 'Times New Roman', 'Times', serif;
// Enable margins to all the elements
// except the first one in each nesting level
$automargin: true;
// ^ How much margin for those elements
$block-margin: 1lh;
// Transition duration
$transition-duration: 150ms;
// Custom media queries
// Use as @media (--medium) { … }
@custom-media --extra-small (width <= 480px);
@custom-media --small (width >= 480px);
@custom-media --medium (width >= 768px);
@custom-media --large (width >= 960px);
@custom-media --extra-large (width >= 1100px);
@custom-media --only-small (480px < width <= 768px);
@custom-media --only-medium (768px < width <= 980px);
@custom-media --only-large (980px < width <= 1100px);
// Spacing variables
$spacing-xs: .5lh;
$spacing-s: 1lh;
$spacing-m: 2lh;
$spacing-l: 3lh;
$spacing-xl: 4lh;
//
// Type Scale
// --------------------------------------------------
// Suggested ratios
// Source: http://type-scale.com/
$_minor-second: 1.067;
$_major-second: 1.125;
$_minor-third: 1.200;
$_major-third: 1.250;
$_perfect-fourth: 1.333;
$_augmented-fourth: 1.414;
$_perfect-fifth: 1.500;
$_golden-ratio: 1.618;