Initial commit

This commit is contained in:
Mikko Ahlroth 2015-05-14 23:58:25 +03:00
commit d07442ed66
49 changed files with 2570 additions and 0 deletions

7
.gitignore vendored Normal file
View file

@ -0,0 +1,7 @@
/_build
/deps
erl_crash.dump
*.ez
config.exs
/data

17
README.md Normal file
View file

@ -0,0 +1,17 @@
# Mebe
Mebe -- _the Minimalistic Elixir Blog Engine_ -- is a simple blog engine written in [Elixir](https://elixir-lang.org),
using the [Phoenix Framework](http://www.phoenixframework.org/).
The engine consists of two parts, both in the `apps/` directory:
1. MebeEngine, which handles parsing the data files into an ETS (_Erlang Term Storage_) in-memory database, and
2. MebeWeb, which uses the Phoenix Framework to serve the blog data to clients.
## Installation for development
* `git clone`
* Copy `*.exs.dist`, removing the `.dist` ending and go through the configs.
* `npm install && bower install && gulp` to build the frontend.
* `mix phoenix.server` to run the development server.

4
apps/mebe_engine/.gitignore vendored Normal file
View file

@ -0,0 +1,4 @@
/_build
/deps
erl_crash.dump
*.ez

View file

@ -0,0 +1,4 @@
MebeEngine
==========
** TODO: Add description **

View file

@ -0,0 +1,27 @@
# This file is responsible for configuring your application
# and its dependencies with the aid of the Mix.Config module.
use Mix.Config
# This configuration is loaded before any dependency and is restricted
# to this project. If another project depends on this project, this
# file won't be loaded nor affect the parent project. For this reason,
# if you want to provide default values for your application for third-
# party users, it should be done in your mix.exs file.
# Sample configuration:
#
# config :logger, :console,
# level: :info,
# format: "$date $time [$level] $metadata$message\n",
# metadata: [:user_id]
# It is also possible to import configuration files, relative to this
# directory. For example, you can emulate configuration per environment
# by uncommenting the line below and defining dev.exs, test.exs and such.
# Configuration from the imported file will override the ones defined
# here (which is why it is important to import them last).
#
# import_config "#{Mix.env}.exs"
config :mebe_engine,
data_path: "CONFIGURE" # The path to crawl post and page data from. Relative to this file's path

View file

@ -0,0 +1,55 @@
defmodule MebeEngine.Crawler do
@moduledoc """
The crawler goes through the specified directory, opening and parsing all the matching files
inside concurrently.
"""
alias MebeEngine.Parser
alias MebeEngine.Models.Page
alias MebeEngine.Models.Post
def crawl(path) do
get_files(path)
|> Enum.map(fn file -> Task.async MebeEngine.Crawler, :parse, [file] end)
|> handle_responses
|> construct_archives
end
def get_files(path) do
Path.wildcard path <> "/**/*.md"
end
def parse(file) do
File.read!(file)
|> Parser.parse(Path.basename file)
end
def handle_responses(tasklist) do
Enum.map tasklist, fn task -> Task.await task end
end
def construct_archives(datalist) do
Enum.reduce datalist, %{pages: %{}, posts: [], years: %{}, months: %{}, tags: %{}}, fn pagedata, acc ->
case pagedata.__struct__ do
Page -> %{acc | pages: Map.put(acc.pages, pagedata.slug, pagedata)}
Post ->
{year, month, _} = pagedata.date
tags = Enum.reduce pagedata.tags, acc.tags, fn tag, tagmap ->
posts = Map.get(tagmap, tag, [])
Map.put(tagmap, tag, [pagedata | posts])
end
%{
acc |
posts: [pagedata | acc.posts],
years: Map.put(acc.years, year, pagedata),
months: Map.put(acc.months, {year, month}, pagedata),
tags: tags
}
end
end
end
end

101
apps/mebe_engine/lib/db.ex Normal file
View file

@ -0,0 +1,101 @@
defmodule MebeEngine.DB do
@moduledoc """
Stuff related to storing the blog data to memory (ETS).
"""
# Table for meta information, like the counts of posts
@meta_table :meta
@page_table :pages
# Table for sequential retrieval of posts (for index page)
@post_table :posts
# Table for quick retrieval of single post (with key)
@single_post_table :single_posts
@year_table :years
@month_table :months
@tag_table :tags
def init() do
:ets.new @meta_table, [:named_table, :set, :protected, read_concurrency: true]
:ets.new @page_table, [:named_table, :set, :protected, read_concurrency: true]
:ets.new @post_table, [:named_table, :ordered_set, :protected, read_concurrency: true]
:ets.new @single_post_table, [:named_table, :set, :protected, read_concurrency: true]
:ets.new @year_table, [:named_table, :set, :protected, read_concurrency: true]
:ets.new @month_table, [:named_table, :set, :protected, read_concurrency: true]
:ets.new @tag_table, [:named_table, :ordered_set, :protected, read_concurrency: true]
end
def insert_count(key, count) do
:ets.insert @meta_table {key, val}
end
def insert_post(post) do
{year, month, day} = post.date
:ets.insert @post_table, {{year, month, day, post.order}, post}
:ets.insert @single_post_table, {{year, month, day, post.slug}, post}
end
def insert_page(page) do
:ets.insert @page_table, {page.slug, page}
end
def insert_year(year, posts) do
:ets.insert @year_table, {year, posts}
end
def insert_month(month, posts) do
:ets.insert @month_table, {month, posts}
end
def insert_tag(tag, posts) do
:ets.insert @tag_table, {tag, posts}
end
def get_reg_posts(first \\ 0, limit \\ :all)
def get_reg_posts(first, limit) do
get_posts(@post_table, first, limit)
end
def get_count(:all) do
[{_, count}] = :ets.match_object @meta_table, {:all, :"$1"}
count
end
def get_post(year, month, day, slug) do
case :ets.match_object @single_post_table, {{year, month, day, slug}, :"$1"} do
[{_, post}] -> post
_ -> nil
end
end
def get_pages() do
:ets.match(@page_table, :"$1")
|> ets_to_data
end
defp get_posts(table, first \\ 0, limit \\ :all)
# Optimization for getting all
defp get_posts(table, 0, :all) do
:ets.select_reverse(table, [{:"$1", [], [:"$_"]}])
|> ets_to_data
end
defp get_posts(table, first, limit) when is_integer(first) and is_integer(limit) do
{result, _} = :ets.select_reverse table, [{:"$1", [], [:"$_"]}], first + limit
{_, result} = Enum.split result, first
ets_to_data result
end
# Remove key from data returned from ETS
defp ets_to_data(data) do
Enum.map data, fn {_, actual} -> actual end
end
end

View file

@ -0,0 +1,18 @@
defmodule MebeEngine do
use Application
# See http://elixir-lang.org/docs/stable/elixir/Application.html
# for more information on OTP Applications
def start(_type, _args) do
import Supervisor.Spec, warn: false
children = [
worker(MebeEngine.Worker, [])
]
# See http://elixir-lang.org/docs/stable/elixir/Supervisor.html
# for other strategies and supported options
opts = [strategy: :one_for_one, name: MebeEngine.Supervisor]
Supervisor.start_link(children, opts)
end
end

View file

@ -0,0 +1,33 @@
defmodule MebeEngine.Models do
@moduledoc """
This module contains the data models (not db models) of the blog engine.
"""
defmodule PageData do
defstruct filename: nil,
title: nil,
headers: [],
content: nil
end
defmodule Post do
defstruct slug: nil,
title: nil,
date: nil,
tags: [],
content: nil,
short_content: nil,
order: 0
end
defmodule Page do
defstruct slug: nil,
title: nil,
content: nil
end
defmodule Archive do
defstruct key: nil,
posts: nil
end
end

View file

@ -0,0 +1,104 @@
defmodule MebeEngine.Parser do
@moduledoc """
This module contains the parser, which parses page data and returns the contents in the correct format.
"""
alias MebeEngine.Models.PageData
alias MebeEngine.Models.Page
alias MebeEngine.Models.Post
def parse(pagedata, filename) do
split_lines(pagedata)
|> parse_raw(%PageData{filename: filename})
|> render_content
|> format
end
def split_lines(pagedata) do
String.split pagedata, ~R/\r?\n/
end
def parse_raw(datalines, pagedata \\ %PageData{}, mode \\ :title)
def parse_raw([title | rest], pagedata, :title) do
parse_raw rest, %{pagedata | title: title}, :headers
end
def parse_raw(["" | rest], pagedata, :headers) do
# Reverse the headers so they appear in the that they do in the file
parse_raw rest, %{pagedata | headers: Enum.reverse(pagedata.headers)}, :content
end
def parse_raw([header | rest], pagedata, :headers) do
headers = [header | pagedata.headers]
parse_raw rest, %{pagedata | headers: headers}, :headers
end
def parse_raw(content, pagedata, :content) when is_list(content) do
%{pagedata | content: Enum.join(content, "\n")}
end
def render_content(pagedata) do
%{pagedata | content: Earmark.to_html pagedata.content}
end
def format(%PageData{
filename: filename,
title: title,
headers: headers,
content: content
}) do
case Regex.run(~R/^(?:(\d{4})-(\d{2})-(\d{2})(?:-(\d{2}))?-)?(.*?).md$/iu, filename) do
[_, "", "", "", "", slug] ->
%Page{
slug: slug,
title: title,
content: content
}
[_, year, month, day, order, slug] ->
[tags | _] = headers
order = format_order order
%Post{
slug: slug,
title: title,
date: date_to_int_tuple({year, month, day}),
tags: parse_tags(tags),
content: content,
short_content: hd(String.split content, ~R/<!--\s*SPLIT\s*-->/u),
order: order
}
end
end
defp parse_tags(tagline) do
String.split tagline, ~R/,\s*/iu
end
defp date_to_int_tuple({year, month, day}) do
{
str_to_int(year),
str_to_int(month),
str_to_int(day)
}
end
defp str_to_int(str) do
{int, _} = String.lstrip(str, ?0)
|> Integer.parse
int
end
defp format_order(""), do: 0
defp format_order(order), do: str_to_int order
end

View file

@ -0,0 +1,46 @@
defmodule MebeEngine.Worker do
@moduledoc """
This worker initializes the post database and keeps it alive while the server is running.
"""
use GenServer
require Logger
alias MebeEngine.Crawler
alias MebeEngine.DB
def start_link(opts \\ []) do
GenServer.start_link(__MODULE__, :ok, opts)
end
def init(:ok) do
load_db
{:ok, nil}
end
@doc """
Initialize the database by crawling the configured path and parsing data to the DB.
"""
def load_db() do
Logger.info "Loading post database…"
%{
pages: pages,
posts: posts,
tags: tags,
years: years,
months: months,
} = MebeEngine.Crawler.crawl Application.get_env(:mebe_engine, :data_path)
Logger.info "Loaded #{Enum.count pages} pages and #{Enum.count posts} posts."
DB.init
Enum.each posts, fn post -> DB.insert_post post end
Enum.each Map.keys(pages), fn page -> DB.insert_page pages[page] end
Enum.each Map.keys(tags), fn tag -> DB.insert_tag(tag, tags[tag]) end
Enum.each Map.keys(years), fn year -> DB.insert_year(year, years[year]) end
Enum.each Map.keys(months), fn month -> DB.insert_month(month, months[month]) end
Logger.info "Posts loaded."
end
end

41
apps/mebe_engine/mix.exs Normal file
View file

@ -0,0 +1,41 @@
defmodule MebeEngine.Mixfile do
use Mix.Project
def project do
[app: :mebe_engine,
version: "0.0.1",
deps_path: "../../deps",
lockfile: "../../mix.lock",
elixir: "~> 1.0",
build_embedded: Mix.env == :prod,
start_permanent: Mix.env == :prod,
deps: deps]
end
# Configuration for the OTP application
#
# Type `mix help compile.app` for more information
def application do
[applications: [:logger],
mod: {MebeEngine, []}]
end
# Dependencies can be Hex packages:
#
# {:mydep, "~> 0.3.0"}
#
# Or git/path repositories:
#
# {:mydep, git: "https://github.com/elixir-lang/mydep.git", tag: "0.1.0"}
#
# To depend on another app inside the umbrella:
#
# {:myapp, in_umbrella: true}
#
# Type `mix help deps` for more examples and options
defp deps do
[
{:earmark, "~> 0.1.16"}
]
end
end

View file

@ -0,0 +1,7 @@
defmodule MebeEngineTest do
use ExUnit.Case
test "the truth" do
assert 1 + 1 == 2
end
end

View file

@ -0,0 +1 @@
ExUnit.start()

0
apps/mebe_web/.bowerrc Normal file
View file

19
apps/mebe_web/.gitignore vendored Normal file
View file

@ -0,0 +1,19 @@
# Mix artifacts
/_build
/deps
/*.ez
# Generate on crash by the VM
erl_crash.dump
# Configs
/config/*.exs
# Static artifacts
/node_modules
/bower_components
# Since we are building js and css from web/static,
# we ignore priv/static/{css,js}. You may want to
# comment this depending on your deployment strategy.
/priv/static/

8
apps/mebe_web/README.md Normal file
View file

@ -0,0 +1,8 @@
# MebeWeb
To start your new Phoenix application:
1. Install dependencies with `mix deps.get`
2. Start Phoenix endpoint with `mix phoenix.server`
Now you can visit `localhost:4000` from your browser.

20
apps/mebe_web/bower.json Normal file
View file

@ -0,0 +1,20 @@
{
"name": "mebe_web",
"version": "0.0.1",
"authors": [
"Mikko Ahlroth <mikko.ahlroth@gmail.com>"
],
"description": "Minimalistic Elixir Blog Engine",
"license": "MIT",
"ignore": [
"**/.*",
"node_modules",
"bower_components",
"test",
"tests"
],
"dependencies": {
"bootstrap-sass": "~3.3.4",
"jquery": "~2.1.4"
}
}

