Make open source

This commit is contained in:
John Lees-Miller
2018-10-27 16:46:26 +01:00
commit 6e82ba528b
163 changed files with 3912 additions and 0 deletions

27
app/admin/admin_users.rb Normal file
View File

@ -0,0 +1,27 @@
ActiveAdmin.register AdminUser do
permit_params :email, :password, :password_confirmation
index do
selectable_column
id_column
column :email
column :current_sign_in_at
column :sign_in_count
column :created_at
actions
end
filter :email
filter :current_sign_in_at
filter :sign_in_count
filter :created_at
form do |f|
f.inputs do
f.input :email
f.input :password
f.input :password_confirmation
end
f.actions
end
end

25
app/admin/attendees.rb Normal file
View File

@ -0,0 +1,25 @@
ActiveAdmin.register Attendee do
scope :child?
scope :diet?
controller do
def apply_sorting(chain)
params[:order] ? chain : chain.reorder(last_name: :asc, first_name: :asc)
end
end
index do
column :first_name
column :last_name
column :email
column :diet
column :notes
column :child
column :updated_at
end
config.batch_actions = false
config.filters = false
config.per_page = 500
config.clear_action_items!
end

32
app/admin/dashboard.rb Normal file
View File

@ -0,0 +1,32 @@
ActiveAdmin.register_page 'Dashboard' do
menu priority: 1, label: proc { I18n.t('active_admin.dashboard') }
content title: proc { I18n.t('active_admin.dashboard') } do
div class: 'blank_slate_container', id: 'dashboard_default_message' do
span class: 'blank_slate' do
span I18n.t('active_admin.dashboard_welcome.welcome')
small I18n.t('active_admin.dashboard_welcome.call_to_action')
end
end
# Here is an example of a simple dashboard with columns and panels.
#
# columns do
# column do
# panel "Recent Posts" do
# ul do
# Post.recent(5).map do |post|
# li link_to(post.title, admin_post_path(post))
# end
# end
# end
# end
# column do
# panel "Info" do
# para "Welcome to ActiveAdmin."
# end
# end
# end
end
end

21
app/admin/guests.rb Normal file
View File

@ -0,0 +1,21 @@
ActiveAdmin.register Guest do
permit_params :email, :first_name, :last_name, :attending, :diet, :songs,
:notes
scope :confirmed
scope :attending
scope :not_attending
form do |_f|
inputs 'Guest' do
input :email, as: :string
input :first_name
input :last_name
input :attending
input :diet, as: :text
input :songs, as: :text
input :notes, as: :text
end
actions
end
end

13
app/admin/plus_ones.rb Normal file
View File

@ -0,0 +1,13 @@
ActiveAdmin.register PlusOne do
permit_params :first_name, :last_name, :diet, :child
form do |_f|
inputs 'Plus One' do
input :first_name
input :last_name
input :diet, as: :text
input :child
end
actions
end
end

View File

@ -0,0 +1,3 @@
//= link_tree ../images
//= link_directory ../javascripts .js
//= link_directory ../stylesheets .css

0
app/assets/images/.keep Normal file
View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

BIN
app/assets/images/jumbo.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 165 KiB

BIN
app/assets/images/plan.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 145 KiB

BIN
app/assets/images/venue.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 296 KiB

View File

@ -0,0 +1 @@
//= require active_admin/base

View File

@ -0,0 +1,19 @@
// This is a manifest file that'll be compiled into application.js, which will include all the files
// listed below.
//
// Any JavaScript/Coffee file within this directory, lib/assets/javascripts, or any plugin's
// vendor/assets/javascripts directory can be referenced here using a relative path.
//
// It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
// compiled file. JavaScript code in this file should be added after the last require_* statement.
//
// Read Sprockets README (https://github.com/rails/sprockets#sprockets-directives) for details
// about supported directives.
//
//= require rails-ujs
//= require jquery
//= require popper
//= require bootstrap-sprockets
//= require_tree .
//= stub active_admin

View File

@ -0,0 +1,13 @@
// Place all the behaviors and hooks related to the matching controller here.
// All this logic will automatically be available in application.js.
$(function () {
var radioMatcher = '#guest-edit-form input[name="guest[attending]"]'
var radioButtons = $(radioMatcher)
function toggleAttendingOnly () {
var attending = $(radioMatcher + ':checked').val()
$('.guest-attending-only textarea').attr('disabled', attending !== 'true')
}
radioButtons.on('change', toggleAttendingOnly)
toggleAttendingOnly()
})

View File

@ -0,0 +1,2 @@
// Place all the behaviors and hooks related to the matching controller here.
// All this logic will automatically be available in application.js.

View File

@ -0,0 +1,2 @@
// Place all the behaviors and hooks related to the matching controller here.
// All this logic will automatically be available in application.js.

View File

