diff --git a/.gitignore b/.gitignore index 5ed8334..e0e53c8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ +*.css* .bundle .env .sass-cache pkg -public/stylesheets/*.css* + diff --git a/Gemfile b/Gemfile index 6b179a7..4b734d1 100644 --- a/Gemfile +++ b/Gemfile @@ -9,6 +9,7 @@ gem 'ox' gem 'puma' gem 'rack-cors' gem 'rake' +gem 'redcarpet' gem 'rufus-scheduler' gem 'sass' gem 'sequel_pg' diff --git a/Gemfile.lock b/Gemfile.lock index ef5ccd2..ef65d2c 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -44,6 +44,7 @@ GEM rb-fsevent (0.10.3) rb-inotify (0.9.10) ffi (>= 0.5.0, < 2) + redcarpet (3.4.0) rubocop (0.57.0) jaro_winkler (~> 1.4.0) parallel (~> 1.10) @@ -96,6 +97,7 @@ DEPENDENCIES rack-cors rack-test rake + redcarpet rubocop rufus-scheduler sass @@ -109,4 +111,4 @@ RUBY VERSION ruby 2.5.1p57 BUNDLED WITH - 1.16.2 + 1.16.4 diff --git a/README.md b/README.md index 1137ca7..a5ada80 100644 --- a/README.md +++ b/README.md @@ -2,63 +2,70 @@ [![Travis](https://travis-ci.org/hakanensari/frankfurter.svg)](https://travis-ci.org/hakanensari/frankfurter) -Frankfurter is a free API for current and historical foreign exchange rates [published by the European Central Bank](https://www.ecb.europa.eu/stats/policy_and_exchange_rates/euro_reference_exchange_rates/html/index.en.html). +Foreign exchange (forex) rates and currency conversion API + +## Getting Started + +Frankfurter is a free, open source API for current and historical foreign exchange rates. It tracks data published by the European Central Bank. Rates are updated around 4PM CET every working day. -## Usage +## Examples Get the current foreign exchange rates. ```http -GET /current +GET /latest HTTP/1.1 ``` Get historical rates for any day since 1999. ```http -GET /2000-01-03 +GET /2000-01-03 HTTP/1.1 +``` + +Get historical rates for a time period. + +```http +GET /2010-01-01..2010-01-31 HTTP/1.1 ``` Rates quote against the Euro by default. Quote against a different currency. ```http -GET /current?from=USD +GET /latest?from=USD HTTP/1.1 ``` Request specific exchange rates. ```http -GET /current?to=GBP +GET /latest?to=USD,GBP HTTP/1.1 ``` -Change the converted amount. +Convert a specific amount. ```http -GET /current?amount=100 +GET /latest?amount=1000&from=GBP&to=USD HTTP/1.1 ``` -Finally, use all the above together. +With a full list of currencies, time series grow large in size. For better performance, use the to parameter to reduce the response weight. ```http -GET /current?from=EUR&to=GBP&amount=100 +GET /2016-01-01..2016-12-31?from=GBP&to=USD HTTP/1.1 ``` -The primary use case is client side. For instance, with [money.js](https://openexchangerates.github.io/money.js/) in the browser +Here we return the current GBP/USD currency pair with JavaScript. ```js -let demo = () => { - let rate = fx(1).from("GBP").to("USD") - alert("£1 = $" + rate.toFixed(4)) -} - -fetch('https://yourdomain.com/current') - .then((resp) => resp.json()) - .then((data) => fx.rates = data.rates) - .then(demo) +// Fetch and display GBP/USD +fetch('/latest?from=GBP&to=USD') + .then(resp => resp.json()) + .then((data) => { alert(`GBPUSD = ${data.rates.USD}`); }); ``` -## Installation +Cache data whenever possible. + +## Deployment To build locally, type @@ -66,27 +73,19 @@ To build locally, type docker-compose up -d ``` -Now you can access the API at +Now you can access the API at `http://localhost:8080`. -``` -http://localhost:8080 -``` - -In production, first create a `.env` file based on [`.env.example`](.env.example). Then, run with +In production, create a [`.env`](.env.example) file and run with ```bash docker-compose -f docker-compose.yml -f docker-compose.prod.yml up -d ``` -To update to a newer image +To update to a newer image, run ```bash docker-compose -f docker-compose.yml -f docker-compose.prod.yml pull docker-compose -f docker-compose.yml -f docker-compose.prod.yml up -d ``` -Within a few minutes, you will be able to access the API at - -``` -https://yourdomain.com:8080 -``` +Within a few minutes, you will access the API at `https://yourdomain.com`. diff --git a/lib/web/public/images/favicon.ico b/lib/web/public/images/favicon.ico deleted file mode 100644 index 8c6f5d0..0000000 Binary files a/lib/web/public/images/favicon.ico and /dev/null differ diff --git a/lib/web/public/images/frankfurter.png b/lib/web/public/images/frankfurter.png index d240a50..cdf0338 100644 Binary files a/lib/web/public/images/frankfurter.png and b/lib/web/public/images/frankfurter.png differ diff --git a/lib/web/public/images/icon.png b/lib/web/public/images/icon.png new file mode 100644 index 0000000..73378ef Binary files /dev/null and b/lib/web/public/images/icon.png differ diff --git a/lib/web/public/stylesheets/application.css b/lib/web/public/stylesheets/application.css index 1217130..83e5ac2 100644 --- a/lib/web/public/stylesheets/application.css +++ b/lib/web/public/stylesheets/application.css @@ -1,109 +1,2 @@ -body { - font-family: 'Open Sans', sans-serif; -} - -strong { - background-color: #f8f9fa; -} - -.navbar { - background-color: #337cf6; - font-size: .875rem; - padding-bottom: 0; - padding-top: 0; -} - -.navbar .nav-link { - color: #fff; -} - -.navbar .nav-link:hover { - color: #f8f9fa; -} - -.container { - max-width: 600px; -} - -.section { - padding-top: 1em; -} - -.hero { - margin: 2em 0; -} - -.hero .container { - text-align: center; -} - -.hero .logo { - padding-bottom: 1em; - width: 100px; -} - -.footer .container { - padding-bottom: 2em; - text-align: center; -} - -.hljs { - background-color: #f8f9fa; - padding: 1em; -} - -.hljs:hover { - cursor: pointer; -} - -.hljs i { - color: #ced4da; - font-style: normal; -} - -@media screen and (max-width: 767px) { - .hero .logo { - width: 75px; - } - - #carbonads { - display: none; - } -} - -@media screen and (min-width: 768px) { - #carbonads { - background-color: #f8f9fa; - position: absolute; - right: 24px; - top: 24px; - width: 150px; - } - - .carbon-text { - color: #495057; - } - - .carbon-img, - .carbon-poweredby, - .carbon-text { - display: block; - margin: 10px; - } - - .carbon-text, - .carbon-poweredby { - line-height: 1.3; - text-decoration: none; - } - - .carbon-text { - font-size: 13px; - } - - .carbon-poweredby { - color: #6c757d; - font-size: 11px; - } -} - +body{background-color:#F08D5C;font-family:'Open Sans', sans-serif}.navbar .navbar-nav .nav-link{color:#fff;margin-bottom:2em;padding-left:12px;padding-right:12px}.navbar .navbar-nav .nav-link:hover{text-decoration:underline}.navbar .navbar-nav .nav-link .fa-fw{margin-right:6px}.content{margin-bottom:2em;max-width:720px}.content .logo img{padding-bottom:1em;width:120px}.content .logo,.content h1,.content h1+p+p{text-align:center}.content h1{font-size:56px}.content h1+p{display:none}.content h1+p+p{margin-bottom:2em}.content h1,.content h1+p+p{color:#FFF}.content h1+p+p,.content h2{font-size:20px}.content h2{font-weight:bold}code:not(.hljs){color:#8f1911}.hljs{border-radius:3px;padding:10px}.hljs.http{color:#2D4A53}.hljs i{color:#2D4A53}.hljs:hover{cursor:pointer}.footer{margin:2em 0;text-align:center}.footer .copyleft{display:inline-block;transform:rotate(180deg)} +/*# sourceMappingURL=application.css.map */ diff --git a/lib/web/public/stylesheets/sass/application.scss b/lib/web/public/stylesheets/sass/application.scss new file mode 100644 index 0000000..3e1edae --- /dev/null +++ b/lib/web/public/stylesheets/sass/application.scss @@ -0,0 +1,98 @@ +body { + background-color: #F08D5C; + font-family: 'Open Sans', sans-serif; +} + +.navbar { + .navbar-nav { + .nav-link { + color: #fff; + margin-bottom: 2em; + padding-left: 12px; + padding-right: 12px; + + &:hover { + text-decoration: underline; + } + + .fa-fw { + margin-right: 6px; + } + } + } +} + +.content { + margin-bottom: 2em; + max-width: 720px; + + .logo { + img { + padding-bottom: 1em; + width: 120px; + } + } + + .logo, + h1, + h1+p+p { + text-align: center; + } + + h1 { + font-size: 56px; + + &+p { + display: none; + } + + &+p+p { + margin-bottom: 2em; + } + + &, + &+p+p { + color: #FFF; + } + } + + h1+p+p, + h2 { + font-size: 20px; + } + + h2 { + font-weight: bold; + } +} + +code:not(.hljs) { + color: #8f1911; +} + +.hljs { + border-radius: 3px; + padding: 10px; + + &.http { + color: #2D4A53; + } + + i { + color: #2D4A53; + } + + &:hover { + cursor: pointer; + } +} + +.footer { + margin: 2em 0; + text-align: center; + + .copyleft { + display:inline-block; + transform: rotate(180deg); + } +} diff --git a/lib/web/server.rb b/lib/web/server.rb index 488a5e6..ea44391 100644 --- a/lib/web/server.rb +++ b/lib/web/server.rb @@ -2,6 +2,7 @@ require 'oj' require 'rack/cors' +require 'redcarpet' require 'sass/plugin/rack' require 'sinatra' @@ -15,11 +16,11 @@ use Rack::Cors do end end -Sass::Plugin.options[:style] = :compressed +css_location = File.join(Sinatra::Application.public_folder, 'stylesheets') +Sass::Plugin.options.update css_location: css_location, + style: :compressed use Sass::Plugin::Rack -set :static_cache_control, [:public, max_age: 60] - configure :development do set :show_exceptions, :after_handler end @@ -33,12 +34,12 @@ configure :test do end helpers do - def versioned_stylesheet(stylesheet) - "/stylesheets/#{stylesheet}.css?" + File.mtime(File.join(Sinatra::Application.public_folder, 'stylesheets', 'sass', "#{stylesheet}.scss")).to_i.to_s - end - def versioned_javascript(javascript) - "/javascripts/#{javascript}.js?" + File.mtime(File.join(Sinatra::Application.public_folder, 'javascripts', "#{javascript}.js")).to_i.to_s + version = File.mtime(File.join(Sinatra::Application.public_folder, + 'javascripts', + "#{javascript}.js")).to_i.to_s + + "/javascripts/#{javascript}.js?#{version}" end def end_of_day_quote @@ -84,7 +85,13 @@ options '*' do end get '/' do - erb :index + # FIXME: We should cache this in production. + parser = Redcarpet::Markdown.new(Redcarpet::Render::HTML, + disable_indented_code_blocks: true, + fenced_code_blocks: true) + content = parser.render(File.read('README.md')) + + erb :index, locals: { content: content } end get '/(?:latest|current)', mustermann_opts: { type: :regexp } do diff --git a/lib/web/views/index.erb b/lib/web/views/index.erb index 6509c4d..a063ff6 100644 --- a/lib/web/views/index.erb +++ b/lib/web/views/index.erb @@ -1,122 +1,77 @@ - + - - Frankfurter - + + Foreign exchange rates and currency conversion API | Frankfurter + - + + - + + <% if ENV['GA_TRACKING_ID'] %> + + + + <% end %> + -
-
- -

Frankfurter

-

- Foreign exchange rates and currency conversion API -

-
-
-
-
-

- Frankfurter is an open-source API for current and historical forex rates published by the European Central Bank. -

-

- The API updates rates around 4PM CET every working day. Historical data goes back to early 1999. -

+
+ -
+ <%= content %> + -
+
-
-
-
Best Practices
-

- The primary use case of Frankfurter is client side. If you are converting currencies in the browser, you will appreciate the unencumbered access currently not possible with similar commercial services. -

-
// Fetch and display GBP/USD
-fetch('/current?from=GBP&to=USD')
-  .then(resp => resp.json())
-  .then((data) => { alert(`GBP/USD = ${data.rates.USD}`); });
-

- If you are working with rates on the server side, download them off the European Central Bank to avoid unnecessary indirection. -

-

- If you require more currencies or granular data, you are better off going with a commercial service. -

-

- Cache results whenever possible. +

+ Frankfurter

-
-
-
-
Notes
-

- This project started out as Fixer in 2013. I renamed it to Frankfurter after selling the original domain in early 2018. -

- The API is open source and comes with no warranty. -

-
-
- + + -