View file

@ -0,0 +1,28 @@
# This file is responsible for configuring your application
# and its dependencies with the aid of the Mix.Config module.
#
# This configuration file is loaded before any dependency and
# is restricted to this project.
use Mix.Config
# Configures the endpoint
config :mebe_web, MebeWeb.Endpoint,
url: [host: "localhost"],
root: Path.expand("..", __DIR__),
secret_key_base: "CONFIGURE",
debug_errors: false
config :blog_config,
blog_name: "CONFIGURE",
blog_author: "CONFIGURE",
posts_per_page: 10
# Configures Elixir's Logger
config :logger, :console,
format: "$time $metadata[$level] $message\n",
metadata: [:request_id]
# Import environment specific config. This must remain at the bottom
# of this file so it overrides the configuration defined above.
import_config "#{Mix.env}.exs"

View file

@ -0,0 +1,27 @@
use Mix.Config
# For development, we disable any cache and enable
# debugging and code reloading.
#
# The watchers configuration can be used to run external
# watchers to your application. For example, we use it
# with brunch.io to recompile .js and .css sources.
config :mebe_web, MebeWeb.Endpoint,
http: [port: 4000],
debug_errors: true,
code_reloader: true,
cache_static_lookup: false,
watchers: []
# Watch static and templates for browser reloading.
config :mebe_web, MebeWeb.Endpoint,
live_reload: [
patterns: [
~r{priv/static/.*(js|css|png|jpeg|jpg|gif)$},
~r{web/views/.*(ex)$},
~r{web/templates/.*(eex)$}
]
]
# Do not include metadata nor timestamps in development logs
config :logger, :console, format: "[$level] $message\n"

View file

@ -0,0 +1,46 @@
use Mix.Config
# For production, we configure the host to read the PORT
# from the system environment. Therefore, you will need
# to set PORT=80 before running your server.
#
# You should also configure the url host to something
# meaningful, we use this information when generating URLs.
config :mebe_web, MebeWeb.Endpoint,
http: [port: {:system, "PORT"}],
url: [host: "example.com"],
cache_static_manifest: "priv/static/manifest.json"
# ## SSL Support
#
# To get SSL working, you will need to add the `https` key
# to the previous section:
#
# config:mebe_web, MebeWeb.Endpoint,
# ...
# https: [port: 443,
# keyfile: System.get_env("SOME_APP_SSL_KEY_PATH"),
# certfile: System.get_env("SOME_APP_SSL_CERT_PATH")]
#
# Where those two env variables point to a file on
# disk for the key and cert.
# Do not print debug messages in production
config :logger, level: :info
# ## Using releases
#
# If you are doing OTP releases, you need to instruct Phoenix
# to start the server for all endpoints:
#
# config :phoenix, :serve_endpoints, true
#
# Alternatively, you can configure exactly which server to
# start per endpoint:
#
# config :mebe_web, MebeWeb.Endpoint, server: true
#
# Finally import the config/prod.secret.exs
# which should be versioned separately.
import_config "prod.secret.exs"