@ -0,0 +1,65 @@
/*
* This is a manifest file that'll be compiled into application.css, which will include all the files
* listed below.
*
* Any CSS and SCSS file within this directory, lib/assets/stylesheets, or any plugin's
* vendor/assets/stylesheets directory can be referenced here using a relative path.
*
* You're free to add application-wide styles to this file and they'll appear at the bottom of the
* compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS
* files in this directory. Styles in this file should be added after the last require_* statement.
* It is generally better to create a new file per style scope.
*
*/
@import url('https://fonts.googleapis.com/css?family=Montserrat:400,700|Arvo:400');
$font-family-sans-serif: Montserrat, Helvetica, Arial, sans-serif;
$font-family-serif: Arvo, Georgia, "Times New Roman", Times, serif;
$font-family-base: $font-family-serif;
$headings-font-family: $font-family-sans-serif;
$body-color: rgba(28, 13, 10, 0.7);
$headings-color: rgba(28, 13, 10, 0.7);
$theme-colors: (
"primary": #ff745c
);
$enable-rounded: false;
@import "bootstrap";
@import "guests";
@import "welcome";
h1 {
font-size: 60px;
font-weight: 700;
letter-spacing: 0.0833333em;
text-transform: uppercase;
}
h2 {
color: theme-color("primary");
font-size: 28px;
font-weight: 700;
line-height: 1.5em;
letter-spacing: 0.0833333em;
text-transform: uppercase;
svg {
fill: currentColor;
margin: 0 auto;
height: 100px;
}
}
.wedding-cta-btn {
letter-spacing: 2px;
padding: 0.75rem 1.5rem;
}
.wedding-required {
color: theme-color("primary");
}

View File

@ -0,0 +1,27 @@
// Place all the styles related to the Guests controller here.
// They will automatically be included in application.css.
// You can use Sass (SCSS) here: http://sass-lang.com/
#guests-leader {
background:
linear-gradient(rgba(28, 13, 10, 0.4), rgba(28, 13, 10, 0.6)),
image-url('flowers.jpg') center center no-repeat;
background-size: cover;
h1 {
color: rgba(255, 255, 255, 0.95);
}
}
.guests-rsvp-nav {
padding-bottom: $spacer;
@media (max-width: 768px) {
li.nav-item { width:100%; }
}
}
.guests-buttons {
padding-top: $spacer;
padding-bottom: 10 * $spacer;
}

View File

@ -0,0 +1,3 @@
// Place all the styles related to the PlusOnes controller here.
// They will automatically be included in application.css.
// You can use Sass (SCSS) here: http://sass-lang.com/

View File

@ -0,0 +1,104 @@
// Place all the styles related to the Welcome controller here.
// They will automatically be included in application.css.
// You can use Sass (SCSS) here: http://sass-lang.com/
#welcome-index-leader {
background: linear-gradient(rgba(28, 13, 10, 0.4), rgba(28, 13, 10, 0.6)),
image-url("jumbo.jpg") center center no-repeat;
background-size: cover;
height: 820px;
* {
color: rgba(255, 255, 255, 0.95);
}
h1 {
margin-top: 200px;
}
.leader-date {
font-size: 17px;
letter-spacing: 4px;
padding: 12pt;
text-transform: uppercase;
}
}
#welcome-index-when-where {
h1 {
padding: 3 * $spacer 0;
}
}
#welcome-index-venue-leader {
background: linear-gradient(rgba(28, 13, 10, 0.4), rgba(28, 13, 10, 0.6)),
image-url("venue.jpg") center center no-repeat;
background-size: cover;
margin-top: 5 * $spacer;
* {
color: rgba(255, 255, 255, 0.95);
}
h1 {
padding: 3 * $spacer 0;
}
}
#welcome-index-venue {
h2 {
padding-top: 3 * $spacer;
padding-bottom: $spacer;
text-align: center;
}
p {
text-align: justify;
}
}
#welcome-index-wedding-plan-leader {
background: linear-gradient(rgba(28, 13, 10, 0.4), rgba(28, 13, 10, 0.6)),
image-url("plan.jpg") center center no-repeat;
background-size: cover;
margin-top: 5 * $spacer;
* {
color: rgba(255, 255, 255, 0.95);
}
h1 {
padding: 3 * $spacer 0;
}
}
#welcome-index-wedding-plan {
h2 {
padding-top: 3 * $spacer;
padding-bottom: $spacer;
text-align: center;
}
p {
text-align: justify;
}
}
#welcome-index-footer {
background-color: #faebe3;
margin-top: 5 * $spacer;
#welcome-index-footer-cta {
padding-top: 8 * $spacer;
padding-bottom: 3 * $spacer;
}
#welcome-index-footer-credits {
padding-top: 5 * $spacer;
padding-bottom: 3 * $spacer;
small {
color: rgba(28, 13, 10, 0.4);
}
}
}

View File

@ -0,0 +1,9 @@
class ApplicationController < ActionController::Base
protect_from_forgery with: :exception
if ENV['BASIC_AUTH_NAME'] && ENV['BASIC_AUTH_PASSWORD']
http_basic_authenticate_with \
name: ENV['BASIC_AUTH_NAME'],
password: ENV['BASIC_AUTH_PASSWORD']
end
end

View File

View File

