Initial commit

This commit is contained in:
Andrew Tomaka 2015-07-21 10:33:31 -04:00
commit 03399ab51c
38 changed files with 964 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
db/*.db
config/app.yml

37
.gitlab-ci.yml Normal file
View File

@ -0,0 +1,37 @@
stages:
- test
- build
- deploy
test:
stage: test
script:
- apt-get update -qy
- apt-get install -y nodejs libqtwebkit-dev qt4-qmake sqlite3 libsqlite3-dev xvfb
- bundle install --path /cache
- RACK_ENV=test bundle exec rake db:create
- RACK_ENV=test bundle exec rake db:migrate
- RACK_ENV=test xvfb-run -a bundle exec rspec
tags:
- rails
build:
stage: build
script:
- docker build -t atomaka/link-share .
except:
- tags
tags:
- docker
deploy:
stage: deploy
script:
- VERSION=$(git describe --tags)
- docker build -t atomaka/link-share .
- docker tag atomaka/link-share:latest docker.atomaka.com/atomaka/link-share:$VERSION
- docker tag atomaka/link-share:latest docker.atomaka.com/atomaka/link-share:latest
- docker login -u $DOCKER_USERNAME -p $DOCKER_PASSWORD -e me@atomaka.com docker.atomaka.com
- docker push docker.atomaka.com/atomaka/link-share
only:
- tags
tags:
- docker

1
.ruby-version Normal file
View File

@ -0,0 +1 @@
2.2.2

17
Dockerfile Normal file
View File

@ -0,0 +1,17 @@
FROM alpine:latest
RUN export LANG=en_US.UTF-8 && \
export LANGUAGE=en_US.UTF-8 && \
export LC_ALL=en_US.UTF-8
RUN apk update \
&& apk add build-base ruby-dev sqlite-dev \
&& apk add ruby ruby-bundler ruby-io-console \
&& rm -rf /var/cache/apk*
WORKDIR /app
COPY Gemfile* ./
RUN bundle install --path=vendor/bundle --jobs=4 --without=development test
COPY . /app
CMD bundle exec rackup -o 0.0.0.0

26
Gemfile Normal file
View File

@ -0,0 +1,26 @@
source 'https://rubygems.org'
gem 'activerecord'
gem 'sinatra'
gem 'sqlite3'
gem 'sinatra-activerecord'
gem 'sinatra-contrib', require: false
gem 'sinatra-flash'
gem 'validate_url'
gem 'slim'
gem 'bigdecimal'
# alpine linux does not include a method to determine the timezone
gem 'tzinfo-data'
group :development do
gem 'rspec'
gem 'capybara-webkit'
gem 'factory_girl'
gem 'database_cleaner'
gem 'launchy'
gem 'pry'
gem 'rerun'
end

143
Gemfile.lock Normal file
View File

@ -0,0 +1,143 @@
GEM
remote: https://rubygems.org/
specs:
activemodel (4.2.3)
activesupport (= 4.2.3)
builder (~> 3.1)
activerecord (4.2.3)
activemodel (= 4.2.3)
activesupport (= 4.2.3)
arel (~> 6.0)
activesupport (4.2.3)
i18n (~> 0.7)
json (~> 1.7, >= 1.7.7)
minitest (~> 5.1)
thread_safe (~> 0.3, >= 0.3.4)
tzinfo (~> 1.1)
addressable (2.3.8)
arel (6.0.2)
backports (3.6.6)
bigdecimal (1.2.7)
builder (3.2.2)
capybara (2.5.0)
mime-types (>= 1.16)
nokogiri (>= 1.3.3)
rack (>= 1.0.0)
rack-test (>= 0.5.4)
xpath (~> 2.0)
capybara-webkit (1.7.1)
capybara (>= 2.3.0, < 2.6.0)
json
celluloid (0.16.0)
timers (~> 4.0.0)
coderay (1.1.0)
database_cleaner (1.4.1)
diff-lcs (1.2.5)
factory_girl (4.5.0)
activesupport (>= 3.0.0)
ffi (1.9.10)
hitimes (1.2.2)
i18n (0.7.0)
json (1.8.3)
launchy (2.4.3)
addressable (~> 2.3)
listen (2.10.1)
celluloid (~> 0.16.0)
rb-fsevent (>= 0.9.3)
rb-inotify (>= 0.9)
method_source (0.8.2)
mime-types (3.0)
mime-types-data (~> 3.2015)
mime-types-data (3.2015.1120)
mini_portile2 (2.0.0)
minitest (5.7.0)
multi_json (1.11.2)
nokogiri (1.6.7)
mini_portile2 (~> 2.0.0.rc2)
pry (0.10.1)
coderay (~> 1.1.0)
method_source (~> 0.8.1)
slop (~> 3.4)
rack (1.6.4)
rack-protection (1.5.3)
rack
rack-test (0.6.3)
rack (>= 1.0)
rb-fsevent (0.9.5)
rb-inotify (0.9.5)
ffi (>= 0.5.0)
rerun (0.10.0)
listen (~> 2.7, >= 2.7.3)
rspec (3.4.0)
rspec-core (~> 3.4.0)
rspec-expectations (~> 3.4.0)
rspec-mocks (~> 3.4.0)
rspec-core (3.4.1)
rspec-support (~> 3.4.0)
rspec-expectations (3.4.0)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.4.0)
rspec-mocks (3.4.0)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.4.0)
rspec-support (3.4.1)
sinatra (1.4.6)
rack (~> 1.4)
rack-protection (~> 1.4)
tilt (>= 1.3, < 3)
sinatra-activerecord (2.0.6)
activerecord (>= 3.2)
sinatra (~> 1.0)
sinatra-contrib (1.4.6)
backports (>= 2.0)
multi_json
rack-protection
rack-test
sinatra (~> 1.4.0)
tilt (>= 1.3, < 3)
sinatra-flash (0.3.0)
sinatra (>= 1.0.0)
slim (3.0.6)
temple (~> 0.7.3)
tilt (>= 1.3.3, < 2.1)
slop (3.6.0)
sqlite3 (1.3.10)
temple (0.7.6)
thread_safe (0.3.5)
tilt (2.0.1)
timers (4.0.1)
hitimes
tzinfo (1.2.2)
thread_safe (~> 0.1)
tzinfo-data (1.2015.7)
tzinfo (>= 1.0.0)
validate_url (1.0.2)
activemodel (>= 3.0.0)
addressable
xpath (2.0.0)
nokogiri (~> 1.3)
PLATFORMS
ruby
DEPENDENCIES
activerecord
bigdecimal
capybara-webkit
database_cleaner
factory_girl
launchy
pry
rerun
rspec
sinatra
sinatra-activerecord
sinatra-contrib
sinatra-flash
slim
sqlite3
tzinfo-data
validate_url
BUNDLED WITH
1.10.6

21
Makefile Normal file
View File

@ -0,0 +1,21 @@
NAME = atomaka/docker-linkshare
CONTAINER = sinatra-linkshare
DOMAIN = linkshare.atomaka.com
PORT = 10081
DATABASE = /home/atomaka/linkshare.db
all: build
build:
docker build -t $(NAME) .
clean:
docker rm -f $(CONTAINER)
deploy: clean
touch $(DATABASE)
docker run -e VIRTUAL_HOST=$(DOMAIN) -d -p $(PORT):9292 -v $(DATABASE):/app/db/linkshare.db --name=$(CONTAINER) --restart=always $(NAME)
docker exec $(CONTAINER) rake db:migrate
update:
git pull origin master

3
README.md Normal file
View File

@ -0,0 +1,3 @@
[![build status](https://git.atomaka.com/ci/projects/2/status.png?ref=master)](https://git.atomaka.com/ci/projects/2?ref=master)
# link-share

7
Rakefile Normal file
View File

@ -0,0 +1,7 @@
require './app'
require 'sinatra/activerecord/rake'
desc "Starts the development server"
task :serve do
`bundle exec rerun -b rackup`
end

89
app.rb Normal file
View File

@ -0,0 +1,89 @@
require './config/init'
get '/manage' do
protected!
@links = get_links(params[:status])
slim :manage
end
get '/new' do
protected!
@link = Link.new
slim :new
end
post '/' do
protected!
@link = Link.new(params[:link])
if @link.save
flash[:success] = 'Link has been created'
redirect '/manage'
else
flash.now[:danger] = 'Did not pass validations'
slim :new
end
end
get '/send' do
protected!
@link = Link.find(params[:id])
@link.mark_sent
flash[:success] = 'Link has been marked as sent'
redirect '/manage'
end
get '/destroy' do
protected!
@link = Link.find(params[:id])
if @link.sent?
flash[:warning] = 'Cannot delete sent link'
else
@link.destroy
flash[:success] = 'Link has been deleted'
end
redirect '/manage'
end
get '/' do
slim :calendar
end
get '/events' do
start = params[:start]
finish = params[:end]
json serialize(Link.calendar(start, finish))
end
private
def get_links(status)
if status == 'sent'
Link.sent
elsif status == 'all'
Link.all
else
Link.unsent
end
end
def serialize(events)
events.map do |event|
{
title: event.title,
url: event.url,
start: event.sent_at.in_time_zone('Eastern Time (US & Canada)')
}
end
end

2
config.ru Normal file
View File

@ -0,0 +1,2 @@
require './app'
run Sinatra::Application

3
config/app.yml.sample Normal file
View File

@ -0,0 +1,3 @@
secret: YOUR_SECRET
users:
USERNAME: PASSWORD

15
config/environments.rb Normal file
View File

@ -0,0 +1,15 @@
set :database, 'sqlite3:db/linkshare.db'
configure :production do
config_file 'config/app.yml'
end
configure :development do
set :show_exceptions, true
config_file 'config/app.yml'
end
configure :test do
set :database, 'sqlite3:db/test.db'
config_file 'spec/fixtures/app.yml'
end

17
config/init.rb Normal file
View File

@ -0,0 +1,17 @@
require 'rubygems'
require 'bundler'
require 'bundler/setup'
require 'sinatra/config_file'
require 'sinatra/json'
Bundler.require
set :root, File.dirname('..')
require_relative 'environments'
require_relative '../models/link'
require_relative '../helpers/application_helper'
config_file 'config/app.yml'
use Rack::Session::Cookie, secret: settings.secret

0
db/.gitkeep Normal file
View File

View File

@ -0,0 +1,11 @@
class AddLinks < ActiveRecord::Migration
def change
create_table :links do |t|
t.string :title
t.string :link
t.datetime :sent_at
t.timestamps null: false
end
end
end

View File

@ -0,0 +1,5 @@
class ChangeLinkToUrl < ActiveRecord::Migration
def change
rename_column :links, :link, :url
end
end

24
db/schema.rb Normal file
View File

@ -0,0 +1,24 @@
# encoding: UTF-8
# This file is auto-generated from the current state of the database. Instead
# of editing this file, please use the migrations feature of Active Record to
# incrementally modify your database, and then regenerate this schema definition.
#
# Note that this schema.rb definition is the authoritative source for your
# database schema. If you need to create the application database on another
# system, you should be using db:schema:load, not running all the migrations
# from scratch. The latter is a flawed and unsustainable approach (the more migrations
# you'll amass, the slower it'll run and the greater likelihood for issues).
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20150721201947) do
create_table "links", force: :cascade do |t|
t.string "title"
t.string "url"
t.datetime "sent_at"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
end

View File

@ -0,0 +1,20 @@
helpers do
def protected!
return if authorized?
headers['WWW-Authenticate'] = 'Basic realm="Restricted Area"'
halt 401, "Not authorized\n"
end
def authorized?
@auth ||= Rack::Auth::Basic::Request.new(request.env)
return unless @auth.provided? and @auth.basic?
username, password = @auth.credentials
user_exists?(username) && settings.users[username] == password
end
def user_exists?(username)
settings.users.keys.include?(username)
end
end

27
models/link.rb Normal file
View File

@ -0,0 +1,27 @@
class Link < ActiveRecord::Base
validates :title,
presence: true
validates :url,
presence: true,
url: true,
uniqueness: true
scope :sent, -> { where('sent_at IS NOT NULL').order('sent_at DESC') }
scope :unsent, -> { where('sent_at IS NULL').order('created_at ASC') }
scope :sent_after, ->(date) { where('sent_at > ?', date) }
scope :sent_before, ->(date) { where('sent_at < ?', date) }
scope :calendar, ->(start, finish) { sent_after(start).sent_before(finish) }
def mark_sent
update_attribute(:sent_at, Time.now)
end
def sent?
!!self.sent_at
end
def url=(url)
url.chomp!('/') if url.respond_to?(:chomp)
write_attribute(:url, url)
end
end

17
public/custom.css Normal file
View File

@ -0,0 +1,17 @@
.link {
padding: 5px;
background: #fff;
}
.link:nth-of-type(odd) {
background: #e7e7e7
}
.controls {
margin-bottom: 10px;
margin-top: 10px;
}
.fc-time {
display: none;
}

10
public/custom.js Normal file
View File

@ -0,0 +1,10 @@
$(document).ready(function() {
$('#calendar').fullCalendar({
header: {
left: 'prev,next today',
center: 'title',
right: ''
},
events: '/events',
});
});

View File

@ -0,0 +1,6 @@
FactoryGirl.define do
factory :link do
sequence(:title) { |n| "Sequenced Title (#{n})" }
sequence(:url) { |n| "http://www.#{n}example#{n}.com/#{n}" }
end
end

View File

@ -0,0 +1,49 @@
require 'spec_helper'
describe 'Admin Create Links' do
context 'when not logged in' do
it 'should not allow access to new link form' do
visit '/new'
expect(page.status_code).to be(401)
end
end
context 'when logged in' do
before(:each) { basic_auth 'admin', 'password' }
context 'with valid data' do
let(:link) { build(:link) }
it 'should allow creation of a link' do
visit '/new'
fill_in :link_title, with: link.title
fill_in :link_url, with: link.url
click_button 'Submit'
expect(page).to have_content 'Link has been created'
end
it 'should list the new link' do
visit '/new'
fill_in :link_title, with: link.title
fill_in :link_url, with: link.url
click_button 'Submit'
expect(page).to have_content link.title
end
end
context 'with invalid data' do
let(:link) { build(:link, title: '') }
it 'should not allow link with invalid data' do
visit '/new'
fill_in :link_title, with: link.title
fill_in :link_url, with: link.url
click_button 'Submit'
expect(page).to have_content 'Did not pass validations'
end
end
end
end

View File

@ -0,0 +1,36 @@
require 'spec_helper'
describe 'Admin Delete Links' do
context 'when not logged in' do
it 'should not allow access to delete links' do
visit '/send'
expect(page.status_code).to be(401)
end
end
context 'when logged in' do
let!(:links) { 10.times.collect { create(:link) } }
before(:each) { basic_auth 'admin', 'password' }
it 'should allow deleting of a link' do
visit '/manage'
first('a', text: 'Delete').click
expect(page).to have_content('Link has been deleted')
end
it 'should remove deleted link all lists' do
link = create(:link)
visit '/manage'
find('a', text: link.title)
.find(:xpath, '../..')
.find('a', text: 'Delete')
.click
visit '/manage?status=all'
expect(page).to_not have_content(link.title)
end
end
end

View File

@ -0,0 +1,53 @@
require 'spec_helper'
describe 'Admin List Links' do
context 'when not logged in' do
it 'should not allow access to list links' do
visit '/manage'
expect(page.status_code).to be(401)
end
end
context 'when logged in' do
let!(:sent_link) { create(:link, sent_at: Time.now) }
let!(:links) { 10.times.collect { create(:link) } }
before(:each) { basic_auth 'admin', 'password' }
context 'unsent' do
it 'should show unsent links by default' do
visit '/manage'
expect(page).to_not have_content(sent_link.title)
links.each { |link| expect(page).to have_content(link.title) }
end
it 'should only list unsent links' do
visit '/manage'
first('a', text: 'Unsent Links').click
expect(page).to_not have_content(sent_link.title)
links.each { |link| expect(page).to have_content(link.title) }
end
end
context 'sent' do
it 'should only list sent links' do
visit '/manage'
first('a', text: 'Sent Links').click
expect(page).to have_content(sent_link.title)
links.each { |link| expect(page).to_not have_content(link.title) }
end
end
context 'all' do
it 'should list all links' do
visit '/manage'
first('a', text: 'All Links').click
expect(page).to have_content(sent_link.title)
links.each { |link| expect(page).to have_content(link.title) }
end
end
end
end

View File

@ -0,0 +1,34 @@
require 'spec_helper'
describe 'Admin Send Links' do
context 'when not logged in' do
it 'should not allow access to send links' do
visit '/send'
expect(page.status_code).to be(401)
end
end
context 'when logged in' do
let!(:links) { 10.times.collect { create(:link) } }
before(:each) { basic_auth 'admin', 'password' }
it 'should allow sending of a link' do
visit '/manage'
first('a', text: 'Send').click
expect(page).to have_content('Link has been marked as sent')
end
it 'should remove sent link from unsent list' do
link = create(:link)
visit '/manage'
find('a', text: link.title)
.find(:xpath, '../..')
.find('a', text: 'Send')
.click
expect(page).to_not have_content(link.title)
end
end
end

View File

@ -0,0 +1,56 @@
require 'spec_helper'
describe 'Authorization' do
context 'when not logged in' do
it 'should not allow access to manage' do
visit '/manage'
expect(page.status_code).to be(401)
end
it 'should not allow access to the create form' do
visit '/new'
expect(page.status_code).to be(401)
end
it 'should not allow access to send' do
visit '/send'
expect(page.status_code).to be(401)
end
it 'should not allow access to delete' do
visit '/destroy'
expect(page.status_code).to be(401)
end
end
it 'should allow accessing the home page' do
visit '/'
expect(page.status_code).to be(200)
end
it 'should allow accessing the events page' do
visit '/events'
expect(page.status_code).to be(200)
end
context 'when logging in' do
context 'with incorrect credentials' do
before(:each) { basic_auth 'baduser', 'badpassword' }
it 'should allow not allow access' do
visit '/manage'
expect(page.status_code).to be(401)
end
end
context 'with correct credentials' do
before(:each) { basic_auth 'admin', 'password' }
it 'should allow access' do
visit '/manage'
expect(page.status_code).to be(200)
end
end
end
end

View File

@ -0,0 +1,17 @@
require 'spec_helper'
describe 'Calendar View' do
OVER_ONE_MONTH = 60*60*24*40
let!(:showing_link) { create(:link, sent_at: Time.now) }
let!(:before_link) { create(:link, sent_at: Time.now - OVER_ONE_MONTH) }
let!(:after_link) { create(:link, sent_at: Time.now + OVER_ONE_MONTH) }
it 'should only show links for the current month', js: true do
visit '/'
expect(page).to have_content(showing_link.title)
expect(page).to_not have_content(before_link.title)
expect(page).to_not have_content(after_link.title)
end
end

3
spec/fixtures/app.yml vendored Normal file
View File

@ -0,0 +1,3 @@
secret: my_secret_something_blan
users:
admin: password

65
spec/models/link_spec.rb Normal file
View File

@ -0,0 +1,65 @@
require 'spec_helper'
describe Link do
let(:link) { build(:link) }
context 'with valid data' do
it 'should be valid' do
expect(link).to be_valid
end
end
context 'with invalid data' do
it 'should not be valid with blank title' do
link.title = ''
expect(link).to be_invalid
end
it 'should not be valid with a blank url' do
link.url = ''
expect(link).to be_invalid
end
it 'should not be valid with a bad url' do
link.url = 'bad'
expect(link).to be_invalid
end
it 'should not be valid with a duplicate url' do
create(:link, url: link.url)
expect(link).to be_invalid
end
end
context '#mark_sent' do
it 'should set a time to sent_at' do
link.mark_sent
expect(link.sent_at).to_not be(nil)
end
end
context '#sent?' do
context 'when already sent' do
it 'should respond with true' do
link.mark_sent
expect(link.sent?).to be(true)
end
it 'should respond with false' do
expect(link.sent?).to be(false)
end
end
end
context '#url' do
it 'removes a trailing slash' do
link.url = 'http://www.url.com/'
expect(link.url).to eq('http://www.url.com')
end
end
end

72
spec/spec_helper.rb Normal file
View File

@ -0,0 +1,72 @@
require 'rack/test'
require 'rspec'
require 'capybara/rspec'
require 'capybara/webkit'
require 'factory_girl'
require 'database_cleaner'
ENV['RACK_ENV'] = 'test'
require File.expand_path '../../app.rb', __FILE__
ActiveRecord::Migration.maintain_test_schema!
module TestingMixin
include Rack::Test::Methods
include RSpec::Matchers
include Capybara::DSL
include FactoryGirl::Syntax::Methods
Capybara.app = Sinatra::Application
Capybara.javascript_driver = :webkit
Capybara.asset_host = 'http://localhost:3000'
FactoryGirl.definition_file_paths = %w{./factories ./test/factories ./spec/factories}
FactoryGirl.find_definitions
def app() Sinatra::Application end
def basic_auth(username, password)
if page.driver.respond_to?(:basic_auth)
page.driver.basic_auth(username, password)
elsif page.driver.respond_to?(:basic_authorize)
page.driver.basic_authorize(username, password)
elsif page.driver.respond_to?(:browser) && page.driver.browser.respond_to?(:basic_authorize)
page.driver.browser.basic_authorize(username, password)
else
raise "I don't know how to log in!"
end
end
end
Capybara::Webkit.configure do |config|
config.allow_unknown_urls
end
RSpec.configure do |config|
config.include TestingMixin
config.before(:suite) do
DatabaseCleaner.clean_with(:truncation)
end
config.before(:each) do
DatabaseCleaner.strategy = :truncation
end
config.before(:each, :js => true) do
DatabaseCleaner.strategy = :truncation
end
config.before(:each) do
DatabaseCleaner.start
end
config.append_after(:each) do
DatabaseCleaner.clean
Capybara.reset_sessions!
end
config.order = :random
Kernel.srand config.seed
end

1
views/calendar.slim Normal file
View File

@ -0,0 +1 @@
#calendar

4
views/form_errors.slim Normal file
View File

@ -0,0 +1,4 @@
- if @link.errors.any?
ul
- @link.errors.full_messages.each do |error|
li= error

33
views/layout.slim Normal file
View File

@ -0,0 +1,33 @@
doctype html
html
head
title Links App
link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css"
link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/fullcalendar/2.3.2/fullcalendar.min.css"
link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/fullcalendar/2.3.2/fullcalendar.print.css" media="print"
link rel="stylesheet" href="/custom.css"
body
.navbar.navbar-default.navbar-static-top.navbar-custom
.container
.navbar-header
button.navbar-toggle.collapsed type='button' data-toggle='collapse' data-target='.navbar-collapse'
span.sr-only Toggle navigation
span.icon-bar
span.icon-bar
span.icon-bar
a href="/" class="navbar-brand" Links
.collapse.navbar-collapse
ul.nav.navbar-nav
li
a href="/manage" Manage
ul.nav.navbar-nav.navbar-right
.container
- unless flash.empty?
- flash.each_key do |key|
.alert class="alert-#{key}" role="alert"= flash[key]
== yield
script src="//cdnjs.cloudflare.com/ajax/libs/jquery/2.1.4/jquery.min.js"
script src="//cdnjs.cloudflare.com/ajax/libs/jqueryui/1.11.4/jquery-ui.min.js"
script src="//cdnjs.cloudflare.com/ajax/libs/moment.js/2.10.6/moment.min.js"
script src="//cdnjs.cloudflare.com/ajax/libs/fullcalendar/2.3.2/fullcalendar.min.js"
script src="custom.js"

14
views/link_list.slim Normal file
View File

@ -0,0 +1,14 @@
.links
- @links.each do |link|
.row.link
.col-md-9
a href="#{link.url}" #{link.title}
.col-md-3
- if link.sent_at
= link.sent_at
- else
ul.list-inline style="margin: 0"
li
a href="/send?id=#{link.id}" Send
li
a href="/destroy?id=#{link.id}" Delete

15
views/manage.slim Normal file
View File

@ -0,0 +1,15 @@
.row.controls
.btn-toolbar.pull-right
a href="/manage?status=unsent" class="btn btn-primary" Unsent Links
a href="/manage?status=sent" class="btn btn-primary" Sent Links
a href="/manage?status=all" class="btn btn-primary" All Links
a href="/new" class="btn btn-primary" New Link
== slim :link_list
.row.controls
.btn-toolbar
a href="/manage?status=unsent" class="btn btn-primary" Unsent Links
a href="/manage?status=sent" class="btn btn-primary" Sent Links
a href="/manage?status=all" class="btn btn-primary" All Links
a href="/new" class="btn btn-primary" New Link

9
views/new.slim Normal file
View File

@ -0,0 +1,9 @@
== slim :form_errors
form action="/" method="post"
.form-group
label for="link_title" Title
input type="text" name="link[title]" id="link_title" class="form-control" value="#{@link.title}"
.form-group
label for="link_url" Link
input type="text" name="link[url]" id="link_url" class="form-control" value="#{@link.url}"
button type="submit" class="btn btn-primary" Submit