View file

@ -0,0 +1,7 @@
use Mix.Config
# In this file, we keep production configuration that
# you likely want to automate and keep it away from
# your version control system.
config :mebe_web, MebeWeb.Endpoint,
secret_key_base: "CONFIGURE"

View file

@ -0,0 +1,10 @@
use Mix.Config
# We don't run a server during test. If one is required,
# you can enable the server option below.
config :mebe_web, MebeWeb.Endpoint,
http: [port: 4001],
server: false
# Print only warnings and errors during test
config :logger, level: :warn

76
apps/mebe_web/gulpfile.js Normal file
View file

@ -0,0 +1,76 @@
// Node modules
var fs = require('fs'), vm = require('vm'), chalk = require('chalk');
// Gulp and plugins
var gulp = require('gulp'), concat = require('gulp-concat'),
replace = require('gulp-replace'), uglify = require('gulp-uglify');
// Gulp minify for smallinizing our CSS
var minify = require('gulp-minify-css');
// Gulp filesize for printing sizes before and after minification
var size = require('gulp-size');
// SASS compiler
var sass = require('gulp-sass');
// Deleting files
var vinylPaths = require('vinyl-paths');
var del = require('del');
// Source maps
var sourcemaps = require('gulp-sourcemaps');
var bower_path = 'bower_components/';
var dest_path = 'priv/static/';
// Minifies all JS files and copies to target path. Files are not concatenated
gulp.task('js', function() {
return gulp.src([
// Uncomment these two lines if you need bootstrap or jQuery
//bower_path + 'jquery/dist/jquery.js',
//bower_path + 'bootstrap-sass/assets/javascripts/bootstrap.js',
'web/static/js/*.js'
])
//.pipe(concat('app.js'))
.pipe(sourcemaps.init())
.pipe(size({title: 'Original JS'}))
.pipe(uglify({ preserveComments: false }).on('error', console.error.bind(console)))
.pipe(size({title: 'Minified JS'}))
.pipe(sourcemaps.write('./'))
.pipe(gulp.dest(dest_path + 'js/'));
});
// Compiles SASS files. Will create one output file for each input file, but usually you
// only have app.scss which will be output as app.css
gulp.task('css', function() {
return gulp.src([
'web/static/css/*.scss'
])
.pipe(sass().on('error', sass.logError))
.pipe(sourcemaps.init())
.pipe(size({title: 'Compiled CSS'}))
.pipe(minify())
.pipe(size({title: 'Minified CSS'}))
.pipe(sourcemaps.write('./'))
.pipe(gulp.dest(dest_path + 'css/'));
});
// Copies fonts
gulp.task('fonts', function() {
return gulp.src(bower_path + 'bootstrap-sass/assets/fonts/bootstrap/*')
.pipe(gulp.dest(dest_path + 'fonts/'));
});
// Removes all files from dest_path
gulp.task('clean', function() {
return gulp.src(dest_path + '**/*', { read: false })
.pipe(vinylPaths(del));
});
gulp.task('default', ['js', 'css', 'fonts'], function(callback) {
callback();
console.log('\nPlaced optimized files in ' + chalk.magenta(dest_path));
});

View file

@ -0,0 +1,26 @@
defmodule MebeWeb do
use Application
# See http://elixir-lang.org/docs/stable/elixir/Application.html
# for more information on OTP Applications
def start(_type, _args) do
import Supervisor.Spec, warn: false
children = [
# Start the endpoint when the application starts
supervisor(MebeWeb.Endpoint, []),
]
# See http://elixir-lang.org/docs/stable/elixir/Supervisor.html
# for other strategies and supported options
opts = [strategy: :one_for_one, name: MebeWeb.Supervisor]
Supervisor.start_link(children, opts)
end
# Tell Phoenix to update the endpoint configuration
# whenever the application is updated.
def config_change(changed, _new, removed) do
MebeWeb.Endpoint.config_change(changed, removed)
:ok
end
end

View file

@ -0,0 +1,33 @@
defmodule MebeWeb.Endpoint do
use Phoenix.Endpoint, otp_app: :mebe_web
# Serve at "/" the given assets from "priv/static" directory
plug Plug.Static,
at: "/", from: :mebe_web,
only: ~w(css images js favicon.ico robots.txt)
# Code reloading can be explicitly enabled under the
# :code_reloader configuration of your endpoint.
if code_reloading? do
plug Phoenix.LiveReloader
plug Phoenix.CodeReloader
end
plug Plug.Logger
plug Plug.Parsers,
parsers: [:urlencoded, :multipart, :json],
pass: ["*/*"],
json_decoder: Poison
plug Plug.MethodOverride
plug Plug.Head
plug Plug.Session,
store: :cookie,
key: "_mebe_web_key",
signing_salt: "WWhqeGIn",
encryption_salt: "z6/IRyg7"
plug :router, MebeWeb.Router
end

41
apps/mebe_web/mix.exs Normal file
View file

@ -0,0 +1,41 @@
defmodule MebeWeb.Mixfile do
use Mix.Project
def project do
[app: :mebe_web,
version: "0.0.1",
deps_path: "../../deps",
lockfile: "../../mix.lock",
elixir: "~> 1.0",
elixirc_paths: elixirc_paths(Mix.env),
compilers: [:phoenix] ++ Mix.compilers,
build_embedded: Mix.env == :prod,
start_permanent: Mix.env == :prod,
deps: deps]
end
# Configuration for the OTP application
#
# Type `mix help compile.app` for more information
def application do
[mod: {MebeWeb, []},
applications: [:phoenix, :cowboy, :logger]]
end
# Specifies which paths to compile per environment
defp elixirc_paths(:test), do: ["lib", "web", "test/support"]
defp elixirc_paths(_), do: ["lib", "web"]
# Specifies your project dependencies
#
# Type `mix help deps` for examples and options
defp deps do
[
{:phoenix, "~> 0.13"},
{:phoenix_live_reload, "~> 0.4"},
{:phoenix_html, "~> 1.0.1"},
{:cowboy, "~> 1.0"},
{:mebe_engine, in_umbrella: true}
]
end
end

View file

@ -0,0 +1,22 @@
{
"name": "mebe_web",
"version": "0.0.1",
"repository": {},
"devDependencies": {
"bower": "~1.4.1",
"chalk": "^1.0.0",
"del": "^1.1.1",
"end-of-stream": "~0.1.5",
"event-stream": "~3.3.1",
"gulp": "^3.8.11",
"gulp-concat": "~2.5.2",
"gulp-html-replace": "~1.4.5",
"gulp-minify-css": "~1.1.1",
"gulp-replace": "~0.5.3",
"gulp-sass": "~2.0.0",
"gulp-size": "~1.2.1",
"gulp-sourcemaps": "^1.5.2",
"gulp-uglify": "~1.2.0",
"vinyl-paths": "^1.0.0"
}
}

View file

@ -0,0 +1,8 @@
defmodule MebeWeb.PageControllerTest do
use MebeWeb.ConnCase
test "GET /" do
conn = get conn(), "/"
assert conn.resp_body =~ "Welcome to Phoenix!"
end
end

View file

@ -0,0 +1,43 @@
defmodule MebeWeb.ConnCase do
@moduledoc """
This module defines the test case to be used by
tests that require setting up a connection.
Such tests rely on `Phoenix.ConnTest` and also
imports other functionalities to make it easier
to build and query models.
Finally, if the test case interacts with the database,
it cannot be async. For this reason, every test runs
inside a transaction which is reset at the beginning
of the test unless the test case is marked as async.
"""
use ExUnit.CaseTemplate
using do
quote do
# Import conveniences for testing with connections
use Phoenix.ConnTest
# Alias the data repository and import query/model functions
alias MebeWeb.Repo
import Ecto.Model
import Ecto.Query, only: [from: 2]
# Import URL helpers from the router
import MebeWeb.Router.Helpers
# The default endpoint for testing
@endpoint MebeWeb.Endpoint
end
end
setup tags do
unless tags[:async] do
Ecto.Adapters.SQL.restart_test_transaction(MebeWeb.Repo, [])
end
:ok
end
end