@ -0,0 +1,75 @@
class GuestsController < ApplicationController
def new
respond_to :html
@guest = Guest.new
end
def create
respond_to :html
@guest = Guest.new(guest_params)
unless ENV['RECAPTCHA_SECRET_KEY'].blank? || verify_recaptcha(model: @guest)
render :new
return
end
if @guest.save
redirect_to guest_path(@guest)
else
existing_guest = Guest.find_by(email: guest_params[:email])
if existing_guest
@guest = existing_guest
GuestMailer.welcome_back_email(@guest).deliver_now
render :new_exists
else
render :new
end
end
end
def show
respond_to :html
@guest = Guest.find_by_id_token(params[:id])
end
def update
@guest = Guest.find_by_id_token(params[:id])
if @guest.update(guest_params)
if @guest.attending?
redirect_to guest_plus_ones_path(@guest)
else
redirect_to confirm_guest_path(@guest)
end
else
render :show
end
end
def confirm
respond_to :html
@guest = Guest.find_by_id_token(params[:id])
end
def complete
respond_to :html
@guest = Guest.find_by_id_token(params[:id])
Guest.transaction do
@guest.update!(guest_params)
@guest.touch :confirmed_at
end
GuestMailer.confirmation_email(@guest).deliver_now
end
private
def guest_params
params.require(:guest).permit(
:email, :first_name, :last_name, :attending, :diet, :songs, :notes
)
end
end

View File

@ -0,0 +1,53 @@
class PlusOnesController < ApplicationController
before_action do
@guest = Guest.find_by_id_token(params[:guest_id])
end
def new
respond_to :html
@plus_one = @guest.plus_ones.new
end
def create
respond_to :html
@plus_one = @guest.plus_ones.new(plus_one_params)
if @plus_one.save
redirect_to guest_plus_ones_path(@guest)
else
render :new
end
end
def index
respond_to :html
@plus_ones = @guest.plus_ones.order(:first_name, :last_name)
end
def edit
@plus_one = @guest.plus_ones.find(params[:id])
respond_to :html
end
def update
@plus_one = @guest.plus_ones.find(params[:id])
if @plus_one.update(plus_one_params)
redirect_to guest_plus_ones_path(@guest)
else
render :edit
end
end
def destroy
respond_to :html
@plus_one = @guest.plus_ones.find_by(id: params[:id])
@plus_one.destroy if @plus_one
redirect_to guest_plus_ones_path(@guest)
end
private
def plus_one_params
params.require(:plus_one).permit(:first_name, :last_name, :diet, :child)
end
end

View File

@ -0,0 +1,6 @@
class WelcomeController < ApplicationController
def index
@google_maps_url = 'https://goo.gl/maps/nBAxNAsmPSS2'
@cool_earth_url = 'https://www.coolearth.org/'
end
end

View File

@ -0,0 +1,24 @@
module ApplicationHelper
def flash_class(key)
case key
when :notice then 'alert alert-info'
when :success then 'alert alert-success'
when :error then 'alert alert-error'
when :alert then 'alert alert-error'
end
end
def errors_for(object)
return unless object.errors.any?
content_tag(:div, class: 'mb-3 card border-danger') do
concat(content_tag(:div, class: 'card-header bg-danger text-white') do
concat "Oops, #{pluralize(object.errors.count, 'problem')}:"
end)
concat(content_tag(:ul, class: 'mb-0 list-group list-group-flush') do
object.errors.full_messages.each do |msg|
concat content_tag(:li, msg, class: 'list-group-item')
end
end)
end
end
end

View File

@ -0,0 +1,2 @@
module GuestsHelper
end

View File

@ -0,0 +1,2 @@
module PlusOnesHelper
end

View File

@ -0,0 +1,2 @@
module WelcomeHelper
end

View File

@ -0,0 +1,2 @@
class ApplicationJob < ActiveJob::Base
end

View File

@ -0,0 +1,4 @@
class ApplicationMailer < ActionMailer::Base
default from: 'from@example.com'
layout 'mailer'
end

View File

@ -0,0 +1,19 @@
class GuestMailer < ApplicationMailer
default from: ENV['FROM_EMAIL'], reply_to: ENV['CONTACT_EMAIL']
def confirmation_email(guest)
@guest = guest
mail(
to: guest.name_with_email,
subject: "#{I18n.t(:wedding_name)}: RSVP Confirmation"
)
end
def welcome_back_email(guest)
@guest = guest
mail(
to: guest.name_with_email,
subject: "#{I18n.t(:wedding_name)}: Your RSVP"
)
end
end

9
app/models/admin_user.rb Normal file
View File

@ -0,0 +1,9 @@
#
# ActiveAdmin console user.
#
class AdminUser < ApplicationRecord
# Include default devise modules. Others available are:
# :confirmable, :lockable, :timeoutable and :omniauthable
devise :database_authenticatable,
:recoverable, :rememberable, :trackable, :validatable
end

View File

@ -0,0 +1,3 @@
class ApplicationRecord < ActiveRecord::Base
self.abstract_class = true
end

7
app/models/attendee.rb Normal file
View File

@ -0,0 +1,7 @@
#
# A guest or plus one.
#
class Attendee < ApplicationRecord
scope :diet?, -> { where.not(diet: nil) }
scope :child?, -> { where(child: true) }
end

