diff --git a/app/controllers/adventures_controller.rb b/app/controllers/adventures_controller.rb
index 752ce98c..f3de55d6 100644
--- a/app/controllers/adventures_controller.rb
+++ b/app/controllers/adventures_controller.rb
@@ -138,6 +138,11 @@ def set_adventure
@adventure = Adventure.find_by_slug!(params[:id])
end
+ def custom_theme
+ @adventure.custom_theme
+ end
+ helper_method :custom_theme
+
def adventure_params
params.require(:adventure).permit(
:title,
@@ -152,7 +157,8 @@ def adventure_params
:back_button,
:show_source,
:character_card,
- :archived
+ :archived,
+ :custom_theme_id
)
end
diff --git a/app/controllers/custom_themes_controller.rb b/app/controllers/custom_themes_controller.rb
new file mode 100644
index 00000000..6998a8e6
--- /dev/null
+++ b/app/controllers/custom_themes_controller.rb
@@ -0,0 +1,105 @@
+class CustomThemesController < ApplicationController
+
+ def index
+ if !current_user
+ flash[:alert] = "You must be logged in to view your custom themes."
+ return redirect_to new_user_session_path
+ end
+
+ @custom_themes = current_user.custom_themes
+ end
+
+ def show
+ if custom_theme.user != current_user
+ flash[:alert] = "You can't view that custom theme."
+ return redirect_to root_url
+ end
+
+ @adventure = Adventure.find_by_slug!('lemonade-stand')
+ render layout: 'custom_theme_preview'
+ end
+
+ def new
+ if !current_user
+ flash[:alert] = "You must be logged in to create a custom theme."
+ return redirect_to new_user_session_path
+ end
+
+ @custom_theme = CustomTheme.new
+ end
+
+ def create
+ if !current_user
+ flash[:alert] = "You must be logged in to create a custom theme."
+ return redirect_to new_user_session_path
+ end
+
+ @custom_theme = CustomTheme.new(custom_theme_params)
+ custom_theme.user = current_user
+
+ if custom_theme.save
+ redirect_to custom_theme
+ else
+ display_errors(:create)
+ render :new
+ end
+ end
+
+ def edit
+ if custom_theme.user != current_user
+ flash[:alert] = "You can't modify that custom theme."
+ return redirect_to root_url
+ end
+ end
+
+ def update
+ if custom_theme.user != current_user
+ flash[:alert] = "You can't modify that custom theme."
+ return redirect_to root_url
+ end
+
+ if custom_theme.update(custom_theme_params)
+ redirect_to custom_theme_path(custom_theme)
+ else
+ display_errors(:update)
+ render :edit
+ end
+ end
+
+ private
+
+ def custom_theme
+ @custom_theme ||= CustomTheme.find_by_slug!(params[:id])
+ end
+ helper_method :custom_theme
+
+ def custom_theme_params
+ params.require(:custom_theme).permit(
+ :title,
+ :background_color,
+ :border_color,
+ :intro_image,
+ :scene_image,
+ :end_image,
+ :header_font_family,
+ :header_font_color,
+ :body_font_family,
+ :body_font_color,
+ :choice_font_family,
+ :choice_font_color,
+ :button_color,
+ :button_font_family,
+ :button_font_color
+ )
+ end
+
+ def display_errors(method)
+ # the slug validation technically "runs" on create, even though it is not a field
+ if method == :create
+ custom_theme.errors.delete(:slug)
+ end
+ if custom_theme.errors.any?
+ flash[:alert] = custom_theme.errors.full_messages.join(", ")
+ end
+ end
+end
diff --git a/app/javascript/packs/adventure-form.tsx b/app/javascript/packs/adventure-form.tsx
new file mode 100644
index 00000000..eaaf620a
--- /dev/null
+++ b/app/javascript/packs/adventure-form.tsx
@@ -0,0 +1,12 @@
+const customThemeSection = document.getElementById("custom_theme_section");
+const themeField = (document.getElementById("adventure_theme")) as HTMLSelectElement;
+
+if (themeField && customThemeSection) {
+ themeField.addEventListener("change", function() {
+ if (themeField.value == "custom") {
+ customThemeSection.classList.remove("no-display");
+ } else {
+ customThemeSection.classList.add("no-display");
+ }
+ });
+}
diff --git a/app/javascript/packs/application.tsx b/app/javascript/packs/application.tsx
index c961cdac..1ea88ec2 100644
--- a/app/javascript/packs/application.tsx
+++ b/app/javascript/packs/application.tsx
@@ -4,6 +4,7 @@ import '../src/button.css'
import '../src/modal.css'
import '../src/css/default/AccountForm.css'
import '../src/css/default/AdventureList.css'
+import '../src/css/default/CustomThemeForm.css'
import '../src/css/default/StoryIndex.css'
import '../src/css/default/ThemeDark.css'
import '../src/css/default/FormattingHelp.css'
diff --git a/app/javascript/packs/custom-theme-form.tsx b/app/javascript/packs/custom-theme-form.tsx
new file mode 100644
index 00000000..2e1eacb3
--- /dev/null
+++ b/app/javascript/packs/custom-theme-form.tsx
@@ -0,0 +1,28 @@
+const headerFontField = (document.getElementById("custom_theme_header_font_family")) as HTMLSelectElement;
+const bodyFontField = (document.getElementById("custom_theme_body_font_family")) as HTMLSelectElement;
+const choiceFontField = (document.getElementById("custom_theme_choice_font_family")) as HTMLSelectElement;
+const buttonFontField = (document.getElementById("custom_theme_button_font_family")) as HTMLSelectElement;
+
+if (headerFontField) {
+ headerFontField.addEventListener("change", function() {
+ headerFontField.style.fontFamily = headerFontField.value;
+ });
+}
+
+if (bodyFontField) {
+ bodyFontField.addEventListener("change", function() {
+ bodyFontField.style.fontFamily = bodyFontField.value;
+ });
+}
+
+if (choiceFontField) {
+ choiceFontField.addEventListener("change", function() {
+ choiceFontField.style.fontFamily = choiceFontField.value;
+ });
+}
+
+if (buttonFontField) {
+ buttonFontField.addEventListener("change", function() {
+ buttonFontField.style.fontFamily = buttonFontField.value;
+ });
+}
diff --git a/app/javascript/packs/custom-theme-preview.tsx b/app/javascript/packs/custom-theme-preview.tsx
new file mode 100644
index 00000000..f5dd4d78
--- /dev/null
+++ b/app/javascript/packs/custom-theme-preview.tsx
@@ -0,0 +1,40 @@
+// Important: Order matters here. Otherwise our flow chart's layout
+// calculations fail
+
+import '../src/global.css'
+import '../src/button.css'
+import '../src/modal.css'
+
+import * as React from 'react'
+import * as ReactDOM from 'react-dom'
+
+import Player from '../src/components/Player'
+import { load } from '../src/persistance'
+
+const slug = SEED.slug
+
+async function render() {
+ const content = await load(slug)
+ const customThemePreview = document.getElementById('custom-theme-preview')
+
+ if (content) {
+ ReactDOM.render(
+ ,
+ customThemePreview
+ )
+ }
+}
+
+render()
diff --git a/app/javascript/src/components/themes/DesertTheme.css b/app/javascript/src/components/themes/DesertTheme.css
index af52e72a..c751d029 100644
--- a/app/javascript/src/components/themes/DesertTheme.css
+++ b/app/javascript/src/components/themes/DesertTheme.css
@@ -44,24 +44,36 @@ body:after {
}
.DesertTheme .PlayerIntro {
- background: url('../../images/desert-theme-intro-background.jpg') 0 0 no-repeat /
- cover;
+ background-image: url('../../images/desert-theme-intro-background.jpg');
+ background-position: 0 0;
+ background-repeat: no-repeat;
+ background-size: cover;
+ margin-top: 0;
+ margin-bottom: 0;
}
.DesertTheme .PlayerScene {
- background: url('../../images/desert-theme-scene-background.jpg') 0 0 no-repeat /
- cover;
+ background-image: url('../../images/desert-theme-scene-background.jpg');
+ background-position: 0 0;
+ background-repeat: no-repeat;
+ background-size: cover;
+ margin-top: 0;
+ margin-bottom: 30px;
}
.DesertTheme .PlayerEnd {
- background: url('../../images/desert-theme-end-background.jpg') 50% 50% no-repeat /
- cover;
+ background-image: url('../../images/desert-theme-end-background.jpg');
+ background-position: 50% 50%;
+ background-repeat: no-repeat;
+ background-size: cover;
+ margin-top: 0;
+ margin-bottom: 0;
text-align: center;
}
.DesertTheme .PlayerForeground {
max-width: 1184px;
- margin: 0 auto;
+ margin: auto auto;
padding: 24px 36px;
width: 100%;
}
diff --git a/app/javascript/src/css/default/AccountForm.css b/app/javascript/src/css/default/AccountForm.css
index 03fd8ada..9e2f94f1 100644
--- a/app/javascript/src/css/default/AccountForm.css
+++ b/app/javascript/src/css/default/AccountForm.css
@@ -69,3 +69,7 @@
display: block;
color: white;
}
+
+.no-display {
+ display: none;
+}
diff --git a/app/javascript/src/css/default/CustomThemeForm.css b/app/javascript/src/css/default/CustomThemeForm.css
new file mode 100644
index 00000000..e3a7debc
--- /dev/null
+++ b/app/javascript/src/css/default/CustomThemeForm.css
@@ -0,0 +1,3 @@
+.CustomThemeForm select {
+ font-size: 20px;
+}
diff --git a/app/models/adventure.rb b/app/models/adventure.rb
index 49a3ba49..6a640006 100644
--- a/app/models/adventure.rb
+++ b/app/models/adventure.rb
@@ -5,7 +5,8 @@ class Adventure < ApplicationRecord
"Light" => "light",
"Viget" => "viget",
"Space" => "space",
- "Desert" => "desert"
+ "Desert" => "desert",
+ "Custom" => "custom",
}
FEATURE_GROUPS = [
@@ -17,6 +18,7 @@ class Adventure < ApplicationRecord
slug_from :slug_source
belongs_to :user, optional: true
+ belongs_to :custom_theme, optional: true
validates :title, :theme, presence: true
@@ -24,6 +26,8 @@ class Adventure < ApplicationRecord
validates :age_limit, numericality: { only_integer: true }, presence: true, if: :has_age_limit
+ validate :custom_theme_set
+
def to_s
title
end
@@ -52,4 +56,10 @@ def editable_by?(current_user)
def slug_source
title
end
+
+ def custom_theme_set
+ if theme == 'custom' && custom_theme.nil?
+ errors.add(:custom_theme, "required when theme is set to 'Custom'")
+ end
+ end
end
diff --git a/app/models/custom_theme.rb b/app/models/custom_theme.rb
new file mode 100644
index 00000000..8a820824
--- /dev/null
+++ b/app/models/custom_theme.rb
@@ -0,0 +1,73 @@
+class CustomTheme < ApplicationRecord
+ include Sluggable
+
+ FONTS = [
+ ["American Typewriter", "American Typewriter, serif"],
+ ["Andale Mono", "Andale Mono, monospace"],
+ ["Apple Chancery", "Apple Chancery, cursive"],
+ ["Arial", "Arial, sans-serif"],
+ ["Arial Narrow", "Arial Narrow, sans-serif"],
+ ["Avantgarde", "Avantgarde, TeX Gyre Adventor, URW Gothic L, sans-serif"],
+ ["Blippo", "Blippo, fantasy"],
+ ["Bookman", "Bookman, URW Bookman L, serif"],
+ ["Bradley Hand", "Bradley Hand, cursive"],
+ ["Brush Script MT", "Brush Script MT, Brush Script Std, cursive"],
+ ["Chalkduster", "Chalkduster, fantasy"],
+ ["Comic Sans MS", "Comic Sans MS, Comic Sans, cursive"],
+ ["Courier", "Courier, monospace"],
+ ["Courier New", "Courier New, monospace"],
+ ["Cursive", "cursive"],
+ ["DejaVu Sans Mono", "DejaVu Sans Mono, monospace"],
+ ["Didot", "Didot, serif"],
+ ["Fantasy", "Fantasy"],
+ ["FreeMono", "FreeMono, monospace"],
+ ["Georgia", "Georgia, serif"],
+ ["Gill Sans", "Gill Sans, sans-serif"],
+ ["Helvetica", "Helvetica, sans-serif"],
+ ["Impact", "Impact, fantasy"],
+ ["Jazz LET", "Jazz LET, fantasy"],
+ ["Luminari", "Luminari, fantasy"],
+ ["Marker Felt", "Marker Felt, fantasy"],
+ ["Monospace", "monospace"],
+ ["New Century Schoolbook", "New Century Schoolbook, TeX Gyre Schola, serif"],
+ ["Noto Sans", "Noto Sans, sans-serif"],
+ ["OCR A Std", "OCR A Std, monospace"],
+ ["Optima", "Optima, sans-serif"],
+ ["Palatino", "Palatino, URW Palladio L, serif"],
+ ["Sans-Serif", "sans-serif"],
+ ["Serif", "serif"],
+ ["Snell Roundhand", "Snell Roundhand, cursive"],
+ ["Stencil Std", "Stencil Std, fantasy"],
+ ["Times", "Times, Times New Roman, serif"],
+ ["Trattatello", "Trattatello, fantasy"],
+ ["Trebuchet MS", "Trebuchet MS, sans-serif"],
+ ["URW Chancery L", "URW Chancery L, cursive"],
+ ["Verdana", "Verdana, sans-serif"]
+ ]
+
+ has_many :adventures
+
+ slug_from :slug_source
+
+ dragonfly_accessor :intro_image
+ dragonfly_accessor :scene_image
+ dragonfly_accessor :end_image
+
+ belongs_to :user
+
+ validates :title, presence: true
+
+ def to_s
+ title
+ end
+
+ def to_param
+ slug
+ end
+
+ private
+
+ def slug_source
+ title
+ end
+end
diff --git a/app/models/user.rb b/app/models/user.rb
index 873d0a89..4c7a091b 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -3,6 +3,7 @@ class User < ApplicationRecord
:recoverable, :rememberable, :trackable, :validatable
has_many :adventures, dependent: :destroy
+ has_many :custom_themes, dependent: :destroy
def is_admin?
email.in? [
diff --git a/app/views/adventures/show.html.erb b/app/views/adventures/show.html.erb
index 2c4bf2d9..31bc9dcb 100644
--- a/app/views/adventures/show.html.erb
+++ b/app/views/adventures/show.html.erb
@@ -21,3 +21,7 @@
showSource: <%= @adventure.show_source? %>
}
+
+<% if @adventure.theme === 'custom' %>
+ <%= render "shared/custom_styles" %>
+<% end %>
diff --git a/app/views/custom_themes/_form.html.erb b/app/views/custom_themes/_form.html.erb
new file mode 100644
index 00000000..2eb39dfd
--- /dev/null
+++ b/app/views/custom_themes/_form.html.erb
@@ -0,0 +1,107 @@
+<%= javascript_pack_tag "custom-theme-form", async: true, defer: true %>
+
+<%= form_for custom_theme, html: { class: "CustomThemeForm" } do |f| %>
+
+ <%= f.label :title, "Title" %>
+ <%= f.text_field :title %>
+
+
+
+ <%= f.label :background_color, "Background Color" %>
+ <%= f.color_field :background_color %>
+
+
+
+ <%= f.label :border_color, "Border Color" %>
+ <%= f.color_field :border_color %>
+
+
+
+ <%= f.label :intro_image, "Intro Photo Background" %>
+ <% if custom_theme.intro_image %>
+ <%= image_tag custom_theme.intro_image.thumb('400x200#').url %>
+ <% end %>
+ <%= f.file_field :intro_image, accept: "image/gif,image/jpeg,image/png" %>
+
+
+
+ <%= f.label :scene_image, "Scene Photo Background" %>
+ <% if custom_theme.scene_image %>
+ <%= image_tag custom_theme.scene_image.thumb('400x200#').url %>
+ <% end %>
+ <%= f.file_field :scene_image, accept: "image/gif,image/jpeg,image/png" %>
+
+
+
+ <%= f.label :end_image, "Ending Photo Background" %>
+ <% if custom_theme.end_image %>
+ <%= image_tag custom_theme.end_image.thumb('400x200#').url %>
+ <% end %>
+ <%= f.file_field :end_image, accept: "image/gif,image/jpeg,image/png" %>
+
+
+
+ <%= f.label :header_font_family, "Header Font" %>
+ <% if custom_theme.header_font_family %>
+ <%= f.select :header_font_family, options_for_select(CustomTheme::FONTS, custom_theme.header_font_family), { include_blank: true }, { style: "font-family:#{custom_theme.header_font_family}" } %>
+ <% else %>
+ <%= f.select :header_font_family, options_for_select(CustomTheme::FONTS), include_blank: true %>
+ <% end %>
+
+
+
+ <%= f.label :header_font_color, "Header Text Color" %>
+ <%= f.color_field :header_font_color %>
+
+
+
+ <%= f.label :body_font_family, "Body Font" %>
+ <% if custom_theme.header_font_family %>
+ <%= f.select :body_font_family, options_for_select(CustomTheme::FONTS, custom_theme.body_font_family), { include_blank: true }, { style: "font-family:#{custom_theme.body_font_family}" } %>
+ <% else %>
+ <%= f.select :body_font_family, options_for_select(CustomTheme::FONTS), include_blank: true %>
+ <% end %>
+
+
+
+ <%= f.label :body_font_color, "Body Text Color" %>
+ <%= f.color_field :body_font_color %>
+
+
+
+ <%= f.label :choice_font_family, "Choice Font" %>
+ <% if custom_theme.choice_font_family %>
+ <%= f.select :choice_font_family, options_for_select(CustomTheme::FONTS, custom_theme.choice_font_family), { include_blank: true }, { style: "font-family:#{custom_theme.choice_font_family}" } %>
+ <% else %>
+ <%= f.select :choice_font_family, options_for_select(CustomTheme::FONTS), include_blank: true %>
+ <% end %>
+
+
+
+ <%= f.label :choice_font_color, "Choice Text Color" %>
+ <%= f.color_field :choice_font_color %>
+
+
+
+ <%= f.label :button_color, "Button Color" %>
+ <%= f.color_field :button_color %>
+
+
+
+ <%= f.label :button_font_family, "Button Font" %>
+ <% if custom_theme.button_font_family %>
+ <%= f.select :button_font_family, options_for_select(CustomTheme::FONTS, custom_theme.button_font_family), { include_blank: true }, { style: "font-family:#{custom_theme.button_font_family}" } %>
+ <% else %>
+ <%= f.select :button_font_family, options_for_select(CustomTheme::FONTS), include_blank: true %>
+ <% end %>
+
+
+
+ <%= f.label :button_font_color, "Button Text Color" %>
+ <%= f.color_field :button_font_color %>
+
+
+
+
+
+<% end %>
diff --git a/app/views/custom_themes/edit.html.erb b/app/views/custom_themes/edit.html.erb
new file mode 100644
index 00000000..8ae41877
--- /dev/null
+++ b/app/views/custom_themes/edit.html.erb
@@ -0,0 +1,9 @@
+
+
<%= custom_theme %>
+
+ <%= render "form", custom_theme: custom_theme, is_update: true %>
+
+
+
diff --git a/app/views/custom_themes/index.html.erb b/app/views/custom_themes/index.html.erb
new file mode 100644
index 00000000..b4dfb51e
--- /dev/null
+++ b/app/views/custom_themes/index.html.erb
@@ -0,0 +1,31 @@
+
+
My Custom Themes
+
+ <% if @custom_themes.size <= 0 %>
+
You have no custom themes.
+ <% end %>
+
+
+ <%= link_to "Create a Custom Theme", new_custom_theme_path, class: "SlantButton" %>
+
+
+
+ <% @custom_themes.each do |custom_theme| %>
+
+ <%= link_to custom_theme do %>
+
+
+
+ <% end %>
+
+
+ <%= link_to custom_theme_path(custom_theme) do %>
+
+ <%= custom_theme %>
+
+ <% end %>
+
+
+ <% end %>
+
+
diff --git a/app/views/custom_themes/new.html.erb b/app/views/custom_themes/new.html.erb
new file mode 100644
index 00000000..8031f334
--- /dev/null
+++ b/app/views/custom_themes/new.html.erb
@@ -0,0 +1,9 @@
+
+
Create a
Custom Theme
+
+ <%= render "form", custom_theme: custom_theme, is_update: false %>
+
+
+
diff --git a/app/views/custom_themes/show.html.erb b/app/views/custom_themes/show.html.erb
new file mode 100644
index 00000000..f5f91017
--- /dev/null
+++ b/app/views/custom_themes/show.html.erb
@@ -0,0 +1,16 @@
+<% content_for :theme, "CustomTheme" %>
+
+
+ <%= link_to 'Edit', [:edit, custom_theme], class: "SlantButton" %>
+
+
+
+
+
+
+<%= render "shared/custom_styles", custom_theme: custom_theme %>
diff --git a/app/views/layouts/custom_theme_preview.html.erb b/app/views/layouts/custom_theme_preview.html.erb
new file mode 100644
index 00000000..a7a7c859
--- /dev/null
+++ b/app/views/layouts/custom_theme_preview.html.erb
@@ -0,0 +1,12 @@
+
+
+
+ <%= render "shared/head_tags" %>
+
+
+ <%= yield %>
+
+ <%= stylesheet_pack_tag "player" %>
+ <%= javascript_pack_tag "custom-theme-preview", async: true, defer: true %>
+
+
diff --git a/app/views/shared/_adventure_form.html.erb b/app/views/shared/_adventure_form.html.erb
index 52706519..f94be175 100644
--- a/app/views/shared/_adventure_form.html.erb
+++ b/app/views/shared/_adventure_form.html.erb
@@ -1,3 +1,5 @@
+<%= javascript_pack_tag "adventure-form", async: true, defer: true %>
+
<%= form_for @adventure do |f| %>
<%= f.label :title, "What's your title?" %>
@@ -18,7 +20,20 @@
<%= f.label :Theme %>
- <%= f.select :theme, options_for_select(Adventure::THEMES) %>
+ <% if @adventure.theme %>
+ <%= f.select :theme, options_for_select(Adventure::THEMES, @adventure.theme) %>
+ <% else %>
+ <%= f.select :theme, options_for_select(Adventure::THEMES) %>
+ <% end %>
+
+
+
+ <%= f.label :custom_theme_id, "Custom Theme" %>
+ <% if @adventure.custom_theme %>
+ <%= f.select :custom_theme_id, options_for_select(CustomTheme.where(user_id: current_user.id).map { |ct| [ct.title, ct.id] }, @adventure.custom_theme_id)%>
+ <% else %>
+ <%= f.select :custom_theme_id, options_for_select(CustomTheme.where(user_id: current_user.id).map { |ct| [ct.title, ct.id] }) %>
+ <% end %>
<% if correct_user %>
diff --git a/app/views/shared/_custom_styles.html.erb b/app/views/shared/_custom_styles.html.erb
new file mode 100644
index 00000000..3b84f4cd
--- /dev/null
+++ b/app/views/shared/_custom_styles.html.erb
@@ -0,0 +1,171 @@
+
diff --git a/app/views/shared/_header.html.erb b/app/views/shared/_header.html.erb
index 7c95779d..bda9a0fc 100644
--- a/app/views/shared/_header.html.erb
+++ b/app/views/shared/_header.html.erb
@@ -16,6 +16,7 @@ height="0" width="0" style="display:none;visibility:hidden">