View file

@ -0,0 +1,6 @@
ExUnit.start
# Create the database, run migrations, and start the test transaction.
Mix.Task.run "ecto.create", ["--quiet"]
Mix.Task.run "ecto.migrate", ["--quiet"]
Ecto.Adapters.SQL.begin_test_transaction(MebeWeb.Repo)

View file

@ -0,0 +1,80 @@
defmodule MebeWeb.PageController do
use MebeWeb.Web, :controller
alias MebeEngine.DB
plug :action
@posts_per_page Application.get_env(:blog_config, :posts_per_page)
def index(conn, params) do
{first, last} = calculate_ranges get_page params
case DB.get_reg_posts first, last do
[] ->
conn
|> put_status(:not_found)
|> render(MebeWeb.ErrorView, :"404")
posts ->
conn
|> assign(:posts, posts)
|> render("index.html")
end
end
def post(conn, %{"year" => year, "month" => month, "day" => day, "slug" => slug}) do
case {
Integer.parse(year),
Integer.parse(month),
Integer.parse(day)
} do
{{y_int, _}, {m_int, _}, {d_int, _}} ->
case DB.get_post y_int, m_int, d_int, slug do
nil ->
conn
|> put_status(:not_found)
|> render(MebeWeb.ErrorView, :"404")
post ->
conn
|> assign(:post, post)
|> render("post.html")
end
_ ->
conn
|> put_status(:not_found)
|> render(MebeWeb.ErrorView, :"404")
end
end
def tag(conn, params) do
"tag"
end
#defp posts_per_page(), do:
defp get_page(params) do
case params do
%{"page" => page} ->
{page_int, ""} = Integer.parse page
page_int
_ -> 1
end
end
defp calculate_ranges(page) do
IO.inspect @posts_per_page
{
(page - 1) * @posts_per_page,
@posts_per_page
}
end
end

View file

@ -0,0 +1,28 @@
defmodule MebeWeb.Router do
use Phoenix.Router
pipeline :browser do
plug :accepts, ["html"]
plug :fetch_session
plug :fetch_flash
plug :protect_from_forgery
end
scope "/", MebeWeb do
pipe_through :browser
get "/", PageController, :index
get "/p/:page", PageController, :index
get "/tag/:tag", PageController, :tag
get "/tag/:tag/p/:page", PageController, :tag
get "/archive/:year", PageController, :year
get "/archive/:year/p/:page", PageController, :year
get "/archive/:year/:month", PageController, :month
get "/archive/:year/:month/p/:page", PageController, :month
get "/:year/:month/:day/:slug", PageController, :post
get "/:slug", PageController, :page
end
end

View file

@ -0,0 +1,334 @@
// Darkly 3.3.4
// Bootswatch
// -----------------------------------------------------
@import url("https://fonts.googleapis.com/css?family=Lato:400,700,400italic");
// Navbar =====================================================================
.navbar {
border-width: 0;
&-default {
.badge {
background-color: #fff;
color: $navbar-default-bg;
}
}
&-inverse {
.badge {
background-color: #fff;
color: $navbar-inverse-bg;
}
}
&-brand {
line-height: 1;
}
&-form {
.form-control {
background-color: white;
&:focus {
border-color: white;
}
}
}
}
// Buttons ====================================================================
.btn {
border-width: 2px;
}
.btn:active {
@include box-shadow(none);
}
.btn-group.open .dropdown-toggle {
@include box-shadow(none);
}
// Typography =================================================================
.text-primary,
.text-primary:hover {
color: lighten($brand-primary, 10%);
}
.text-success,
.text-success:hover {
color: $brand-success;
}
.text-danger,
.text-danger:hover {
color: $brand-danger;
}
.text-warning,
.text-warning:hover {
color: $brand-warning;
}
.text-info,
.text-info:hover {
color: $brand-info;
}
// Tables =====================================================================
table,
.table {
a:not(.btn) {
text-decoration: underline;
}
.dropdown-menu a {
text-decoration: none;
}
.success,
.warning,
.danger,
.info {
color: #fff;
> th > a,
> td > a,
> a {
color: #fff;
}
}
> thead > tr > th,
> tbody > tr > th,
> tfoot > tr > th,
> thead > tr > td,
> tbody > tr > td,
> tfoot > tr > td {
border: none;
}
&-bordered > thead > tr > th,
&-bordered > tbody > tr > th,
&-bordered > tfoot > tr > th,
&-bordered > thead > tr > td,
&-bordered > tbody > tr > td,
&-bordered > tfoot > tr > td {
border: 1px solid $table-border-color;
}
}
// Forms ======================================================================
input,
textarea {
color: $input-color;
}
.form-control,
input,
textarea {
border: 2px hidden transparent;
@include box-shadow(none);
&:focus {
@include box-shadow(none);
}
}
.has-warning {
.help-block,
.control-label,
.radio,
.checkbox,
.radio-inline,
.checkbox-inline,
.form-control-feedback {
color: $brand-warning;
}
.form-control,
.form-control:focus {
@include box-shadow(none);
}
.input-group-addon {
border-color: $brand-warning;
}
}
.has-error {
.help-block,
.control-label,
.radio,
.checkbox,
.radio-inline,
.checkbox-inline,
.form-control-feedback {
color: $brand-danger;
}
.form-control,
.form-control:focus {
@include box-shadow(none);
}
.input-group-addon {
border-color: $brand-danger;
}
}
.has-success {
.help-block,
.control-label,
.radio,
.checkbox,
.radio-inline,
.checkbox-inline,
.form-control-feedback {
color: $brand-success;
}
.form-control,
.form-control:focus {
@include box-shadow(none);
}
.input-group-addon {
border-color: $brand-success;
}
}
.input-group-addon {
color: $text-color;
}
// Navs =======================================================================
.nav {
.open > a,
.open > a:hover,
.open > a:focus {
border-color: $nav-tabs-border-color;
}
}
.nav-tabs > li > a,
.nav-pills > li > a {
color: #fff;
}
.pager {
a,
a:hover {
color: #fff;
}
.disabled {
&>a,
&>a:hover,
&>a:focus,
&>span {
background-color: $pagination-disabled-bg;
}
}
}
.breadcrumb a {
color: #fff;
}
// Indicators =================================================================
.close {
text-decoration: none;
text-shadow: none;
opacity: 0.4;
&:hover,
&:focus {
opacity: 1;
}
}
.alert {
.alert-link {
color: #fff;
text-decoration: underline;
}
}
// Progress bars ==============================================================
.progress {
height: 10px;
@include box-shadow(none);
.progress-bar {
font-size: 10px;
line-height: 10px;
}
}
// Containers =================================================================
.well {
@include box-shadow(none);
}
a.list-group-item {
&.active,
&.active:hover,
&.active:focus {
border-color: $list-group-border;
}
&-success {
&.active {
background-color: $state-success-bg;
}
&.active:hover,
&.active:focus {
background-color: darken($state-success-bg, 5%);
}
}
&-warning {
&.active {
background-color: $state-warning-bg;
}
&.active:hover,
&.active:focus {
background-color: darken($state-warning-bg, 5%);
}
}
&-danger {
&.active {
background-color: $state-danger-bg;
}
&.active:hover,
&.active:focus {
background-color: darken($state-danger-bg, 5%);
}
}
}
.popover {
color: $text-color;
}
.panel-default > .panel-heading {
background-color: $panel-footer-bg;
}

View file