View File

View File

@ -0,0 +1,27 @@
#
# Make a model findable only when a secure token is provided.
#
module FindableWithToken
extend ActiveSupport::Concern
ID_TOKEN_RX = /\A(\d+)-(\w+)\z/
included do
has_secure_token
def to_param
id ? "#{id}-#{token}" : nil
end
def self.find_by_id_token(id_token)
raise ActiveRecord::RecordNotFound unless id_token =~ ID_TOKEN_RX
id = Regexp.last_match(1)
token = Regexp.last_match(2)
record = find(id)
raise ActiveRecord::RecordNotFound unless
ActiveSupport::SecurityUtils.secure_compare(record.token, token)
record
end
end
end

41
app/models/guest.rb Normal file
View File

@ -0,0 +1,41 @@
#
# A primary guest.
#
class Guest < ApplicationRecord
include FindableWithToken
auto_strip_attributes :email, :first_name, :last_name, :diet, :songs, :notes
validates :email, presence: true, uniqueness: true
validates :email, format: Devise.email_regexp, allow_blank: true
validates :first_name, presence: true, if: :persisted?
validates :first_name, length: { maximum: 1024 }
validates :last_name, presence: true, if: :persisted?
validates :last_name, length: { maximum: 1024 }
def name
"#{first_name} #{last_name}"
end
def name_with_email
"#{name} <#{email}>"
end
# Don't allow long or odd names in emails; may be spam.
def email_safe_salutation
return 'Hello,' if
first_name.blank? || first_name !~ /\A[\p{Word}\s'-]{1,30}\z/i
"Dear #{first_name},"
end
validates :diet, length: { maximum: 8192 }
validates :songs, length: { maximum: 8192 }
validates :notes, length: { maximum: 8192 }
has_many :plus_ones, dependent: :destroy
scope :confirmed, -> { where.not(confirmed_at: nil) }
scope :attending, -> { confirmed.where(attending: true) }
scope :not_attending, -> { confirmed.where(attending: false) }
end

19
app/models/plus_one.rb Normal file
View File

@ -0,0 +1,19 @@
#
# An extra guest.
#
class PlusOne < ApplicationRecord
belongs_to :guest
auto_strip_attributes :first_name, :last_name, :diet
validates :first_name, presence: true
validates :first_name, length: { maximum: 1024 }
validates :last_name, presence: true
validates :last_name, length: { maximum: 1024 }
def name
"#{first_name} #{last_name}"
end
validates :diet, length: { maximum: 8192 }
end

View File

@ -0,0 +1,3 @@
<strong>Alert</strong><br>
Qikiqtaaluk Region, Nunavut<br>
Canada<br>

View File

@ -0,0 +1,3 @@
<strong>Halley Research Station</strong><br>
Brunt Ice Shelf<br>
Antartica

View File

@ -0,0 +1,25 @@
<p><%= @guest.email_safe_salutation %></p>
<% if @guest.attending %>
<p>We look forward to seeing you!</p>
<p>The address is:</p>
<address><%= render partial: 'application/wedding_address' %></address>
<h3>Your RSVP</h3>
<p>If you would like to view or update your RSVP, you can use the following link to get back to it:</p>
<p><%= link_to guest_url(@guest), guest_url(@guest) %></p>
<h3>Sending a Card?</h3>
<p>If you need it, our mailing address is:</p>
<address><%= render partial: 'application/mailing_address' %></address>
<h3>Questions and answers</h3>
<p>Please check <%= link_to 'our wedding website', root_url %> for more information, or you can reply to this email.</p>
<% else %>
<p>We're sorry to hear you can't make it. If you change your mind, you can update your RSVP with the link below:</p>
<p><%= link_to guest_url(@guest), guest_url(@guest) %></p>
<p>We hope to see you another time!</p>
<% end %>
<p>&nbsp;</p>
<p><%= t :wedding_couple_names %></p>

View File

@ -0,0 +1,11 @@
<p><%= @guest.email_safe_salutation %></p>
<p>You recently entered your email address to RSVP on <%= ENV['CANONICAL_HOST'] %>.</p>
<p>If you would like to view or update your RSVP, you can use the following link to get back to it:</p>
<p><%= link_to guest_url(@guest), guest_url(@guest) %></p>
<p>If you have any questions, you can ask us by reply.</p>
<p>&nbsp;</p>
<p><%= t :wedding_couple_names %></p>

View File

@ -0,0 +1,9 @@
<div class="form-group row">
<div class="col-md-3">
<%= form.label :diet, 'Dietary Preferences', class: 'col-form-label' %>
</div>
<div class="col-md-9">
<%= form.text_area :diet, id: "#{id_stem}_diet", class: 'form-control' %>
<small class="form-text text-muted">For example, vegan, vegetarian, gluten free, and any allergies or intolerances.</small>
</div>
</div>

View File

@ -0,0 +1,5 @@
<div id="guests-leader" class="jumbotron text-center">
<div class="container">
<h1>RSVP</h1>
</div>
</div>

View File

@ -0,0 +1,19 @@
<div class="form-group row">
<div class="col-md-3">
<%= form.label :first_name, 'First Name', class: 'col-form-label' %>
<span class="wedding-required">*</span>
</div>
<div class="col-md-9">
<%= form.text_field :first_name, id: "#{id_stem}_first_name", class: 'form-control', autofocus: true, required: true %>
</div>
</div>
<div class="form-group row">
<div class="col-md-3">
<%= form.label :last_name, 'Last Name', class: 'col-form-label' %>
<span class="wedding-required">*</span>
</div>
<div class="col-md-9">
<%= form.text_field :last_name, id: "#{id_stem}_last_name", class: 'form-control', required: true %>
</div>
</div>

View File

@ -0,0 +1,21 @@
<nav class="guests-rsvp-nav">
<ul class="nav nav-pills nav-fill">
<li class="nav-item">
<%= link_to '1. About You', guest_path(@guest), class: ['nav-link', active == 1 ? 'active' : ''] %>
</li>
<li class="nav-item">
<% if active >= 2 %>
<%= link_to '2. Plus Ones', guest_plus_ones_path(@guest), class: ['nav-link', active == 2 ? 'active' : ''] %>
<% else %>
<a class="nav-link <%= active == 2 ? 'active' : '' %>">2. Plus Ones</a>
<% end %>
</li>
<li class="nav-item">
<% if active >= 3 %>
<%= link_to '3. Confirm', confirm_guest_path(@guest), class: ['nav-link', active == 3 ? 'active' : ''] %>
<% else %>
<a class="nav-link <%= active == 3 ? 'active' : '' %>">3. Confirm</a>
<% end %>
</li>
</ul>
</nav>

View File

@ -0,0 +1,12 @@
<div class="container">
<div class="row">
<div class="offset-lg-2 col-lg-8">
<%= render partial: 'leader' %>
<h2>Thanks!</h2>
<p>We've sent an email confirmation to <tt><%= @guest.email %></tt>.</p>
<p>If you don't receive the email within a few minutes, please check your spam folder, and if it's not there <%= mail_to ENV['CONTACT_EMAIL'], 'contact us' %>.</p>
<p>The email contains a link that you can use to update your RSVP later if you need to.</p>
<p><%= link_to 'Back to the Home Page', root_path %></p>
</div>
</div>
</div>

View File

@ -0,0 +1,98 @@
<div class="container">
<div class="row">
<div class="offset-lg-2 col-lg-8">
<%= render partial: 'leader' %>
<%= render partial: 'rsvp_nav', locals: { active: 3 } %>
<h2>Confirm</h2>
<%= form_with(model: @guest, url: complete_guest_path(@guest), local: true) do |form| %>
<div class="row">
<div class="col-md-12">
<%= errors_for(@guest) %>
</div>
</div>
<% if @guest.attending? %>
<h3>Your Details</h3>
<div class="card mb-3">
<h5 class="card-header"><%= @guest.name %></h5>
<div class="card-body">
<div class="row">
<div class="col-md-4">
Email
</div>
<div class="col-md-8">
<p><tt><%= @guest.email %></tt></p>
</div>
</div>
<div class="row">
<div class="col-md-4">
Dietary Preferences
</div>
<div class="col-md-8">
<% if @guest.diet.present? %>
<pre><%= @guest.diet %></pre>
<% else %>
<p>(None)</p>
<% end %>
</div>
</div>
<div class="row">
<div class="col-md-4">
Song Suggestions
</div>
<div class="col-md-8">
<% if @guest.songs.present? %>
<pre><%= @guest.songs %></pre>
<% else %>
<p>(None)</p>
<% end %>
</div>
</div>
<div class="row">
<div class="col-md-12 text-right">
<%= link_to 'Edit', guest_path(@guest), class: 'card-link', data: { disable_with: 'Loading...' } %>
</div>
</div>
</div>
</div>
<h3>Plus Ones</h3>
<% if @guest.plus_ones.any? %>
<%= render @guest.plus_ones %>
<% else %>
<p>(None &mdash; just you.)</p>
<% end %>
<% else %>
<p>Sorry to hear you won't be joining us!</p>
<h3>Your Details</h3>
<div class="card mb-3">
<h5 class="card-header"><%= @guest.name %></h5>
<div class="card-body">
<div class="row">
<div class="col-md-4">
Email
</div>
<div class="col-md-8">
<p><tt><%= @guest.email %></tt></p>
</div>
</div>
</div>
</div>
<% end %>
<h3>Other Comments?</h3>
<%= form.text_area :notes, class: 'form-control' %>
<small class="form-text text-muted">Anything else you'd like to let us know?</small>
<div class="row guests-buttons">
<div class="offset-md-3 col-md-6 text-center">
<%= form.submit 'Complete RSVP', class: 'btn btn-primary', data: { disable_with: 'Sending...' } %>
</div>
</div>
<% end %>
</div>
</div>
</div>

View File

@ -0,0 +1,30 @@
<div class="container">
<div class="row">
<div class="offset-lg-2 col-lg-8">
<%= render partial: 'leader' %>
<p>Please enter your email address to begin. We'll send you an email with important information once you have RSVP'd.</p>
<%= form_with scope: :guest, url: guests_path, local: true do |form| %>
<div class="row">
<div class="col-md-12">
<%= errors_for(@guest) %>
</div>
</div>
<div class="form-group row">
<div class="col-md-3">
<%= form.label :email, class: 'col-form-label' %>
</div>
<div class="col-md-9">
<%= form.email_field :email, id: 'guest_email', class: 'form-control', autofocus: true, required: true %>
</div>
</div>
<div class="row guests-buttons">
<div class="offset-md-3 col-md-6 text-center">
<%= invisible_recaptcha_tags text: 'Continue', class: 'btn btn-primary', data: { disable_with: 'Sending...' } %>
</div>
</div>
<% end %>
</div>
</div>
</div>

View File

@ -0,0 +1,11 @@
<div class="container">
<div class="row">
<div class="offset-lg-2 col-lg-8">
<%= render partial: 'leader' %>
<h2>Welcome Back</h2>
<p>It looks like you've already registered your RSVP, so we've sent you an email with a link that you can use to update it.</p>
<p>Please check your <tt><%= @guest.email %></tt> email including spam folders!</p>
<p>If you don't receive the email, please <%= mail_to ENV['CONTACT_EMAIL'], 'contact us' %>.</p>
</div>
</div>
</div>

View File

@ -0,0 +1,67 @@
<div class="container">
<div class="row">
<div class="offset-lg-2 col-lg-8">
<%= render partial: 'leader' %>
<%= render partial: 'rsvp_nav', locals: { active: 1 } %>
<h2>About You</h2>
<%= form_with(model: @guest, method: :patch, local: true, id: 'guest-edit-form') do |form| %>
<div class="row">
<div class="col-md-12">
<%= errors_for(@guest) %>
</div>
</div>
<div class="form-group row">
<div class="col-md-3">
<%= form.label :email, class: 'col-form-label' %>
<span class="wedding-required">*</span>
</div>
<div class="col-md-9">
<%= form.email_field :email, id: 'guest_email', class: 'form-control', required: true %>
</div>
</div>
<%= render partial: 'name', locals: { form: form, id_stem: 'guest' } %>
<div class="form-group row">
<div class="offset-md-3 col-md-9">
<p><strong>Will you be joining us?</strong></p>
<div class="form-check form-check-inline">
<%= form.radio_button :attending, 'true', checked: true, id: 'guest_attending_true', class: 'form-check-input' %>
<%= form.label :attending, 'Yes', value: 'true', class: 'form-check-label' %>
</div>
<div class="form-check form-check-inline">
<%= form.radio_button :attending, 'false', id: 'guest_attending_false', class: 'form-check-input' %>
<%= form.label :attending, 'No', value: 'false', class: 'form-check-label' %>
</div>
</div>
</div>
<div class="guest-attending-only">
<%= render partial: 'dietary_preferences', locals: { form: form, id_stem: 'guest' } %>
<div class="form-group row">
<div class="col-md-3">
<%= form.label :songs, 'Song Suggestions', class: 'col-form-label' %>
</div>
<div class="col-md-9">
<%= form.text_area :songs, id: 'guest_songs', class: 'form-control' %>
<small class="form-text text-muted">We're crowd sourcing our playlist! Choose some songs you'd like to hear.</small>
</div>
</div>
</div>
<div class="row guests-buttons">
<div class="col-md-3">
<span class="wedding-required">* Required</span>
</div>
<div class="col-md-6 text-center">
<%= form.submit 'Continue', class: 'btn btn-primary', data: { disable_with: 'Sending...' } %>
</div>
</div>
<% end %>
</div>
</div>
</div>

View File

@ -0,0 +1,21 @@
<!DOCTYPE html>
<html>
<head>
<title><%= t :wedding_name %></title>
<%= csrf_meta_tags %>
<%= stylesheet_link_tag 'application', media: 'all' %>
<%= javascript_include_tag 'application' %>
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="robots" content="noindex, nofollow">
</head>
<body>
<% flash.each do |key, value| %>
<%= content_tag(:div, class: flash_class(key)) do -%>
<%= value %>
<% end -%>
<% end %>
<%= yield %>
</body>
</html>

View File

@ -0,0 +1,10 @@
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
</head>
<body>
<%= yield %>
</body>
</html>

View File

@ -0,0 +1 @@
<%= yield %>

View File

@ -0,0 +1,32 @@
<%= form_with(model: [@guest, @plus_one], local: true) do |form| %>
<div class="row">
<div class="col-md-12">
<%= errors_for(@plus_one) %>
</div>
</div>
<%= render partial: 'guests/name', locals: { form: form, id_stem: 'plus_one' } %>
<%= render partial: 'guests/dietary_preferences', locals: { form: form, id_stem: 'plus_one' } %>
<div class="form-group row">
<div class="offset-md-3 col-md-9">
<div class="form-check form-check-inline">
<%= form.check_box :child, id: 'plus_one_child', class: 'form-check-input' %>
<%= form.label :child, 'Child (12 or under)', class: 'form-check-label' %>
</div>
</div>
</div>
<div class="row guests-buttons">
<div class="col-md-3">
<span class="wedding-required">* Required</span>
</div>
<div class="col-md-6 text-center">
<%= form.submit submit_action, class: 'btn btn-primary', data: { disable_with: 'Sending...' } %>
</div>
<div class="col-md-3 text-right">
<%= link_to 'Cancel', guest_plus_ones_path(@guest), class: 'btn btn-secondary' %>
</div>
</div>
<% end %>

View File

@ -0,0 +1,31 @@
<div class="card mb-3">
<h5 class="card-header"><%= plus_one.name %></h5>
<div class="card-body">
<div class="row">
<div class="col-md-4">
Dietary Preferences
</div>
<div class="col-md-8">
<% if plus_one.diet.present? %>
<pre><%= plus_one.diet %></pre>
<% else %>
<p>(None)</p>
<% end %>
</div>
</div>
<div class="row">
<div class="col-md-4">
Child
</div>
<div class="col-md-8">
<p><%= plus_one.child ? 'Yes' : 'No' %></p>
</div>
</div>
<div class="row">
<div class="col-md-12 text-right">
<%= link_to 'Edit', edit_guest_plus_one_path(plus_one.guest, plus_one), class: 'card-link', data: { disable_with: 'Loading...' } %>
<%= link_to 'Remove', guest_plus_one_path(plus_one.guest, plus_one), method: :delete, class: 'card-link', data: { disable_with: 'Removing...', confirm: 'Are you sure?' } %>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,11 @@
<div class="container">
<div class="row">
<div class="offset-lg-2 col-lg-8">
<%= render partial: 'guests/leader' %>
<%= render partial: 'guests/rsvp_nav', locals: { active: 2 } %>
<h2>Edit Plus One</h2>
<%= render partial: 'form', locals: { submit_action: 'Save Plus One' } %>
</div>
</div>
</div>

View File

@ -0,0 +1,26 @@
<div class="container">
<div class="row">
<div class="offset-lg-2 col-lg-8">
<%= render partial: 'guests/leader' %>
<%= render partial: 'guests/rsvp_nav', locals: { active: 2 } %>
<h2>Plus Ones</h2>
<p>Please let us know below if you're bringing someone else to the wedding. Children are also very welcome.
<p>&nbsp;</p>
<% if @plus_ones.any? %>
<%= render @plus_ones %>
<% else %>
<p>No plus ones added yet. Flying solo.</p>
<% end %>
<p>
<%= link_to 'Add Plus One', new_guest_plus_one_path(@guest), class: 'btn btn-primary', data: { disable_with: 'Loading...' } %>
</p>
<p>&nbsp;</p>
<p class="guests-buttons">
<%= link_to 'Continue', confirm_guest_path(@guest), class: 'btn btn-primary' %>
</p>
</div>
</div>
</div>

View File

@ -0,0 +1,11 @@
<div class="container">
<div class="row">
<div class="offset-lg-2 col-lg-8">
<%= render partial: 'guests/leader' %>
<%= render partial: 'guests/rsvp_nav', locals: { active: 2 } %>
<h2>Add Plus One</h2>
<%= render partial: 'form', locals: { submit_action: 'Add Plus One' } %>
</div>
</div>
</div>

View File

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="0 0 8 8">
<path d="M0 0v2h7v-2h-7zm0 3v4.91c0 .05.04.09.09.09h6.81c.05 0 .09-.04.09-.09v-4.91h-7zm1 1h1v1h-1v-1zm2 0h1v1h-1v-1zm2 0h1v1h-1v-1zm-4 2h1v1h-1v-1zm2 0h1v1h-1v-1z" />
</svg>

After

Width:  |  Height:  |  Size: 263 B

View File

@ -0,0 +1,14 @@
<footer id="welcome-index-footer" class="page-footer">
<div class="container-fluid">
<div class="row">
<div id="welcome-index-footer-cta" class="col-md-12 text-center">
<%= link_to 'RSVP TODAY!', new_guest_path, class: 'btn btn-primary wedding-cta-btn' %>
</div>
</div>
<div class="row">
<div class="col-md-12 text-center" id="welcome-index-footer-credits">
<small><% t :photo_credits %></small>
</div>
</div>
</div>
</footer>

View File

@ -0,0 +1,11 @@
<section id="welcome-index-leader" class="jumbotron text-center">
<div class="container">
<h1 class="jumbotron-heading"><%= t :wedding_couple_names %></h1>
<p class="leader-date">
<%= t :wedding_date %>, <%= t :wedding_location %>
</p>
<p>
<%= link_to 'RSVP TODAY!', new_guest_path, class: 'btn btn-primary wedding-cta-btn' %>
</p>
</div>
</section>

View File

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="0 0 8 8">
<path d="M3 0c-1.66 0-3 1.34-3 3 0 2 3 5 3 5s3-3 3-5c0-1.66-1.34-3-3-3zm0 1c1.11 0 2 .9 2 2 0 1.11-.89 2-2 2-1.1 0-2-.89-2-2 0-1.1.9-2 2-2z" transform="translate(1)" />
</svg>

After

Width:  |  Height:  |  Size: 264 B

View File

@ -0,0 +1,40 @@
<section id="welcome-index-wedding-plan-leader" class="jumbotron text-center">
<div class="container">
<h1>The Plan</h1>
</div>
</section>
<section id="welcome-index-wedding-plan">
<div class="container">
<div class="row">
<div class="offset-md-3 col-md-6">
<h2>Order of the Day</h2>
<p><strong>12:30pm</strong> &mdash; Guests arrive</p>
<p><strong>1pm</strong> &mdash; Wedding service</p>
<p><strong>1:30pm</strong> &mdash; Cocktails, photos, confetti and canapes</p>
<p><strong>3pm-6pm</strong> &mdash; Food and drink</p>
<p><strong>7pm</strong> &mdash; Cutting of the cake! Drinking, dancing!</p>
<p><strong>12am</strong> &mdash; Bar closes</p>
<p>There will be a tab for the evening, followed by a cash bar.</p>
<p><strong>Dress code</strong> is warm.</p>
<h2>Gifts &amp; Cards</h2>
<p>We'd be pleased if you made a donation to charity rather than bringing a gift. Our goal is to raise £1,000 for <%= link_to 'Cool Earth', @cool_earth_url %>, the non-profit that works alongside rainforest communities to halt deforestation and climate change.</p>
<p class="text-center"><%= link_to 'Donate to Cool Earth', @cool_earth_url, class: 'btn btn-primary wedding-cta-btn' %></p>
<p>If you'd like to send us a card, we'll send you our mailing address by email once you RSVP.</p>
<h2>Menu</h2>
<p><strong>Starter</strong> &mdash; Home Cured Salmon Gravadlax with Horseradish Cream &amp; Beetroot Tart <sup>*</sup> &dagger;</p>
<p><strong>Main</strong> &mdash; Fricassee of Chicken with Lyonnaise Potato &ddagger;</p>
<p><strong>Dessert</strong> &mdash; Choux Swan with Chocolate & Brandy Sauce <sup>&sect;</sup></p>
<p><strong>Children's Menu</strong>: Garlic bread, chicken goujons and chips, ice cream.</p>
<ul class="list-unstyled">
<li><small><sup>*</sup> Gluten free &amp; vegan starter: Tomato soup</small></li>
<li><small>&dagger; Gluten free &amp; dairy free starter: Gravadlax served without tart</small></li>
<li><small>&ddagger; Main is gluten free. Vegan main: Aubergine &amp; Black Bean Chilli with Cumin Rice</small></li>
<li><small><sup>&sect;</sup> Vegan, gluten free &amp; dairy free dessert: Fruit platter</small></li>
</ul>
<p>The venue is making every effort to cater for vegan, vegetarian, gluten free, allergies and other dietary preferences, as indicated in your RSVPs.</p>
<h2>RSVPs</h2>
<p>Please RSVP as soon as possible, so we know who is coming and can make sure we keep a seat and a piece of cake for you if you can come! If it turns out you cant make it, please let us know as soon as you can.</p>
</div>
</div>
</div>
</section>

View File

@ -0,0 +1,19 @@
<section id="welcome-index-venue-leader" class="jumbotron text-center">
<div class="container">
<h1>The Venue</h1>
</div>
</section>
<section id="welcome-index-venue">
<div class="container">
<div class="row">
<div class="offset-md-3 col-md-6">
<h2>How do I get there?</h2>
<p>Ships sail regularly from Ushuaia, Argentina. Allow two days.</p>
<p>There is ample snow mobile parking. Watch out for penguins.</p>
<h2>Coming from out of town?</h2>
<p>The station has a number of rooms that you can get at the <strong>discounted wedding rate</strong>.</p>
<p>Check in is from 3pm, so you may not be able to get ready in your room if you book it just for the night of the wedding.</p>
</div>
</div>
</div>
</section>

View File

@ -0,0 +1,33 @@
<section id="welcome-index-when-where">
<div class="container">
<div class="row">
<div class="col-md-12 text-center">
<h1>When &amp; Where</h1>
</div>
</div>
<div class="row">
<div class="col-md-6 text-center">
<h2><%= link_to 'wedding.ics' do -%>
<%= render partial: 'calendar_svg' %>
<%- end %></h2>
<p>
<strong>12:30pm &ndash; late</strong><br>
<%= t :wedding_date %><br>
&nbsp;<br>
</p>
<p><%= link_to 'Add to Calendar', 'wedding.ics' %></p>
</div>
<div class="col-md-6 text-center">
<h2><%= link_to @google_maps_url do -%>
<%= render partial: 'map_marker_svg' %>
<%- end %></h2>
<p>
<%= render partial: 'wedding_address' %>
</p>
<p>
<%= link_to 'Google Maps', @google_maps_url %>
</p>
</div>
</div>
</div>
</section>

View File

@ -0,0 +1,7 @@
<main role="main">
<%= render partial: 'jumbo' %>
<%= render partial: 'when_where' %>
<%= render partial: 'venue' %>
<%= render partial: 'plan' %>
</main>
<%= render partial: 'footer' %>