@ -0,0 +1,861 @@
// Darkly 3.3.4
// Variables
// --------------------------------------------------
//== Colors
//
//## Gray and brand colors for use across Bootstrap.
$gray-base: #000;
$gray-darker: lighten($gray-base, 13.5%); // #222
$gray-dark: #303030; // #333
$gray: #464545;
$gray-light: #999; // #999
$gray-lighter: #EBEBEB; // #eee
$brand-primary: #375a7f;
$brand-success: #00bc8c;
$brand-info: #3498DB;
$brand-warning: #F39C12;
$brand-danger: #E74C3C;
//== Scaffolding
//
//## Settings for some of the most global styles.
//** Background color for `<body>`.
$body-bg: $gray-darker;
//** Global text color on `<body>`.
$text-color: #fff;
//** Global textual link color.
$link-color: desaturate(lighten($brand-success, 10%),10%);
//** Link hover color set via `darken()` function.
$link-hover-color: $link-color;
//** Link hover decoration.
$link-hover-decoration: underline;
//== Typography
//
//## Font, line-height, and color for body text, headings, and more.
$font-family-sans-serif: "Lato", "Helvetica Neue", Helvetica, Arial, sans-serif;
$font-family-serif: Georgia, "Times New Roman", Times, serif;
//** Default monospace fonts for `<code>`, `<kbd>`, and `<pre>`.
$font-family-monospace: Menlo, Monaco, Consolas, "Courier New", monospace;
$font-family-base: $font-family-sans-serif;
$font-size-base: 15px;
$font-size-large: ceil(($font-size-base * 1.25)); // ~18px
$font-size-small: ceil(($font-size-base * 0.85)); // ~12px
$font-size-h1: floor(($font-size-base * 2.6)); // ~36px
$font-size-h2: floor(($font-size-base * 2.15)); // ~30px
$font-size-h3: ceil(($font-size-base * 1.7)); // ~24px
$font-size-h4: ceil(($font-size-base * 1.25)); // ~18px
$font-size-h5: $font-size-base;
$font-size-h6: ceil(($font-size-base * 0.85)); // ~12px
//** Unit-less `line-height` for use in components like buttons.
$line-height-base: 1.428571429; // 20/14
//** Computed "line-height" (`font-size` * `line-height`) for use with `margin`, `padding`, etc.
$line-height-computed: floor(($font-size-base * $line-height-base)); // ~20px
//** By default, this inherits from the `<body>`.
$headings-font-family: $font-family-base;
$headings-font-weight: 400;
$headings-line-height: 1.1;
$headings-color: inherit;
//== Iconography
//
//## Specify custom location and filename of the included Glyphicons icon font. Useful for those including Bootstrap via Bower.
//** Load fonts from this directory.
$icon-font-path: "../fonts/";
//** File name for all font files.
$icon-font-name: "glyphicons-halflings-regular";
//** Element ID within SVG icon file.
$icon-font-svg-id: "glyphicons_halflingsregular";
//== Components
//
//## Define common padding and border radius sizes and more. Values based on 14px text and 1.428 line-height (~20px to start).
$padding-base-vertical: 10px;
$padding-base-horizontal: 15px;
$padding-large-vertical: 18px;
$padding-large-horizontal: 27px;
$padding-small-vertical: 6px;
$padding-small-horizontal: 9px;
$padding-xs-vertical: 1px;
$padding-xs-horizontal: 5px;
$line-height-large: 1.3333333; // extra decimals for Win 8.1 Chrome
$line-height-small: 1.5;
$border-radius-base: 4px;
$border-radius-large: 6px;
$border-radius-small: 3px;
//** Global color for active items (e.g., navs or dropdowns).
$component-active-color: #fff;
//** Global background color for active items (e.g., navs or dropdowns).
$component-active-bg: $brand-primary;
//** Width of the `border` for generating carets that indicator dropdowns.
$caret-width-base: 4px;
//** Carets increase slightly in size for larger components.
$caret-width-large: 5px;
//== Tables
//
//## Customizes the `.table` component with basic values, each used across all table variations.
//** Padding for `<th>`s and `<td>`s.
$table-cell-padding: 8px;
//** Padding for cells in `.table-condensed`.
$table-condensed-cell-padding: 5px;
//** Default background color used for all tables.
$table-bg: transparent;
//** Background color used for `.table-striped`.
$table-bg-accent: lighten($gray-dark, 5%);
//** Background color used for `.table-hover`.
$table-bg-hover: $gray;
$table-bg-active: $table-bg-hover;
//** Border color for table and cell borders.
$table-border-color: $gray;
//== Buttons
//
//## For each of Bootstrap's buttons, define text, background and border color.
$btn-font-weight: normal;
$btn-default-color: $text-color;
$btn-default-bg: $gray;
$btn-default-border: $btn-default-bg;
$btn-primary-color: #fff;
$btn-primary-bg: $brand-primary;
$btn-primary-border: $btn-primary-bg;
$btn-success-color: $btn-primary-color;
$btn-success-bg: $brand-success;
$btn-success-border: $btn-success-bg;
$btn-info-color: $btn-success-color;
$btn-info-bg: $brand-info;
$btn-info-border: $btn-info-bg;
$btn-warning-color: $btn-success-color;
$btn-warning-bg: $brand-warning;
$btn-warning-border: $btn-warning-bg;
$btn-danger-color: $btn-success-color;
$btn-danger-bg: $brand-danger;
$btn-danger-border: $btn-danger-bg;
$btn-link-disabled-color: $gray-light;
//== Forms
//
//##
//** `<input>` background color
$input-bg: #fff;
//** `<input disabled>` background color
$input-bg-disabled: $gray-lighter;
//** Text color for `<input>`s
$input-color: $gray;
//** `<input>` border color
$input-border: #f1f1f1;
// TODO: Rename `$input-border-radius` to `$input-border-radius-base` in v4
//** Default `.form-control` border radius
// This has no effect on `<select>`s in some browsers, due to the limited stylability of `<select>`s in CSS.
$input-border-radius: $border-radius-base;
//** Large `.form-control` border radius
$input-border-radius-large: $border-radius-large;
//** Small `.form-control` border radius
$input-border-radius-small: $border-radius-small;
//** Border color for inputs on focus
$input-border-focus: #fff;
//** Placeholder text color
$input-color-placeholder: $gray-light;
//** Default `.form-control` height
$input-height-base: ($line-height-computed + ($padding-base-vertical * 2) + 4);
//** Large `.form-control` height
$input-height-large: (ceil($font-size-large * $line-height-large) + ($padding-large-vertical * 2) + 4);
//** Small `.form-control` height
$input-height-small: (floor($font-size-small * $line-height-small) + ($padding-small-vertical * 2) + 4);
//** `.form-group` margin
$form-group-margin-bottom: 15px;
$legend-color: $text-color;
$legend-border-color: transparent;
//** Background color for textual input addons
$input-group-addon-bg: $gray;
//** Border color for textual input addons
$input-group-addon-border-color: transparent;
//** Disabled cursor for form controls and buttons.
$cursor-disabled: not-allowed;
//== Dropdowns
//
//## Dropdown menu container and contents.
//** Background for the dropdown menu.
$dropdown-bg: $gray-dark;
//** Dropdown menu `border-color`.
$dropdown-border: rgba(0,0,0,.15);
//** Dropdown menu `border-color` **for IE8**.
$dropdown-fallback-border: #ccc;
//** Divider color for between dropdown items.
$dropdown-divider-bg: $gray;
//** Dropdown link text color.
$dropdown-link-color: $gray-lighter;
//** Hover color for dropdown links.
$dropdown-link-hover-color: #fff;
//** Hover background for dropdown links.
$dropdown-link-hover-bg: $component-active-bg;
//** Active dropdown menu item text color.
$dropdown-link-active-color: #fff;
//** Active dropdown menu item background color.
$dropdown-link-active-bg: $component-active-bg;
//** Disabled dropdown menu item background color.
$dropdown-link-disabled-color: $gray-light;
//** Text color for headers within dropdown menus.
$dropdown-header-color: $gray-light;
//** Deprecated `$dropdown-caret-color` as of v3.1.0
$dropdown-caret-color: #000;
//-- Z-index master list
//
// Warning: Avoid customizing these values. They're used for a bird's eye view
// of components dependent on the z-axis and are designed to all work together.
//
// Note: These variables are not generated into the Customizer.
$zindex-navbar: 1000;
$zindex-dropdown: 1000;
$zindex-popover: 1060;
$zindex-tooltip: 1070;
$zindex-navbar-fixed: 1030;
$zindex-modal-background: 1040;
$zindex-modal: 1050;
//== Media queries breakpoints
//
//## Define the breakpoints at which your layout will change, adapting to different screen sizes.
// Extra small screen / phone
//** Deprecated `$screen-xs` as of v3.0.1
$screen-xs: 480px;
//** Deprecated `$screen-xs-min` as of v3.2.0
$screen-xs-min: $screen-xs;
//** Deprecated `$screen-phone` as of v3.0.1
$screen-phone: $screen-xs-min;
// Small screen / tablet
//** Deprecated `$screen-sm` as of v3.0.1
$screen-sm: 768px;
$screen-sm-min: $screen-sm;
//** Deprecated `$screen-tablet` as of v3.0.1
$screen-tablet: $screen-sm-min;
// Medium screen / desktop
//** Deprecated `$screen-md` as of v3.0.1
$screen-md: 992px;
$screen-md-min: $screen-md;
//** Deprecated `$screen-desktop` as of v3.0.1
$screen-desktop: $screen-md-min;
// Large screen / wide desktop
//** Deprecated `$screen-lg` as of v3.0.1
$screen-lg: 1200px;
$screen-lg-min: $screen-lg;
//** Deprecated `$screen-lg-desktop` as of v3.0.1
$screen-lg-desktop: $screen-lg-min;
// So media queries don't overlap when required, provide a maximum
$screen-xs-max: ($screen-sm-min - 1);
$screen-sm-max: ($screen-md-min - 1);
$screen-md-max: ($screen-lg-min - 1);
//== Grid system
//
//## Define your custom responsive grid.
//** Number of columns in the grid.
$grid-columns: 12;
//** Padding between columns. Gets divided in half for the left and right.
$grid-gutter-width: 30px;
// Navbar collapse
//** Point at which the navbar becomes uncollapsed.
$grid-float-breakpoint: $screen-sm-min;
//** Point at which the navbar begins collapsing.
$grid-float-breakpoint-max: ($grid-float-breakpoint - 1);
//== Container sizes
//
//## Define the maximum width of `.container` for different screen sizes.
// Small screen / tablet
$container-tablet: (720px + $grid-gutter-width);
//** For `$screen-sm-min` and up.
$container-sm: $container-tablet;
// Medium screen / desktop
$container-desktop: (940px + $grid-gutter-width);
//** For `$screen-md-min` and up.
$container-md: $container-desktop;
// Large screen / wide desktop
$container-large-desktop: (1140px + $grid-gutter-width);
//** For `$screen-lg-min` and up.
$container-lg: $container-large-desktop;
//== Navbar
//
//##
// Basics of a navbar
$navbar-height: 60px;
$navbar-margin-bottom: $line-height-computed;
$navbar-border-radius: $border-radius-base;
$navbar-padding-horizontal: floor(($grid-gutter-width / 2));
$navbar-padding-vertical: (($navbar-height - $line-height-computed) / 2);
$navbar-collapse-max-height: 340px;
$navbar-default-color: #777;
$navbar-default-bg: $brand-primary;
$navbar-default-border: transparent;
// Navbar links
$navbar-default-link-color: #fff;
$navbar-default-link-hover-color: $brand-success;
$navbar-default-link-hover-bg: transparent;
$navbar-default-link-active-color: #fff;
$navbar-default-link-active-bg: darken($navbar-default-bg, 10%);
$navbar-default-link-disabled-color: #ccc;
$navbar-default-link-disabled-bg: transparent;
// Navbar brand label
$navbar-default-brand-color: $navbar-default-link-color;
$navbar-default-brand-hover-color: $navbar-default-link-hover-color;
$navbar-default-brand-hover-bg: transparent;
// Navbar toggle
$navbar-default-toggle-hover-bg: darken($navbar-default-bg, 10%);
$navbar-default-toggle-icon-bar-bg: #fff;
$navbar-default-toggle-border-color: darken($navbar-default-bg, 10%);
// Inverted navbar
// Reset inverted navbar basics
$navbar-inverse-color: #fff;
$navbar-inverse-bg: $brand-success;
$navbar-inverse-border: transparent;
// Inverted navbar links
$navbar-inverse-link-color: $navbar-inverse-color;
$navbar-inverse-link-hover-color: $brand-primary;
$navbar-inverse-link-hover-bg: transparent;
$navbar-inverse-link-active-color: $navbar-inverse-color;
$navbar-inverse-link-active-bg: darken($navbar-inverse-bg, 5%);
$navbar-inverse-link-disabled-color: #aaa;
$navbar-inverse-link-disabled-bg: transparent;
// Inverted navbar brand label
$navbar-inverse-brand-color: $navbar-inverse-link-color;
$navbar-inverse-brand-hover-color: $navbar-inverse-link-hover-color;
$navbar-inverse-brand-hover-bg: transparent;
// Inverted navbar toggle
$navbar-inverse-toggle-hover-bg: darken($navbar-inverse-bg, 10%);
$navbar-inverse-toggle-icon-bar-bg: #fff;
$navbar-inverse-toggle-border-color: darken($navbar-inverse-bg, 10%);
//== Navs
//
//##
//=== Shared nav styles
$nav-link-padding: 10px 15px;
$nav-link-hover-bg: $gray-dark;
$nav-disabled-link-color: lighten($gray, 10%);
$nav-disabled-link-hover-color: lighten($gray, 10%);
//== Tabs
$nav-tabs-border-color: $gray;
$nav-tabs-link-hover-border-color: $gray;
$nav-tabs-active-link-hover-bg: $body-bg;
$nav-tabs-active-link-hover-color: $brand-success;
$nav-tabs-active-link-hover-border-color: $nav-tabs-link-hover-border-color;
$nav-tabs-justified-link-border-color: $gray-lighter;
$nav-tabs-justified-active-link-border-color: $body-bg;
//== Pills
$nav-pills-border-radius: $border-radius-base;
$nav-pills-active-link-hover-bg: $component-active-bg;
$nav-pills-active-link-hover-color: $component-active-color;
//== Pagination
//
//##
$pagination-color: #fff;
$pagination-bg: $brand-success;
$pagination-border: transparent;
$pagination-hover-color: #fff;
$pagination-hover-bg: lighten($brand-success, 6%);
$pagination-hover-border: transparent;
$pagination-active-color: #fff;
$pagination-active-bg: lighten($brand-success, 6%);
$pagination-active-border: transparent;
$pagination-disabled-color: #fff;
$pagination-disabled-bg: darken($brand-success, 15%);
$pagination-disabled-border: transparent;
//== Pager
//
//##
$pager-bg: $pagination-bg;
$pager-border: $pagination-border;
$pager-border-radius: 15px;
$pager-hover-bg: $pagination-hover-bg;
$pager-active-bg: $pagination-active-bg;
$pager-active-color: $pagination-active-color;
$pager-disabled-color: #ddd;
//== Jumbotron
//
//##
$jumbotron-padding: 30px;
$jumbotron-color: inherit;
$jumbotron-bg: $gray-dark;
$jumbotron-heading-color: inherit;
$jumbotron-font-size: ceil(($font-size-base * 1.5));
//== Form states and alerts
//
//## Define colors for form feedback states and, by default, alerts.
$state-success-text: #fff;
$state-success-bg: $brand-success;
$state-success-border: $brand-success;
$state-info-text: #fff;
$state-info-bg: $brand-info;
$state-info-border: $brand-info;
$state-warning-text: #fff;
$state-warning-bg: $brand-warning;
$state-warning-border: $brand-warning;
$state-danger-text: #fff;
$state-danger-bg: $brand-danger;
$state-danger-border: $brand-danger;
//== Tooltips
//
//##
//** Tooltip max width
$tooltip-max-width: 200px;
//** Tooltip text color
$tooltip-color: #fff;
//** Tooltip background color
$tooltip-bg: #000;
$tooltip-opacity: .9;
//** Tooltip arrow width
$tooltip-arrow-width: 5px;
//** Tooltip arrow color
$tooltip-arrow-color: $tooltip-bg;
//== Popovers
//
//##
//** Popover body background color
$popover-bg: $gray-dark;
//** Popover maximum width
$popover-max-width: 276px;
//** Popover border color
$popover-border-color: rgba(0,0,0,.2);
//** Popover fallback border color
$popover-fallback-border-color: #999;
//** Popover title background color
$popover-title-bg: darken($popover-bg, 3%);
//** Popover arrow width
$popover-arrow-width: 10px;
//** Popover arrow color
$popover-arrow-color: $popover-bg;
//** Popover outer arrow width
$popover-arrow-outer-width: ($popover-arrow-width + 1);
//** Popover outer arrow color
$popover-arrow-outer-color: fadein($popover-border-color, 5%);
//** Popover outer arrow fallback color
$popover-arrow-outer-fallback-color: darken($popover-fallback-border-color, 20%);
//== Labels
//
//##
//** Default label background color
$label-default-bg: $gray;
//** Primary label background color
$label-primary-bg: $brand-primary;
//** Success label background color
$label-success-bg: $brand-success;
//** Info label background color
$label-info-bg: $brand-info;
//** Warning label background color
$label-warning-bg: $brand-warning;
//** Danger label background color
$label-danger-bg: $brand-danger;
//** Default label text color
$label-color: #fff;
//** Default text color of a linked label
$label-link-hover-color: #fff;
//== Modals
//
//##
//** Padding applied to the modal body
$modal-inner-padding: 20px;
//** Padding applied to the modal title
$modal-title-padding: 15px;
//** Modal title line-height
$modal-title-line-height: $line-height-base;
//** Background color of modal content area
$modal-content-bg: $gray-dark;
//** Modal content border color
$modal-content-border-color: rgba(0,0,0,.2);
//** Modal content border color **for IE8**
$modal-content-fallback-border-color: #999;
//** Modal backdrop background color
$modal-backdrop-bg: #000;
//** Modal backdrop opacity
$modal-backdrop-opacity: .7;
//** Modal header border color
$modal-header-border-color: $gray;
//** Modal footer border color
$modal-footer-border-color: $modal-header-border-color;
$modal-lg: 900px;
$modal-md: 600px;
$modal-sm: 300px;
//== Alerts
//
//## Define alert colors, border radius, and padding.
$alert-padding: 15px;
$alert-border-radius: $border-radius-base;
$alert-link-font-weight: bold;
$alert-success-bg: $state-success-bg;
$alert-success-text: $state-success-text;
$alert-success-border: $state-success-border;
$alert-info-bg: $state-info-bg;
$alert-info-text: $state-info-text;
$alert-info-border: $state-info-border;
$alert-warning-bg: $state-warning-bg;
$alert-warning-text: $state-warning-text;
$alert-warning-border: $state-warning-border;
$alert-danger-bg: $state-danger-bg;
$alert-danger-text: $state-danger-text;
$alert-danger-border: $state-danger-border;
//== Progress bars
//
//##
//** Background color of the whole progress component
$progress-bg: $gray-lighter;
//** Progress bar text color
$progress-bar-color: #fff;
//** Variable for setting rounded corners on progress bar.
$progress-border-radius: $border-radius-base;
//** Default progress bar color
$progress-bar-bg: $brand-primary;
//** Success progress bar color
$progress-bar-success-bg: $brand-success;
//** Warning progress bar color
$progress-bar-warning-bg: $brand-warning;
//** Danger progress bar color
$progress-bar-danger-bg: $brand-danger;
//** Info progress bar color
$progress-bar-info-bg: $brand-info;
//== List group
//
//##
//** Background color on `.list-group-item`
$list-group-bg: $gray-dark;
//** `.list-group-item` border color
$list-group-border: $gray;
//** List group border radius
$list-group-border-radius: $border-radius-base;
//** Background color of single list items on hover
$list-group-hover-bg: transparent;
//** Text color of active list items
$list-group-active-color: $component-active-color;
//** Background color of active list items
$list-group-active-bg: $component-active-bg;
//** Border color of active list elements
$list-group-active-border: $list-group-active-bg;
//** Text color for content within active list items
$list-group-active-text-color: lighten($list-group-active-bg, 40%);
//** Text color of disabled list items
$list-group-disabled-color: $gray-light;
//** Background color of disabled list items
$list-group-disabled-bg: $gray-lighter;
//** Text color for content within disabled list items
$list-group-disabled-text-color: $list-group-disabled-color;
$list-group-link-color: $link-color;
$list-group-link-hover-color: $list-group-link-color;
$list-group-link-heading-color: darken($link-color, 5%);
//== Panels
//
//##
$panel-bg: $gray-dark;
$panel-body-padding: 15px;
$panel-heading-padding: 10px 15px;
$panel-footer-padding: $panel-heading-padding;
$panel-border-radius: $border-radius-base;
//** Border color for elements within panels
$panel-inner-border: $gray;
$panel-footer-bg: $gray;
$panel-default-text: $text-color;
$panel-default-border: $gray;
$panel-default-heading-bg: $gray-dark;
$panel-primary-text: #fff;
$panel-primary-border: $brand-primary;
$panel-primary-heading-bg: $brand-primary;
$panel-success-text: $state-success-text;
$panel-success-border: $state-success-border;
$panel-success-heading-bg: $state-success-bg;
$panel-info-text: $state-info-text;
$panel-info-border: $state-info-border;
$panel-info-heading-bg: $state-info-bg;
$panel-warning-text: $state-warning-text;
$panel-warning-border: $state-warning-border;
$panel-warning-heading-bg: $state-warning-bg;
$panel-danger-text: $state-danger-text;
$panel-danger-border: $state-danger-border;
$panel-danger-heading-bg: $state-danger-bg;
//== Thumbnails
//
//##
//** Padding around the thumbnail image
$thumbnail-padding: 2px;
//** Thumbnail background color
$thumbnail-bg: $body-bg;
//** Thumbnail border color
$thumbnail-border: $gray;
//** Thumbnail border radius
$thumbnail-border-radius: $border-radius-base;
//** Custom text color for thumbnail captions
$thumbnail-caption-color: $text-color;
//** Padding around the thumbnail caption
$thumbnail-caption-padding: 9px;
//== Wells
//
//##
$well-bg: $gray-dark;
$well-border: transparent;
//== Badges
//
//##
$badge-color: #fff;
//** Linked badge text color on hover
$badge-link-hover-color: #fff;
$badge-bg: $gray;
//** Badge text color in active nav link
$badge-active-color: $brand-primary;
//** Badge background color in active nav link
$badge-active-bg: #fff;
$badge-font-weight: bold;
$badge-line-height: 1;
$badge-border-radius: 10px;
//== Breadcrumbs
//
//##
$breadcrumb-padding-vertical: 8px;
$breadcrumb-padding-horizontal: 15px;
//** Breadcrumb background color
$breadcrumb-bg: $gray;
//** Breadcrumb text color
$breadcrumb-color: $text-color;
//** Text color of current page in the breadcrumb
$breadcrumb-active-color: $gray-light;
//** Textual separator for between breadcrumb elements
$breadcrumb-separator: "/";
//== Carousel
//
//##
$carousel-text-shadow: 0 1px 2px rgba(0,0,0,.6);
$carousel-control-color: #fff;
$carousel-control-width: 15%;
$carousel-control-opacity: .5;
$carousel-control-font-size: 20px;
$carousel-indicator-active-bg: #fff;
$carousel-indicator-border-color: #fff;
$carousel-caption-color: #fff;
//== Close
//
//##
$close-font-weight: bold;
$close-color: #fff;
$close-text-shadow: none;
//== Code
//
//##
$code-color: #c7254e;
$code-bg: #f9f2f4;
$kbd-color: #fff;
$kbd-bg: #333;
$pre-bg: $gray-lighter;
$pre-color: $gray-dark;
$pre-border-color: #ccc;
$pre-scrollable-max-height: 340px;
//== Type
//
//##
//** Horizontal offset for forms and lists.
$component-offset-horizontal: 180px;
//** Text muted color
$text-muted: $gray-light;
//** Abbreviations and acronyms border color
$abbr-border-color: $gray-light;
//** Headings small color
$headings-small-color: $gray-light;
//** Blockquote small color
$blockquote-small-color: $gray-light;
//** Blockquote font size
$blockquote-font-size: ($font-size-base * 1.25);
//** Blockquote border color
$blockquote-border-color: $gray;
//** Page header border color
$page-header-border-color: transparent;
//** Width of horizontal description list titles
$dl-horizontal-offset: $component-offset-horizontal;
//** Horizontal line color.
$hr-border: $gray;

View file

@ -0,0 +1,97 @@
@import "../../../bower_components/bootstrap-sass/assets/stylesheets/bootstrap/variables";
@import "variables";
@import "../../../bower_components/bootstrap-sass/assets/stylesheets/bootstrap";
@import "bootswatch";
/* Scale images with the width of the site */
.postimage, p>img, figure>img {
max-width: 100%;
margin: 10px 0;
}
/* Style images and their captions */
figure>img {
margin: 0;
}
figcaption {
margin: 10px;
font-family: 'Times New Roman', serif;
font-size: 1.1em;
font-style: italic;
}
/* YouTube videos */
iframe.youtube {
width: 100%;
max-width: 560px;
min-height: 315px;
}
/* Table styles */
table {
margin: 10px;
}
td, th {
padding: 10px;
border: 1px solid #eee;
}
/* Fix heading */
.navbar-default {
width: 100%;
border-radius: 0;
}
.footer {
margin: 30px;
margin-bottom: 20px;
padding-top: 20px;
border-top: 1px solid #444;
div {
text-align: center;
}
}
/* Post styles */
.post-header {
margin-bottom: 20px;
h1 {
margin-top: 30px;
}
.post-meta {
font-style: italic;
}
}
.container .row:first-child .post-header h1 {
margin-top: 0;
}
ul.post-tags {
list-style-type: none;
margin: 0;
padding: 0;
display: inline-block;
li {
display: inline-block;
padding: 3px;
background-color: #123456;
border-radius: 3px;
a {
color: #abcdef;
}
}
}

View file

@ -0,0 +1 @@

View file

@ -0,0 +1,55 @@
/*
* This script will check old Laine-style hashes when the page has been loaded and redirects
* to the correct address if such a hash is found.
* This is done in JS because the hash is not sent to the server.
*/
(function () {
var RE = [
[
/^\#\!\/(\d{4})\/(\d\d)\/(\d\d)\/(.*)$/,
'/$1/$2/$3/$4'
],
[
/^\#\![^\/]+$/,
'/$1'
],
[
/^\#\!\/tag\/([^\/]+)$/,
'/tag/$1'
],
[
/^\#\!\/tag\/([^\/]+)(\/\d+)$/,
'/tag/$1/p/$2'
],
[
/^\#\!\/archives\/(\d{4})$/,
'/archive/$1'
],
[
/^\#\!\/archives\/(\d{4})(\/\d+)$/,
'/archive/$1/p/$2'
],
[
/^\#\!\/archives\/(\d{4})\/(\d\d)$/,
'/archive/$1/$2'
],
[
/^\#\!\/archives\/(\d{4})\/(\d\d)(\/\d+)$/,
'/archive/$1/$2/p/$3'
]
];
var currentHash = window.location.hash;
if (currentHash) {
for (var i = 0; i < RE.length; ++i) {
var results = RE[i].exec(currentHash);
if (results !== null) {
window.location.replace(currentHash.replace(RE[i][0], RE[i][1]));
break;
}
}
}
}());

View file

@ -0,0 +1,11 @@
<div class="jumbotron">
<h1>HTTP/1.1 404 Not Found</h1>
<p>
You tried to find something that doesnt exist. Maybe it did at some point, but it sure doesnt anymore.
</p>
<p>
<a href="<%= page_path @conn, :index %>">Go away</a>.
</p>
</div>

View file

@ -0,0 +1,51 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="">
<meta name="author" content="">
<title>Hello Phoenix!</title>
<link rel="stylesheet" href="<%= static_path(@conn, "/css/app.css") %>">
</head>
<body>
<div class="navbar navbar-default">
<div class="container-fluid">
<div class="navbar-header">
<a href="<%= page_path @conn, :index %>" class="navbar-brand">
Random notes
</a>
</div>
<ul class="nav navbar-nav">
<li><a href="<%= page_path @conn, :page, "about" %>">About</a></li>
</ul>
</div>
</div>
<div class="container">
<%= @inner %>
<div class="footer row">
<div class="col-xs-12 col-sm-6">
<p>
© Mikko Ahlroth
</p>
</div>
<div class="col-xs-12 col-sm-6">
<p>
<a href="<%= page_path @conn, :page, "mebe" %>">
Powered by Elixir + Phoenix + Mebe
</a>
</p>
</div>
</div>
</div> <!-- /container -->
<script src="<%= static_path(@conn, "/js/app.js") %>"></script>
<script src="<%= static_path(@conn, "/js/old_hash_redirector.js") %>"></script>
</body>
</html>

View file

@ -0,0 +1,7 @@
<%= for post <- @posts do %>
<div class="row">
<div class="col-xs-10 col-xs-push-1">
<%= render "post_elem.html", conn: @conn, post: post, short: true %>
</div>
</div>
<% end %>

View file

@ -0,0 +1 @@
<%= render "post_elem.html", conn: @conn, post: @post, short: false %>

View file

@ -0,0 +1,52 @@
<% {year, month, day} = @post.date %>
<%
# Justified versions of the month and day
{j_month, j_day} = {
String.rjust(Integer.to_string(month), 2, ?0),
String.rjust(Integer.to_string(day), 2, ?0)
}
%>
<div class="post">
<div class="post-header">
<%= if @short do %>
<a href="<%= page_path @conn, :post, year, j_month, j_day, @post.slug %>">
<h1><%= @post.title %></h1>
</a>
<%= else %>
<h1><%= @post.title %></h1>
<% end %>
<div class="post-meta">
<span>
Posted on <%= year %>-<%= j_month %>-<%= j_day %>.
</span>
<%= if @post.tags do %>
<ul class="post-tags">
<%= for tag <- @post.tags do %>
<li>
<a href="<%= page_path @conn, :tag, tag %>">
<%= tag %>
</a>
</li>
<% end %>
</ul>
<% end %>
</div>
</div>
<%= if @short do %>
<div class="post-content post-short-content">
<%= raw @post.short_content %>
</div>
<%= else %>
<div class="post-content">
<%= raw @post.content %>
</div>
<% end %>
</div>

View file

@ -0,0 +1,17 @@
defmodule MebeWeb.ErrorView do
use MebeWeb.Web, :view
def render("404.html", %{conn: conn}) do
render "not_found.html", %{conn: conn}
end
def render("500.html", _assigns) do
"HTTP/1.1 500 Internal Server Error"
end
# In case no render clause matches or no
# template is found, let's render it as 500
def template_not_found(_template, assigns) do
render "500.html", assigns
end
end

View file

@ -0,0 +1,3 @@
defmodule MebeWeb.LayoutView do
use MebeWeb.Web, :view
end

View file

@ -0,0 +1,3 @@
defmodule MebeWeb.PageView do
use MebeWeb.Web, :view
end

48
apps/mebe_web/web/web.ex Normal file
View file

@ -0,0 +1,48 @@
defmodule MebeWeb.Web do
@moduledoc """
A module that keeps using definitions for controllers,
views and so on.
This can be used in your application as:
use MebeWeb.Web, :controller
use MebeWeb.Web, :view
The definitions below will be executed for every view,
controller, etc, so keep them short and clean, focused
on imports, uses and aliases.
Do NOT define functions inside the quoted expressions
below.
"""
def controller do
quote do
use Phoenix.Controller
# Import URL helpers from the router
import MebeWeb.Router.Helpers
end
end
def view do
quote do
use Phoenix.View, root: "web/templates"
# Import convenience functions from controllers
import Phoenix.Controller, only: [get_flash: 2]
# Import URL helpers from the router
import MebeWeb.Router.Helpers
# Use all HTML functionality (forms, tags, etc)
use Phoenix.HTML
end
end
@doc """
When used, dispatch to the appropriate controller/view/etc.
"""
defmacro __using__(which) when is_atom(which) do
apply(__MODULE__, which, [])
end
end

26
mix.exs Normal file
View file

@ -0,0 +1,26 @@
defmodule Mebe.Mixfile do
use Mix.Project
def project do
[apps_path: "apps",
build_embedded: Mix.env == :prod,
start_permanent: Mix.env == :prod,
deps: deps]
end
# Dependencies can be Hex packages:
#
# {:mydep, "~> 0.3.0"}
#
# Or git/path repositories:
#
# {:mydep, git: "https://github.com/elixir-lang/mydep.git", tag: "0.1.0"}
#
# Type `mix help deps` for more examples and options.
#
# Dependencies listed here are available only for this project
# and cannot be accessed from applications inside the apps folder
defp deps do
[]
end
end

10
mix.lock Normal file
View file

@ -0,0 +1,10 @@
%{"cowboy": {:hex, :cowboy, "1.0.0"},
"cowlib": {:hex, :cowlib, "1.0.1"},
"earmark": {:hex, :earmark, "0.1.16"},
"fs": {:hex, :fs, "0.9.2"},
"phoenix": {:hex, :phoenix, "0.13.0"},
"phoenix_html": {:hex, :phoenix_html, "1.0.1"},
"phoenix_live_reload": {:hex, :phoenix_live_reload, "0.4.0"},
"plug": {:hex, :plug, "0.12.2"},
"poison": {:hex, :poison, "1.4.0"},
"ranch": {:hex, :ranch, "1.0.0"}}