initial: something resembling a minimum viable product

This commit is contained in:
Neetpone 2024-04-02 13:33:19 -04:00
commit 563aa6a0f8
130 changed files with 6276 additions and 0 deletions

7
.gitattributes vendored Normal file
View file

@ -0,0 +1,7 @@
# See https://git-scm.com/docs/gitattributes for more about git attribute files.
# Mark the database schema as having been generated.
db/schema.rb linguist-generated
# Mark any vendored files as having been vendored.
vendor/* linguist-vendored

28
.gitignore vendored Normal file
View file

@ -0,0 +1,28 @@
# See https://help.github.com/articles/ignoring-files for more about ignoring files.
#
# If you find yourself ignoring temporary files generated by your text editor
# or operating system, you probably want to add a global ignore instead:
# git config --global core.excludesfile '~/.gitignore_global'
# Ignore bundler config.
/.bundle
# Ignore all logfiles and tempfiles.
/log/*
/tmp/*
!/log/.keep
!/tmp/.keep
# Ignore pidfiles, but keep the directory.
/tmp/pids/*
!/tmp/pids/
!/tmp/pids/.keep
/public/assets
# Ignore master key for decrypting credentials and more.
/config/master.key
public/cached-images

1
.ruby-version Normal file
View file

@ -0,0 +1 @@
ruby-3.2.2

41
Gemfile Normal file
View file

@ -0,0 +1,41 @@
source "https://rubygems.org"
git_source(:github) { |repo| "https://github.com/#{repo}.git" }
ruby "3.2.2"
gem "rails", "~> 7.0.8", ">= 7.0.8.1"
gem "sprockets-rails"
gem "pg", "~> 1.1"
gem "puma", "~> 5.0"
gem 'slim-rails'
gem 'kaminari'
gem 'redis'
gem 'elasticsearch-model'
gem 'model-msearch'
gem 'fancy_searchable', github: 'Twibooru/fancy_searchable', ref: '40687c9'
gem 'sidekiq'
# Windows does not include zoneinfo files, so bundle the tzinfo-data gem
gem "tzinfo-data", platforms: %i[ mingw mswin x64_mingw jruby ]
# Use Sass to process CSS
# gem "sassc-rails"
group :development, :test do
# See https://guides.rubyonrails.org/debugging_rails_applications.html#debugging-with-the-debug-gem
gem "debug", platforms: %i[ mri mingw x64_mingw ]
end
group :development do
gem "web-console"
gem 'annotate'
end
group :test do
# Use system testing [https://guides.rubyonrails.org/testing.html#system-testing]
gem "capybara"
gem "selenium-webdriver"
end

291
Gemfile.lock Normal file
View file

@ -0,0 +1,291 @@
GIT
remote: https://github.com/Twibooru/fancy_searchable.git
revision: 40687c924d19a51335a79f5157150aca9d531cc9
ref: 40687c9
specs:
fancy_searchable (0.2.5)
activesupport (>= 7.0)
elasticsearch-model (>= 7.2.1)
GEM
remote: https://rubygems.org/
specs:
actioncable (7.0.8.1)
actionpack (= 7.0.8.1)
activesupport (= 7.0.8.1)
nio4r (~> 2.0)
websocket-driver (>= 0.6.1)
actionmailbox (7.0.8.1)
actionpack (= 7.0.8.1)
activejob (= 7.0.8.1)
activerecord (= 7.0.8.1)
activestorage (= 7.0.8.1)
activesupport (= 7.0.8.1)
mail (>= 2.7.1)
net-imap
net-pop
net-smtp
actionmailer (7.0.8.1)
actionpack (= 7.0.8.1)
actionview (= 7.0.8.1)
activejob (= 7.0.8.1)
activesupport (= 7.0.8.1)
mail (~> 2.5, >= 2.5.4)
net-imap
net-pop
net-smtp
rails-dom-testing (~> 2.0)
actionpack (7.0.8.1)
actionview (= 7.0.8.1)
activesupport (= 7.0.8.1)
rack (~> 2.0, >= 2.2.4)
rack-test (>= 0.6.3)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.0, >= 1.2.0)
actiontext (7.0.8.1)
actionpack (= 7.0.8.1)
activerecord (= 7.0.8.1)
activestorage (= 7.0.8.1)
activesupport (= 7.0.8.1)
globalid (>= 0.6.0)
nokogiri (>= 1.8.5)
actionview (7.0.8.1)
activesupport (= 7.0.8.1)
builder (~> 3.1)
erubi (~> 1.4)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.1, >= 1.2.0)
activejob (7.0.8.1)
activesupport (= 7.0.8.1)
globalid (>= 0.3.6)
activemodel (7.0.8.1)
activesupport (= 7.0.8.1)
activerecord (7.0.8.1)
activemodel (= 7.0.8.1)
activesupport (= 7.0.8.1)
activestorage (7.0.8.1)
actionpack (= 7.0.8.1)
activejob (= 7.0.8.1)
activerecord (= 7.0.8.1)
activesupport (= 7.0.8.1)
marcel (~> 1.0)
mini_mime (>= 1.1.0)
activesupport (7.0.8.1)
concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (>= 1.6, < 2)
minitest (>= 5.1)
tzinfo (~> 2.0)
addressable (2.8.6)
public_suffix (>= 2.0.2, < 6.0)
annotate (3.2.0)
activerecord (>= 3.2, < 8.0)
rake (>= 10.4, < 14.0)
base64 (0.2.0)
bindex (0.8.1)
builder (3.2.4)
capybara (3.40.0)
addressable
matrix
mini_mime (>= 0.1.3)
nokogiri (~> 1.11)
rack (>= 1.6.0)
rack-test (>= 0.6.3)
regexp_parser (>= 1.5, < 3.0)
xpath (~> 3.2)
concurrent-ruby (1.2.3)
connection_pool (2.4.1)
crass (1.0.6)
date (3.3.4)
debug (1.9.2)
irb (~> 1.10)
reline (>= 0.3.8)
elasticsearch (7.17.10)
elasticsearch-api (= 7.17.10)
elasticsearch-transport (= 7.17.10)
elasticsearch-api (7.17.10)
multi_json
elasticsearch-model (7.2.1)
activesupport (> 3)
elasticsearch (~> 7)
hashie
elasticsearch-transport (7.17.10)
faraday (>= 1, < 3)
multi_json
erubi (1.12.0)
faraday (2.9.0)
faraday-net_http (>= 2.0, < 3.2)
faraday-net_http (3.1.0)
net-http
globalid (1.2.1)
activesupport (>= 6.1)
hashie (5.0.0)
i18n (1.14.4)
concurrent-ruby (~> 1.0)
io-console (0.7.2)
irb (1.12.0)
rdoc
reline (>= 0.4.2)
kaminari (1.2.2)
activesupport (>= 4.1.0)
kaminari-actionview (= 1.2.2)
kaminari-activerecord (= 1.2.2)
kaminari-core (= 1.2.2)
kaminari-actionview (1.2.2)
actionview
kaminari-core (= 1.2.2)
kaminari-activerecord (1.2.2)
activerecord
kaminari-core (= 1.2.2)
kaminari-core (1.2.2)
loofah (2.22.0)
crass (~> 1.0.2)
nokogiri (>= 1.12.0)
mail (2.8.1)
mini_mime (>= 0.1.1)
net-imap
net-pop
net-smtp
marcel (1.0.4)
matrix (0.4.2)
method_source (1.0.0)
mini_mime (1.1.5)
minitest (5.22.3)
model-msearch (0.0.2)
elasticsearch-model
multi_json (1.15.0)
net-http (0.4.1)
uri
net-imap (0.4.10)
date
net-protocol
net-pop (0.1.2)
net-protocol
net-protocol (0.2.2)
timeout
net-smtp (0.5.0)
net-protocol
nio4r (2.7.1)
nokogiri (1.16.3-x86_64-linux)
racc (~> 1.4)
pg (1.5.6)
psych (5.1.2)
stringio
public_suffix (5.0.4)
puma (5.6.8)
nio4r (~> 2.0)
racc (1.7.3)
rack (2.2.9)
rack-test (2.1.0)
rack (>= 1.3)
rails (7.0.8.1)
actioncable (= 7.0.8.1)
actionmailbox (= 7.0.8.1)
actionmailer (= 7.0.8.1)
actionpack (= 7.0.8.1)
actiontext (= 7.0.8.1)
actionview (= 7.0.8.1)
activejob (= 7.0.8.1)
activemodel (= 7.0.8.1)
activerecord (= 7.0.8.1)
activestorage (= 7.0.8.1)
activesupport (= 7.0.8.1)
bundler (>= 1.15.0)
railties (= 7.0.8.1)
rails-dom-testing (2.2.0)
activesupport (>= 5.0.0)
minitest
nokogiri (>= 1.6)
rails-html-sanitizer (1.6.0)
loofah (~> 2.21)
nokogiri (~> 1.14)
railties (7.0.8.1)
actionpack (= 7.0.8.1)
activesupport (= 7.0.8.1)
method_source
rake (>= 12.2)
thor (~> 1.0)
zeitwerk (~> 2.5)
rake (13.1.0)
rdoc (6.6.3.1)
psych (>= 4.0.0)
redis (5.1.0)
redis-client (>= 0.17.0)
redis-client (0.21.1)
connection_pool
regexp_parser (2.9.0)
reline (0.5.0)
io-console (~> 0.5)
rexml (3.2.6)
rubyzip (2.3.2)
selenium-webdriver (4.19.0)
base64 (~> 0.2)
rexml (~> 3.2, >= 3.2.5)
rubyzip (>= 1.2.2, < 3.0)
websocket (~> 1.0)
sidekiq (7.2.2)
concurrent-ruby (< 2)
connection_pool (>= 2.3.0)
rack (>= 2.2.4)
redis-client (>= 0.19.0)
slim (5.2.1)
temple (~> 0.10.0)
tilt (>= 2.1.0)
slim-rails (3.6.3)
actionpack (>= 3.1)
railties (>= 3.1)
slim (>= 3.0, < 6.0, != 5.0.0)
sprockets (4.2.1)
concurrent-ruby (~> 1.0)
rack (>= 2.2.4, < 4)
sprockets-rails (3.4.2)
actionpack (>= 5.2)
activesupport (>= 5.2)
sprockets (>= 3.0.0)
stringio (3.1.0)
temple (0.10.3)
thor (1.3.1)
tilt (2.3.0)
timeout (0.4.1)
tzinfo (2.0.6)
concurrent-ruby (~> 1.0)
uri (0.13.0)
web-console (4.2.1)
actionview (>= 6.0.0)
activemodel (>= 6.0.0)
bindex (>= 0.4.0)
railties (>= 6.0.0)
websocket (1.2.10)
websocket-driver (0.7.6)
websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.5)
xpath (3.2.0)
nokogiri (~> 1.8)
zeitwerk (2.6.13)
PLATFORMS
x86_64-linux
DEPENDENCIES
annotate
capybara
debug
elasticsearch-model
fancy_searchable!
kaminari
model-msearch
pg (~> 1.1)
puma (~> 5.0)
rails (~> 7.0.8, >= 7.0.8.1)
redis
selenium-webdriver
sidekiq
slim-rails
sprockets-rails
tzinfo-data
web-console
RUBY VERSION
ruby 3.2.2p53
BUNDLED WITH
2.4.10

19
LICENSE Normal file
View file

@ -0,0 +1,19 @@
Copyright (c) 2024 Neetpone
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

2
README.md Normal file
View file

@ -0,0 +1,2 @@
# FoalFetch
A replacement for FiMFetch.

6
Rakefile Normal file
View file

@ -0,0 +1,6 @@
# Add your own tasks in files placed in lib/tasks ending in .rake,
# for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.
require_relative "config/application"
Rails.application.load_tasks

View file

@ -0,0 +1,6 @@
//= link_tree ../images
//= link_directory ../stylesheets .css
//= link_tree ../../javascript .js
//= link_tree ../../../vendor/javascript .js
//= link application.js

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

View file

@ -0,0 +1,98 @@
const makeEl = function(html) {
const template = document.createElement('template');
template.innerHTML = html.trim();
return template.content.firstChild;
};
function addTag(element, tag) {
const existingElements = element.value.split(',').filter(n => n);
if (existingElements.indexOf(tag) === -1) {
existingElements.push(tag);
element.value = existingElements.join(',');
}
}
function removeTag(element, tag) {
const existingElements = element.value.split(',').filter(n => n);
const index = existingElements.indexOf(tag);
if (index !== -1) {
existingElements.splice(index, 1);
element.value = existingElements.join(',');
}
}
function makeTagElement(hiddenInput, visualInput, tagName, excluded) {
// Set up the tag element
const tagEl = makeEl(`<div class="${excluded ? 'excluded' : ''}"><span>${tagName}</span></div>`);
tagEl.firstChild.addEventListener('click', function(e) {
if (tagEl.classList.contains('excluded')) {
removeTag(hiddenInput, '-' + tagName);
addTag(hiddenInput, tagName);
tagEl.classList.remove('excluded');
} else {
removeTag(hiddenInput, tagName);
addTag(hiddenInput, '-' + tagName);
tagEl.classList.add('excluded');
}
});
// Set up the child "X" button
const removeEl = makeEl('<a class="remove" />');
removeEl.addEventListener('click', function(e) {
removeTag(hiddenInput, tagEl.classList.contains('excluded') ? '-' + tagName : tagName);
visualInput.removeChild(tagEl);
});
tagEl.appendChild(removeEl);
return tagEl;
}
function setupFancyTagEditor(rootElement) {
rootElement.classList.remove('hidden');
const selectDropdown = rootElement.querySelector('select');
const hiddenInput = rootElement.querySelector('input[type=text]');
const visualInput = rootElement.querySelector('.selected-tags');
if (!selectDropdown || !hiddenInput || !visualInput) {
console.log('Nothing there!');
return;
}
hiddenInput.setAttribute('type', 'hidden');
// Load existing tags from the input field as visual tag elements
for (const tag of hiddenInput.value.split(',').filter(n => n)) {
let tagElement;
if (tag[0] === '-') {
tagElement = makeTagElement(hiddenInput, visualInput, tag.substring(1), true);
} else {
tagElement = makeTagElement(hiddenInput, visualInput, tag, false);
}
visualInput.appendChild(tagElement);
}
selectDropdown.addEventListener('change', function(e) {
const tagName = selectDropdown.value;
addTag(hiddenInput, tagName);
visualInput.appendChild(makeTagElement(hiddenInput, visualInput, tagName, false));
});
}
function whenReady() {
for (const element of document.querySelectorAll('.js-tag-editor')) {
setupFancyTagEditor(element);
}
}
if (document.readyState !== 'loading') {
whenReady();
} else {
document.addEventListener('DOMContentLoaded', whenReady);
}

View file

@ -0,0 +1,151 @@
body.about {
background-color: #fff6ea;
}
.about article {
display: inline-block;
max-width: 800px;
text-align: left;
padding: 0 5px;
line-height: 20px;
font-size: 18px;
margin: 10px 0 0;
}
.about article h1, .about article h2 {
text-align: center;
font-size: 28px;
margin: 5px;
}
.about article h1 {
font-size: 35px;
margin: 15px 0 25px;
}
.about article section {
padding: 10px 15px;
background-color: #fdfdfd;
margin: 0 0 15px;
overflow: auto;
box-shadow: 0 0 5px #a5a5a5 , 0 0 8px #797979;
}
.about p {
margin: 10px 0;
}
.about dl {
line-height: 24px;
}
.about dt {
float: left;
width: 100px;
text-align: right;
font-weight: bold;
font-family: Montserrat, Arial, sans-serif;
color: #187099;
padding: 0 0 10px;
}
.about dd {
margin: 0 0 0 110px;
color: #0F5221;
font-family: Arial, sans-serif;
}
.about dd:after {
content: "";
display: block;
clear: both;
}
.about code {
white-space: pre-wrap;
}
.about pre {
background-color: #eff0f1;
}
.about img.fb-logo {
margin: 15px 0 0 15px;
max-width: 64px;
}
.about .tile {
width: 150px;
height: 180px;
font-size: 10px;
display: inline-block;
overflow: hidden;
padding: 4px;
/*border: 1px solid #333;*/
}
.about .tile img {
max-width: 100%;
}
.about .tiles {
text-align: center;
}
.about .tile figure {
margin: 0;
display: table-cell;
height: 180px;
vertical-align: bottom;
}
.about .tile figcaption {
min-height: 45px;
}
.about .tile figcaption span {
display: inline-block;
margin-left: 4px;
}
.collapsable {
overflow: hidden;
-webkit-transition-delay: 2s;
transition-delay: 2s;
transition: max-height .5s ease-out;
}
.collapsable.full {
transition: max-height .8s ease-in;
max-height:0px;
padding-top: 10px;
}
.about .c {
text-align: center;
}
.about .stars {
display: inline-block;
width: auto;
height: 60px;
}
.about .stars .star {
font-size: 45px;
line-height: 45px;
width: 45px;
height: 45px;
margin: 5px 4px;
}
.about .stars .star:before {
left: 0;
top: 0;
padding: 0;
width: 100%;
height: 100%;
}
.about article .stat {
white-space: pre;
font-family: monospace;
font-size: 17px;
}
.donates h3 {
display: block;
text-align: center;
font-weight: bold;
}
.donates ul.names {
list-style: none;
}
.donates ul.names li {
height: 35px;
font-size: 18px;
display: inline-block;
overflow: hidden;
padding: 4px;
margin: 3px 4px;
}

View file

@ -0,0 +1,50 @@
/*
* This is a manifest file that'll be compiled into application.css, which will include all the files
* listed below.
*
* Any CSS (and SCSS, if configured) 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
* 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.
*
*= require_tree .
*= require_self
*/
.cols {
display: flex;
flex-direction: row;
flex-wrap: nowrap;
}
.opts {
text-align: left;
margin: 0.75rem;
}
.opts > input {
display: block;
}
.opts > label {
display: block;
}
.fake-link {
text-decoration: underline;
cursor: pointer;
}
input#show-advanced + div {
display: none;
}
input#show-advanced:checked + div {
display: block;
}
.search-s > form {
text-align: left;
padding-left: 12rem;
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,4 @@
module ApplicationCable
class Channel < ActionCable::Channel::Base
end
end

View file

@ -0,0 +1,4 @@
module ApplicationCable
class Connection < ActionCable::Connection::Base
end
end

View file

@ -0,0 +1,21 @@
class ApplicationController < ActionController::Base
before_action :start_timer
before_action :setup_pagination_and_tags
private
def start_timer
@start_time = Time.zone.now
end
def setup_pagination_and_tags
@per_page = 15
per_page = params[:per_page]
if per_page
per_page = per_page.to_i
@per_page = per_page if per_page.between? 1, 50
end
@page_num = params[:page].to_i
end
end

View file

@ -0,0 +1,5 @@
class AuthorsController < ApplicationController
def show
@author = Author.find(params[:id])
end
end

View file

@ -0,0 +1,6 @@
class ChaptersController < ApplicationController
def show
@story = Story.find(params[:story_id])
@chapter = @story.chapters.find_by(number: params[:id])
end
end

View file

View file

@ -0,0 +1,28 @@
require 'open-uri'
class ImagesController < ApplicationController
def show
url = params[:url]
parsed = URI.parse(url)
if parsed.host != 'cdn-img.fimfiction.net'
render nothing: true, status: :bad_request
return
end
hash = Digest::SHA256.hexdigest(url)
path = Rails.root.join('public', 'cached-images', hash + File.extname(url))
our_url = '/cached-images/' + hash + File.extname(url)
if File.exist? path
redirect_to our_url
return
end
File.open(path, 'wb') do |fp|
fp.write(Net::HTTP.get(parsed))
end
redirect_to our_url
end
end

View file

@ -0,0 +1,125 @@
# This whole class is a giant mess but I coded it fast so give me a break.
class SearchController < ApplicationController
ALLOWED_SORT_DIRS = [:asc, :desc]
ALLOWED_SORT_FIELDS = [:title, :author, :date_published, :date_updated, :num_words, :rel]
before_action :load_tags
def index
@search_params = {}
end
def search
unless setup_scope
return
end
using_random = @search_params['luck'].present?
# This was mainly written this way to match the way FiMFetch's query interface looks, without using JS.
# I should do a Derpibooru-esque textual search system sometime.
@search = Story.fancy_search(per_page: @per_page,
page: @page_num) do |s|
s.add_query match: { title: { query: @search_params['q'], operator: 'AND' } } unless @search_params['q'].blank?
s.add_query match: { author: { query: @search_params['author'], operator: 'AND' } } if @search_params['author'].present?
# ratings -> match stories with any of them
s.add_filter bool: {
should: @search_params['ratings'].keys.map { |k| { term: { content_rating: k } } }
} unless @search_params['ratings'].blank?
# completeness -> match stories with any of them
s.add_filter bool: {
should: @search_params['state'].keys.map { |k| { term: { completion_status: k } } }
} unless @search_params['state'].blank?
# tags -> match any of the included tags, exclude any of the excluded taags
tag_musts, tag_must_nots = parse_tag_queries
s.add_filter terms: {
tags: tag_musts
} if tag_musts.any?
s.add_filter bool: {
must_not: tag_must_nots.map { |t| { term: { tags: t } } }
} if tag_must_nots.any?
# sort direction
if using_random
s.add_sort _random: :desc
else
s.add_sort parse_sort
end
end
if using_random && @search.total_count > 0
redirect_to story_path(@search.records[0])
return
end
@records = @search.records
end
private
def load_tags
@character_tags = Tag.where(type: 'character').pluck(:name)
@other_tags = Tag.where.not(type: 'character').pluck(:name)
end
# returns: [included tags, excluded tags]
def parse_tag_queries
tag_searches = (@search_params['tags'] + ',' + @search_params['characters']).split(',').reject &:blank?
[tag_searches.select { |t| t[0] != '-' }, tag_searches.select { |t| t[0] == '-' }]
end
def parse_sort
sf = ALLOWED_SORT_FIELDS.detect { |f| @search_params['sf'] == f.to_s } || :date_updated
sd = ALLOWED_SORT_DIRS.detect { |d| @search_params['sd'] == d.to_s } || :desc
sf = case sf
when :rel then
:_score
else
sf
end
{sf => sd}
end
# FIXME: This is some of the worst Ruby code I have ever written.
def setup_scope
@scope_key = Random.hex(16)
scope_valid = false
# scope passed, try to look it up in redis and use the search params from it
if params[:scope].present?
result = $redis.get("search_scope/#{params[:scope]}")
if result.present?
@search_params = JSON.load(result)
@scope_key = params[:scope]
scope_valid = true
else
redirect_to '/'
return false
end
else
@search_params = params
end
# you can't JSON.dump a Parameters
if @search_params.is_a? ActionController::Parameters
@search_params = @search_params.permit!.to_h
end
$redis.setex("search_scope/#{@scope_key}", 3600, JSON.dump(@search_params))
# if the scope was invalid (no key passed, or invalid key passed), redirect to the results page
# with the new key we just generated for the params we had.
unless scope_valid
redirect_to "/search?scope=#{@scope_key}"
return false
end
true
end
end

View file

@ -0,0 +1,4 @@
class StaticPagesController < ApplicationController
def about
end
end

View file

@ -0,0 +1,9 @@
class StoriesController < ApplicationController
def index
end
def show
@story = Story.find(params[:id])
@chapters = @story.chapters.order(number: :asc)
end
end

View file

@ -0,0 +1,15 @@
module ApplicationHelper
# https://infusion.media/content-marketing/how-to-calculate-reading-time/
def reading_time(words)
minutes = words / 200.0
distance_of_time_in_words(minutes * 60.0)
end
def render_time
diff = ((Time.zone.now - @start_time) * 1000.0).round(2)
diff < 1000 ? "#{diff}ms" : "#{(diff / 1000.0).round(2)}s"
rescue StandardError
'unknown ms'
end
end

View file

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

View file

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

View file

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

View file

@ -0,0 +1,47 @@
module SearchHelper
def rating_display(rating)
case rating
when 'everyone' then
tag.div class: 'frating ev', title: 'Rated Everyone' do
'Everyone'
end
when 'teen' then
tag.div class: 'frating', title: 'Rated Teen' do
'Teen'
end
when 'explicit' then
tag.div class: 'frating', title: 'Rated Explicit' do
'Explicit'
end
end
end
def status_display(status)
case status
when 'complete' then
tag.div class: 'fstatus sc', title: 'Complete' do
'Complete'
end
when 'incomplete' then
tag.div class: 'fstatus si', title: 'Incomplete' do
'Incomplete'
end
when 'cancelled' then
tag.div class: 'fstatus sn', title: 'Cancelled' do
'Cancelled'
end
end
end
def tag_to_html(t)
tag.div class: 'ftag', title: t.name do
t.name
end
end
def character_tag(t)
tag.div class: 'ftag', title: t.name do
t.name
end
end
end

View file

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

View file

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

View file

@ -0,0 +1,61 @@
module StoryIndex
def self.included(base)
base.settings index: { number_of_shards: 5, max_result_window: 10_000_000 } do
mappings dynamic: false do
indexes :id, type: 'integer'
indexes :author_id, type: 'keyword'
indexes :completion_status, type: 'keyword'
indexes :content_rating, type: 'keyword'
indexes :date_published, type: 'date'
indexes :date_updated, type: 'date'
indexes :date_modified, type: 'date'
indexes :num_comments, type: 'integer'
indexes :num_views, type: 'integer'
indexes :num_words, type: 'integer'
indexes :rating, type: 'integer'
indexes :short_description, type: 'text', analyzer: 'snowball'
indexes :description_html, type: 'text', analyzer: 'snowball'
indexes :title, type: 'text', analyzer: 'snowball'
indexes :author, type: 'text', analyzer: 'snowball'
indexes :tags, type: 'keyword'
end
end
base.extend ClassMethods
end
module ClassMethods
def default_sort(_options = {})
[date_published: :desc]
end
def allowed_search_fields(access_options = {})
[:title, :completion_status, :content_rating, :date_published, :date_updated, :date_modified, :num_comments, :num_views, :num_words, :rating, :short_description, :description_html, :title, :tags, :author]
end
end
def as_json(*)
{
id: id,
author_id: author.id,
completion_status: completion_status,
content_rating: content_rating,
date_published: date_published,
date_updated: date_updated,
date_modified: date_modified,
num_comments: num_comments,
num_views: num_views,
num_words: num_words,
rating: rating,
short_description: short_description,
description_html: description_html,
title: title,
tags: tags.map(&:name),
author: author.name
}
end
def as_indexed_json(*)
as_json
end
end

View file

@ -0,0 +1,7 @@
class ApplicationJob < ActiveJob::Base
# Automatically retry jobs that encountered a deadlock
# retry_on ActiveRecord::Deadlocked
# Most jobs are safe to ignore if the underlying records are no longer available
# discard_on ActiveJob::DeserializationError
end

View file

@ -0,0 +1,11 @@
# frozen_string_literal: true
class IndexUpdateJob < ApplicationJob
queue_as :high
def perform(cls, id)
obj = cls.constantize.find(id)
obj.update_index(defer: false) if obj
rescue StandardError => ex
Rails.logger.error ex.message
end
end

View file

@ -0,0 +1,3 @@
class ApplicationRecord < ActiveRecord::Base
primary_abstract_class
end

3
app/models/author.rb Normal file
View file

@ -0,0 +1,3 @@
class Author < ApplicationRecord
has_many :stories
end

3
app/models/chapter.rb Normal file
View file

@ -0,0 +1,3 @@
class Chapter < ApplicationRecord
belongs_to :story
end

View file

View file

@ -0,0 +1,34 @@
# frozen_string_literal: true
require 'elasticsearch/model'
module Indexable
extend ActiveSupport::Concern
included do
include Elasticsearch::Model
include "#{name}Index".constantize
after_commit(on: :create) do
__elasticsearch__.index_document
end
after_commit(on: :destroy) do
__elasticsearch__.delete_document
end
end
def update_index(defer: true, priority: :high)
if defer
if priority == :high
IndexUpdateJob.perform_later(self.class.to_s, id)
elsif priority == :rebuild
IndexRebuildJob.perform_later(self.class.to_s, id)
else
raise ArgumentError, 'No such priority known'
end
else
__elasticsearch__.index_document
end
end
end

9
app/models/story.rb Normal file
View file

@ -0,0 +1,9 @@
class Story < ApplicationRecord
include FancySearchable::Searchable
include Indexable
belongs_to :author
has_many :chapters
has_many :taggings, validate: false
has_many :tags, through: :taggings, validate: false
end

View file

@ -0,0 +1,6 @@
class Story::Tagging < ApplicationRecord
belongs_to :story
belongs_to :tag
validates :tag, uniqueness: { scope: [:story_id] }
end

2
app/models/tag.rb Normal file
View file

@ -0,0 +1,2 @@
class Tag < ApplicationRecord
end

View file

@ -0,0 +1,29 @@
body.story
#wrap
header.banner.main
= link_to '/'
= image_tag '/img/banner.png'
article#story.fic-data.hyphenate
header
h1
= link_to @story.title, story_path(@story)
span.author
' by
= link_to @story.author.name, author_path(@story.author)
hr
h2= @chapter.title
span.chapnav.prev
- if @chapter.number == 1
= link_to 'Load Full Story', story_chapter_path(@story, 0)
- else
= link_to 'Previous Chapter', story_chapter_path(@story, @chapter.number - 1)
span.chapnav.next
- if @chapter.number < @story.chapters.count
= link_to 'Next Chapter', story_chapter_path(@story, @chapter.number + 1)
hr
== @chapter.body
span.chapnav.bot
- if @chapter.number < @story.chapters.count
= link_to 'Next Chapter', story_chapter_path(@story, @chapter.number + 1)

View file

@ -0,0 +1,8 @@
/ Link to the "First" page
- available local variables
url : url to the first page
current_page : a page object for the currently displayed page
total_pages : total number of pages
per_page : number of items to fetch per page
remote : data-remote
= link_to_unless current_page.first?, '« First', url, remote: remote

View file

@ -0,0 +1,7 @@
/ Non-link tag that stands for skipped pages...
- available local variables
current_page : a page object for the currently displayed page
total_pages : total number of pages
per_page : number of items to fetch per page
remote : data-remote
span.page.gap …

View file

@ -0,0 +1,8 @@
/ Link to the "Last" page
- available local variables
url : url to the last page
current_page : a page object for the currently displayed page
total_pages : total number of pages
per_page : number of items to fetch per page
remote : data-remote
= link_to_unless current_page.last?, 'Last »', url, remote: remote

View file

@ -0,0 +1,8 @@
/ Link to the "Next" page
- available local variables
url : url to the next page
current_page : a page object for the currently displayed page
total_pages : total number of pages
per_page : number of items to fetch per page
remote : data-remote
= link_to_unless current_page.last?, 'Next ', url, rel: 'next', remote: remote, title: 'Next Page (k)', class: 'js-next'

View file

@ -0,0 +1,12 @@
/ Link showing page number
- available local variables
page : a page object for "this" page
url : url to this page
current_page : a page object for the currently displayed page
total_pages : total number of pages
per_page : number of items to fetch per page
remote : data-remote
- if page.current?
span class="page-current" = page
- else
= link_to page, url, remote: remote, rel: page.rel, class: 'page'

View file

@ -0,0 +1,18 @@
/ The container tag
- available local variables
current_page : a page object for the currently displayed page
total_pages : total number of pages
per_page : number of items to fetch per page
remote : data-remote
paginator : the paginator that renders the pagination tags inside
== paginator.render do
nav.pagination
==> first_page_tag unless current_page.first?
==> prev_page_tag unless current_page.first?
- each_page do |page|
- if page.display_tag?
==> page_tag page
- elsif !page.was_truncated?
==> gap_tag
==> next_page_tag unless current_page.last?
== last_page_tag unless current_page.last?

View file

@ -0,0 +1,8 @@
/ Link to the "Previous" page
- available local variables
url : url to the previous page
current_page : a page object for the currently displayed page
total_pages : total number of pages
per_page : number of items to fetch per page
remote : data-remote
= link_to_unless current_page.first?, ' Prev', url, rel: 'prev', remote: remote, title: 'Previous Page (j)', class: 'js-prev'

View file

@ -0,0 +1,3 @@
header.banner.main
= link_to '/'
= image_tag '/img/banner.png'

View file

@ -0,0 +1,35 @@
doctype html
html
head
- title_ext = yield :title
title= title_ext.present? ? "#{title_ext} - FoalFetch" : "FoalFetch - The Uncensored FiM Fiction Archive"
meta charset="utf-8"
meta name="viewport" content="width=device-width,initial-scale=1"
= csrf_meta_tags
= csp_meta_tag
= stylesheet_link_tag 'application'
= javascript_include_tag 'application'
link href="//netdna.bootstrapcdn.com/font-awesome/4.0.3/css/font-awesome.min.css" rel="stylesheet"
body
= yield
footer
div
section
h4 Stats
span.stat Page generated in #{render_time}
/ span.stat FIXME registered users
/ span.stat FIXME visitors total
section
h4 FoalFetch
span.stat Frontend designed by DataByte, backend coded by Floorb.
span.stat
a href="/about" About FoalFetch
span.stat.disclaimer
' This site and its contents are not affiliated with Hasbro, Inc or FiMFetch.
| My Little Pony: Friendship is Magic and all its characters are trademarks of Hasbro, Inc.
section
h4 Friends
span.stat
a href="https://twibooru.org" Twibooru
span.stat
a href="https://ponepaste.org" PonePaste

View file

@ -0,0 +1,127 @@
label.fake-link.search-adv-link for="show-advanced"
| [Toggle Advanced Options]
input type="checkbox" id="show-advanced" style="display: none;"
div.search-adv
.cols
small More options coming soon!
.cols
.opts
b Rating
br
= label_tag "ratings_everyone"
= check_box_tag "ratings[everyone]", 1, @search_params.dig('ratings', 'everyone').present?
| Everyone
= label_tag "ratings_teen"
= check_box_tag "ratings[teen]", 1, @search_params.dig('ratings', 'teen').present?
| Teen
= label_tag "ratings_mature"
= check_box_tag "ratings[mature]", 1, @search_params.dig('ratings', 'mature').present?
| Mature
.opts
b Story State
br
= label_tag "state_complete"
= check_box_tag "state[complete]", 1, @search_params.dig('state', 'complete').present?
| Complete
= label_tag "state_incomplete"
= check_box_tag "state[incomplete]", 1, @search_params.dig('state', 'incomplete').present?
| Incomplete
= label_tag "state_on_hiatus"
= check_box_tag "state[on_hiatus]", 1, @search_params.dig('state', 'on_hiatus').present?
| On Hiatus
= label_tag "state_cancelled"
= check_box_tag "state[cancelled]", 1, @search_params.dig('state', 'cancelled').present?
| Cancelled
/.opts
b Story Age
br
= label_tag
= radio_button_tag 'ac', 'lt', checked: 'checked'
| Newer Than
= label_tag
= radio_button_tag 'ac', 'gt'
| Older Than
= select_tag :age, options_for_select({ \
'30 days' => 30, '90 days' => 90, '180 days' => 180, '1 year' => 365, \
'2 years' => 730, '3 years' => 1095, '4 years' => 1460, '5 years' => 1825, \
'6 years' => 2190, '7 years' => 2555, '8 years' => 2920, '9 years' => 3285, '10 years' => 3650})
/.opts
b Removed Stories
br
= select_tag "removed", options_for_select({'Include' => 1, 'Exclude' => 0, 'Only' => 'o'})
.cols
/.opts
b Likes:
br
= label_tag
= radio_button_tag 'lc', 'gt', checked: 'checked'
| More Than
= label_tag
= radio_button_tag 'lc', 'lt'
| Less Than
= number_field_tag 'likes'
/.opts
b Words:
br
= label_tag
= radio_button_tag 'wc', 'gt', checked: 'checked'
| More Than
= label_tag
= radio_button_tag 'wc', 'lt'
| Less Than
= number_field_tag 'words'
.opts
b Author:
br
= text_field_tag :author, @search_params['author']
/.cols
.opts
b FiMFiction Rating:
br
= label_tag
= radio_button_tag 'rc', 'gt', checked: 'checked'
| More Than
= label_tag
= radio_button_tag 'rc', 'lt'
| Less Than
div.stars-sm
= radio_button_tag :stars, 5, true, class: 'star star-5'
= label_tag 'star_5', '', class: 'star'
= radio_button_tag :stars, 4, true, class: 'star star-4'
= label_tag 'star_4', '', class: 'star'
= radio_button_tag :stars, 3, true, class: 'star star-3'
= label_tag 'star_3', '', class: 'star'
= radio_button_tag :stars, 2, true, class: 'star star-2'
= label_tag 'star_2', '', class: 'star'
= radio_button_tag :stars, 1, true, class: 'star star-1'
= label_tag 'star_1', '', class: 'star'
.cols
.opts
b Characters
br
noscript
p Please enable JavaScript if you wish to have a more friendly tag searching experience. Otherwise, ignore the dropdown and separate exact tag names by commas in the field.
.js-tag-editor
.selected-tags
= text_field_tag :characters, @search_params['characters']
= select_tag :fancy_tags, options_for_select(@character_tags)
.cols
.opts
b Tags
br
noscript
p Please enable JavaScript if you wish to have a more friendly tag searching experience. Otherwise, ignore the dropdown and separate exact tag names by commas in the field.
.js-tag-editor
.selected-tags
= text_field_tag :tags, @search_params['tags']
= select_tag :fancy_characters, options_for_select(@other_tags)
.cols
.opts
b Sort By
br
=> select_tag :sf, options_for_select({'Query Relevance' => 'rel', 'Title' => 'title', 'Author' => 'author', \
'Publish Date' => 'date_published', 'Updated Date' => 'date_updated', 'Word Count' => 'num_words'}, @search_params['sf'])
= select_tag :sd, options_for_select({'High to Low' => 'desc', 'Low to High' => 'asc'}, @search_params['sd'])
- if show_button
.buttons
= submit_tag 'Go Fetch!', name: 'search'

View file

@ -0,0 +1,15 @@
.main
#wrap
nav.home
= link_to "Daily Fictions", "/ficoftheday"
= link_to "News", "/news"
= link_to "About", "/about"
section.search.search-l
img alt="FoalFetch" src="/img/banner.png"
= form_tag "/search", method: :post
.searchbox
= search_field_tag "q", nil, placeholder: 'Search story titles...'
= render partial: 'advanced', locals: { show_button: false }
.buttons
= submit_tag 'Go Fetch!', name: 'search'
= submit_tag 'Pick one for me!', name: 'luck'

View file

@ -0,0 +1,58 @@
.main
#wrap
section.search.search-s
= form_tag '/search', method: :post
a.logo href="/"
= image_tag '/img/logo_small.png'
.searchbox
= search_field_tag 'q', @search_params['q'], placeholder: 'Search story titles...'
= render partial: 'advanced', locals: { show_button: true }
- @records.each do |rec|
- character_tags = rec.tags.where(type: 'character')
- normal_tags = rec.tags.where.not(type: 'character')
section.result
section.fic-cell
header
- if rec.cover_image
= image_tag '/images?url=' + CGI.escape(rec.cover_image), alt: 'Cover Image'
.details
h2= link_to rec.title, story_path(rec)
span.author
' by
= link_to rec.author.name, author_path(rec.author)
br
span.popular
span.stats
p.description= rec.short_description
footer
.rating
= rating_display(rec.rating)
= status_display(rec.completion_status)
- normal_tags.each do |t|
= tag_to_html(t)
.characters
- character_tags.each do |t|
= character_display(t)
br
.ficstats
span.chapters
= pluralize(rec.chapters.count, 'Chapter') + ','
'&nbsp;
span.words
=<> rec.num_words
' Words:&nbsp;
span.time_est
' Estimated
=> reading_time(rec.num_words)
| to read
.published
' Published
span= rec.date_published
.updated
' Last Update
span= rec.date_updated
.searchnav
= paginate(@records, params: { scope: @scope_key })
br
span.searchtime Found #{@search.total_count} results.

View file

@ -0,0 +1,25 @@
- content_for :title, 'About'
.about
#wrap
= render partial: 'layouts/banner'
nav.home
= link_to 'Home', '/'
= link_to 'News', '/news'
= link_to 'About', '/about', class: 'current'
article
section
h1 About FoalFetch
p FoalFetch is a <b>uncensored</b> archive of My Little Pony: Friendship is Magic fan fiction works. Born after
DataByte began censoring FiMFetch, the project provides an ever-growing list of features:
ul
li Archive of active and past works of fiction
li Easy-to-use granular search features
li Random daily fictions
li Multiple downloadable formats
p And some long-term goals:
ul
li Scrape multiple fanfiction platforms for MLP:FiM stories
li An OPDS browsing service for compatible e-Readers
p This site has been designed from the ground-up in an attempt to provide the simplest and most streamlined access to all the pony you should ever want - even content the site's operators don't agree with.

View file

View file

@ -0,0 +1,52 @@
- content_for :title, @story.title
div.story
div#wrap
= render partial: 'layouts/banner'
section.fic-cell.large
header
- if @story.cover_image
a
= image_tag '/images?url=' + CGI.escape(@story.cover_image), alt: 'Cover Image'
div.details
h2= @story.title
span.author
' by
= link_to @story.author.name, author_path(@story.author)
div.desc_short
= @story.short_description
span.popular
/ likes, etc
section.description#desc
p
== @story.description_html
section
.rating
=> rating_display(@story.content_rating)
=> status_display(@story.completion_status)
'&nbsp;
.ficstats
span.words> #{@story.num_words} words:
span.time_est
' Estimated
=> reading_time(@story.num_words)
| to read
span.cached
.dl-links
span Download:
.chapterlist
h3= pluralize(@chapters.count, 'Chapter') + ':'
ol
- @chapters.each do |c|
li
span.chapter_title
=> link_to c.title, story_chapter_path(@story, c.number)
span.date= c.date_published
.word_count= c.num_words
footer
.published
' Published
span= @story.date_published
.updated
' Last Update
span= @story.date_modified

109
bin/bundle Executable file
View file

@ -0,0 +1,109 @@
#!/usr/bin/env ruby
# frozen_string_literal: true
#
# This file was generated by Bundler.
#
# The application 'bundle' is installed as part of a gem, and
# this file is here to facilitate running it.
#
require "rubygems"
m = Module.new do
module_function
def invoked_as_script?
File.expand_path($0) == File.expand_path(__FILE__)
end
def env_var_version
ENV["BUNDLER_VERSION"]
end
def cli_arg_version
return unless invoked_as_script? # don't want to hijack other binstubs
return unless "update".start_with?(ARGV.first || " ") # must be running `bundle update`
bundler_version = nil
update_index = nil
ARGV.each_with_index do |a, i|
if update_index && update_index.succ == i && a =~ Gem::Version::ANCHORED_VERSION_PATTERN
bundler_version = a
end
next unless a =~ /\A--bundler(?:[= ](#{Gem::Version::VERSION_PATTERN}))?\z/
bundler_version = $1
update_index = i
end
bundler_version
end
def gemfile
gemfile = ENV["BUNDLE_GEMFILE"]
return gemfile if gemfile && !gemfile.empty?
File.expand_path("../Gemfile", __dir__)
end
def lockfile
lockfile =
case File.basename(gemfile)
when "gems.rb" then gemfile.sub(/\.rb$/, ".locked")
else "#{gemfile}.lock"
end
File.expand_path(lockfile)
end
def lockfile_version
return unless File.file?(lockfile)
lockfile_contents = File.read(lockfile)
return unless lockfile_contents =~ /\n\nBUNDLED WITH\n\s{2,}(#{Gem::Version::VERSION_PATTERN})\n/
Regexp.last_match(1)
end
def bundler_requirement
@bundler_requirement ||=
env_var_version ||
cli_arg_version ||
bundler_requirement_for(lockfile_version)
end
def bundler_requirement_for(version)
return "#{Gem::Requirement.default}.a" unless version
bundler_gem_version = Gem::Version.new(version)
bundler_gem_version.approximate_recommendation
end
def load_bundler!
ENV["BUNDLE_GEMFILE"] ||= gemfile
activate_bundler
end
def activate_bundler
gem_error = activation_error_handling do
gem "bundler", bundler_requirement
end
return if gem_error.nil?
require_error = activation_error_handling do
require "bundler/version"
end
return if require_error.nil? && Gem::Requirement.new(bundler_requirement).satisfied_by?(Gem::Version.new(Bundler::VERSION))
warn "Activating bundler (#{bundler_requirement}) failed:\n#{gem_error.message}\n\nTo install the version of bundler this project requires, run `gem install bundler -v '#{bundler_requirement}'`"
exit 42
end
def activation_error_handling
yield
nil
rescue StandardError, LoadError => e
e
end
end
m.load_bundler!
if m.invoked_as_script?
load Gem.bin_path("bundler", "bundle")
end

4
bin/importmap Executable file
View file

@ -0,0 +1,4 @@
#!/usr/bin/env ruby
require_relative "../config/application"
require "importmap/commands"

4
bin/rails Executable file
View file

@ -0,0 +1,4 @@
#!/usr/bin/env ruby
APP_PATH = File.expand_path("../config/application", __dir__)
require_relative "../config/boot"
require "rails/commands"

4
bin/rake Executable file
View file

@ -0,0 +1,4 @@
#!/usr/bin/env ruby
require_relative "../config/boot"
require "rake"
Rake.application.run

33
bin/setup Executable file
View file

@ -0,0 +1,33 @@
#!/usr/bin/env ruby
require "fileutils"
# path to your application root.
APP_ROOT = File.expand_path("..", __dir__)
def system!(*args)
system(*args) || abort("\n== Command #{args} failed ==")
end
FileUtils.chdir APP_ROOT do
# This script is a way to set up or update your development environment automatically.
# This script is idempotent, so that you can run it at any time and get an expectable outcome.
# Add necessary setup steps to this file.
puts "== Installing dependencies =="
system! "gem install bundler --conservative"
system("bundle check") || system!("bundle install")
# puts "\n== Copying sample files =="
# unless File.exist?("config/database.yml")
# FileUtils.cp "config/database.yml.sample", "config/database.yml"
# end
puts "\n== Preparing database =="
system! "bin/rails db:prepare"
puts "\n== Removing old logs and tempfiles =="
system! "bin/rails log:clear tmp:clear"
puts "\n== Restarting application server =="
system! "bin/rails restart"
end

6
config.ru Normal file
View file

@ -0,0 +1,6 @@
# This file is used by Rack-based servers to start the application.
require_relative "config/environment"
run Rails.application
Rails.application.load_server

34
config/application.rb Normal file
View file

@ -0,0 +1,34 @@
require_relative "boot"
require "rails"
# Pick the frameworks you want:
require "active_model/railtie"
require "active_job/railtie"
require "active_record/railtie"
# require "active_storage/engine"
require "action_controller/railtie"
# require "action_mailer/railtie"
# require "action_mailbox/engine"
# require "action_text/engine"
require "action_view/railtie"
require "action_cable/engine"
require "rails/test_unit/railtie"
# Require the gems listed in Gemfile, including any gems
# you've limited to :test, :development, or :production.
Bundler.require(*Rails.groups)
module Foalfetch
class Application < Rails::Application
# Initialize configuration defaults for originally generated Rails version.
config.load_defaults 7.0
# Configuration for the application, engines, and railties goes here.
#
# These settings can be overridden in specific environments using the files
# in config/environments, which are processed later.
#
# config.time_zone = "Central Time (US & Canada)"
# config.eager_load_paths << Rails.root.join("extras")
end
end

3
config/boot.rb Normal file
View file

@ -0,0 +1,3 @@
ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__)
require "bundler/setup" # Set up gems listed in the Gemfile.

11
config/cable.yml Normal file
View file

@ -0,0 +1,11 @@
development:
adapter: redis
url: redis://localhost:6379/1
test:
adapter: test
production:
adapter: redis
url: <%= ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" } %>
channel_prefix: foalfetch_production

View file

@ -0,0 +1 @@
tL8+fuPHdTfbSgNCZVdXNdQ+wKg+87PpLyl5vgFSiqX1lg0WBNv43li9xM4kUU4Zj7YmjS49yqsyOFMhrW+g31Hh+cLZ3fx97XPVV97tu6/l8Dg9Ornldm6+wxg5zpOCrQyvF66DFouH9wMgw0iaEM3wWsvZjFDtsaa/kpgpPqGNUWhMFF6RNnbLeuGN22sm3d6FKEOFa0Ekl+YnKoxnbywshyG3+mAgpLAMt73u62KjiZsgIOku81k44WKDFLqqIeQdEumrWD8IxGFVIVPPm8xzGYq+K5LnIIIF0cw5coEVHZGU904fVvrVxmb0sMU5ZmozM6e98D86SpAQYj6LLFGbKFcZFBgiLknlLnkXDtwIN6chSUNT9lQmSqN4cL8byDwXI2X9hjugIcV7Qn/nGRJ1b4tOrWV/tWo/--CmilwP4O5Ch/FIvK--JD2c8xj2FnJJsSsRAU8HoQ==

86
config/database.yml Normal file
View file

@ -0,0 +1,86 @@
# PostgreSQL. Versions 9.3 and up are supported.
#
# Install the pg driver:
# gem install pg
# On macOS with Homebrew:
# gem install pg -- --with-pg-config=/usr/local/bin/pg_config
# On macOS with MacPorts:
# gem install pg -- --with-pg-config=/opt/local/lib/postgresql84/bin/pg_config
# On Windows:
# gem install pg
# Choose the win32 build.
# Install PostgreSQL and put its /bin directory on your path.
#
# Configure Using Gemfile
# gem "pg"
#
default: &default
adapter: postgresql
encoding: unicode
# For details on connection pooling, see Rails configuration guide
# https://guides.rubyonrails.org/configuring.html#database-pooling
pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
development:
<<: *default
database: foalfetch_development
# The specified database role being used to connect to postgres.
# To create additional roles in postgres see `$ createuser --help`.
# When left blank, postgres will use the default role. This is
# the same name as the operating system user running Rails.
#username: foalfetch
# The password associated with the postgres role (username).
#password:
# Connect on a TCP socket. Omitted by default since the client uses a
# domain socket that doesn't need configuration. Windows does not have
# domain sockets, so uncomment these lines.
#host: localhost
# The TCP port the server listens on. Defaults to 5432.
# If your server runs on a different port number, change accordingly.
#port: 5432
# Schema search path. The server defaults to $user,public
#schema_search_path: myapp,sharedapp,public
# Minimum log levels, in increasing order:
# debug5, debug4, debug3, debug2, debug1,
# log, notice, warning, error, fatal, and panic
# Defaults to warning.
#min_messages: notice
# Warning: The database defined as "test" will be erased and
# re-generated from your development database when you run "rake".
# Do not set this db to the same as development or production.
test:
<<: *default
database: foalfetch_test
# As with config/credentials.yml, you never want to store sensitive information,
# like your database password, in your source code. If your source code is
# ever seen by anyone, they now have access to your database.
#
# Instead, provide the password or a full connection URL as an environment
# variable when you boot the app. For example:
#
# DATABASE_URL="postgres://myuser:mypass@localhost/somedatabase"
#
# If the connection URL is provided in the special DATABASE_URL environment
# variable, Rails will automatically merge its configuration values on top of
# the values provided in this file. Alternatively, you can specify a connection
# URL environment variable explicitly:
#
# production:
# url: <%= ENV["MY_APP_DATABASE_URL"] %>
#
# Read https://guides.rubyonrails.org/configuring.html#configuring-a-database
# for a full overview on how database connection configuration can be specified.
#
production:
<<: *default
database: foalfetch_production
username: foalfetch
password: <%= ENV["FOALFETCH_DATABASE_PASSWORD"] %>

5
config/environment.rb Normal file
View file

@ -0,0 +1,5 @@
# Load the Rails application.
require_relative "application"
# Initialize the Rails application.
Rails.application.initialize!

View file

@ -0,0 +1,62 @@
require "active_support/core_ext/integer/time"
Rails.application.configure do
# Settings specified here will take precedence over those in config/application.rb.
# In the development environment your application's code is reloaded any time
# it changes. This slows down response time but is perfect for development
# since you don't have to restart the web server when you make code changes.
config.cache_classes = false
# Do not eager load code on boot.
config.eager_load = false
# Show full error reports.
config.consider_all_requests_local = true
# Enable server timing
config.server_timing = true
# Enable/disable caching. By default caching is disabled.
# Run rails dev:cache to toggle caching.
if Rails.root.join("tmp/caching-dev.txt").exist?
config.action_controller.perform_caching = true
config.action_controller.enable_fragment_cache_logging = true
config.cache_store = :memory_store
config.public_file_server.headers = {
"Cache-Control" => "public, max-age=#{2.days.to_i}"
}
else
config.action_controller.perform_caching = false
config.cache_store = :null_store
end
# Print deprecation notices to the Rails logger.
config.active_support.deprecation = :log
# Raise exceptions for disallowed deprecations.
config.active_support.disallowed_deprecation = :raise
# Tell Active Support which deprecation messages to disallow.
config.active_support.disallowed_deprecation_warnings = []
# Raise an error on page load if there are pending migrations.
config.active_record.migration_error = :page_load
# Highlight code that triggered database queries in logs.
config.active_record.verbose_query_logs = true
# Suppress logger output for asset requests.
config.assets.quiet = true
# Raises error for missing translations.
# config.i18n.raise_on_missing_translations = true
# Annotate rendered view with file names.
# config.action_view.annotate_rendered_view_with_filenames = true
# Uncomment if you wish to allow Action Cable access from any origin.
# config.action_cable.disable_request_forgery_protection = true
end

View file

@ -0,0 +1,84 @@
require "active_support/core_ext/integer/time"
Rails.application.configure do
# Settings specified here will take precedence over those in config/application.rb.
# Code is not reloaded between requests.
config.cache_classes = true
# Eager load code on boot. This eager loads most of Rails and
# your application in memory, allowing both threaded web servers
# and those relying on copy on write to perform better.
# Rake tasks automatically ignore this option for performance.
config.eager_load = true
# Full error reports are disabled and caching is turned on.
config.consider_all_requests_local = false
config.action_controller.perform_caching = true
# Ensures that a master key has been made available in either ENV["RAILS_MASTER_KEY"]
# or in config/master.key. This key is used to decrypt credentials (and other encrypted files).
# config.require_master_key = true
# Disable serving static files from the `/public` folder by default since
# Apache or NGINX already handles this.
config.public_file_server.enabled = ENV["RAILS_SERVE_STATIC_FILES"].present?
# Compress CSS using a preprocessor.
# config.assets.css_compressor = :sass
# Do not fallback to assets pipeline if a precompiled asset is missed.
config.assets.compile = false
# Enable serving of images, stylesheets, and JavaScripts from an asset server.
# config.asset_host = "http://assets.example.com"
# Specifies the header that your server uses for sending files.
# config.action_dispatch.x_sendfile_header = "X-Sendfile" # for Apache
# config.action_dispatch.x_sendfile_header = "X-Accel-Redirect" # for NGINX
# Mount Action Cable outside main process or domain.
# config.action_cable.mount_path = nil
# config.action_cable.url = "wss://example.com/cable"
# config.action_cable.allowed_request_origins = [ "http://example.com", /http:\/\/example.*/ ]
# Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies.
# config.force_ssl = true
# Include generic and useful information about system operation, but avoid logging too much
# information to avoid inadvertent exposure of personally identifiable information (PII).
config.log_level = :info
# Prepend all log lines with the following tags.
config.log_tags = [ :request_id ]
# Use a different cache store in production.
# config.cache_store = :mem_cache_store
# Use a real queuing backend for Active Job (and separate queues per environment).
# config.active_job.queue_adapter = :resque
# config.active_job.queue_name_prefix = "foalfetch_production"
# Enable locale fallbacks for I18n (makes lookups for any locale fall back to
# the I18n.default_locale when a translation cannot be found).
config.i18n.fallbacks = true
# Don't log any deprecations.
config.active_support.report_deprecations = false
# Use default logging formatter so that PID and timestamp are not suppressed.
config.log_formatter = ::Logger::Formatter.new
# Use a different logger for distributed setups.
# require "syslog/logger"
# config.logger = ActiveSupport::TaggedLogging.new(Syslog::Logger.new "app-name")
if ENV["RAILS_LOG_TO_STDOUT"].present?
logger = ActiveSupport::Logger.new(STDOUT)
logger.formatter = config.log_formatter
config.logger = ActiveSupport::TaggedLogging.new(logger)
end
# Do not dump schema after migrations.
config.active_record.dump_schema_after_migration = false
end

View file

@ -0,0 +1,50 @@
require "active_support/core_ext/integer/time"
# The test environment is used exclusively to run your application's
# test suite. You never need to work with it otherwise. Remember that
# your test database is "scratch space" for the test suite and is wiped
# and recreated between test runs. Don't rely on the data there!
Rails.application.configure do
# Settings specified here will take precedence over those in config/application.rb.
# Turn false under Spring and add config.action_view.cache_template_loading = true.
config.cache_classes = true
# Eager loading loads your whole application. When running a single test locally,
# this probably isn't necessary. It's a good idea to do in a continuous integration
# system, or in some way before deploying your code.
config.eager_load = ENV["CI"].present?
# Configure public file server for tests with Cache-Control for performance.
config.public_file_server.enabled = true
config.public_file_server.headers = {
"Cache-Control" => "public, max-age=#{1.hour.to_i}"
}
# Show full error reports and disable caching.
config.consider_all_requests_local = true
config.action_controller.perform_caching = false
config.cache_store = :null_store
# Raise exceptions instead of rendering exception templates.
config.action_dispatch.show_exceptions = false
# Disable request forgery protection in test environment.
config.action_controller.allow_forgery_protection = false
# Print deprecation notices to the stderr.
config.active_support.deprecation = :stderr
# Raise exceptions for disallowed deprecations.
config.active_support.disallowed_deprecation = :raise
# Tell Active Support which deprecation messages to disallow.
config.active_support.disallowed_deprecation_warnings = []
# Raises error for missing translations.
# config.i18n.raise_on_missing_translations = true
# Annotate rendered view with file names.
# config.action_view.annotate_rendered_view_with_filenames = true
end

7
config/importmap.rb Normal file
View file

@ -0,0 +1,7 @@
# Pin npm packages by running ./bin/importmap
pin "application"
pin "@hotwired/turbo-rails", to: "turbo.min.js"
pin "@hotwired/stimulus", to: "stimulus.min.js"
pin "@hotwired/stimulus-loading", to: "stimulus-loading.js"
pin_all_from "app/javascript/controllers", under: "controllers"

View file

@ -0,0 +1,12 @@
# Be sure to restart your server when you modify this file.
# Version of your assets, change this if you want to expire all your assets.
Rails.application.config.assets.version = "1.0"
# Add additional assets to the asset load path.
# Rails.application.config.assets.paths << Emoji.images_path
# Precompile additional assets.
# application.js, application.css, and all non-JS/CSS in the app/assets
# folder are already added.
# Rails.application.config.assets.precompile += %w( admin.js admin.css )

View file

@ -0,0 +1,25 @@
# Be sure to restart your server when you modify this file.
# Define an application-wide content security policy.
# See the Securing Rails Applications Guide for more information:
# https://guides.rubyonrails.org/security.html#content-security-policy-header
# Rails.application.configure do
# config.content_security_policy do |policy|
# policy.default_src :self, :https
# policy.font_src :self, :https, :data
# policy.img_src :self, :https, :data
# policy.object_src :none
# policy.script_src :self, :https
# policy.style_src :self, :https
# # Specify URI for violation reports
# # policy.report_uri "/csp-violation-report-endpoint"
# end
#
# # Generate session nonces for permitted importmap and inline scripts
# config.content_security_policy_nonce_generator = ->(request) { request.session.id.to_s }
# config.content_security_policy_nonce_directives = %w(script-src)
#
# # Report violations without enforcing the policy.
# # config.content_security_policy_report_only = true
# end

View file

@ -0,0 +1,17 @@
# frozen_string_literal: true
Elasticsearch::Model.client = Elasticsearch::Client.new(
host: 'https://127.0.0.1:9200',
http: {
user: 'elastic',
password: 'FrLV=CCE56dYsK*87jEo'
},
request_timeout: 30
) do |f|
f.ssl[:verify] = false
end
# Prevents this message from appearing in logs:
# You are setting a key that conflicts with a built-in method Hashie::Mash#key defined in Hash.
# This can cause unexpected behavior when accessing the key via as a property.
# You can still access the key via the #[] method.
Hashie.logger = Logger.new(nil)

View file

@ -0,0 +1,8 @@
# Be sure to restart your server when you modify this file.
# Configure parameters to be filtered from the log file. Use this to limit dissemination of
# sensitive information. See the ActiveSupport::ParameterFilter documentation for supported
# notations and behaviors.
Rails.application.config.filter_parameters += [
:passw, :secret, :token, :_key, :crypt, :salt, :certificate, :otp, :ssn
]

View file

@ -0,0 +1,16 @@
# Be sure to restart your server when you modify this file.
# Add new inflection rules using the following format. Inflections
# are locale specific, and you may define rules for as many different
# locales as you wish. All of these examples are active by default:
# ActiveSupport::Inflector.inflections(:en) do |inflect|
# inflect.plural /^(ox)$/i, "\\1en"
# inflect.singular /^(ox)en/i, "\\1"
# inflect.irregular "person", "people"
# inflect.uncountable %w( fish sheep )
# end
# These inflection rules are supported but not enabled by default:
# ActiveSupport::Inflector.inflections(:en) do |inflect|
# inflect.acronym "RESTful"
# end

View file

@ -0,0 +1,11 @@
# Define an application-wide HTTP permissions policy. For further
# information see https://developers.google.com/web/updates/2018/06/feature-policy
#
# Rails.application.config.permissions_policy do |f|
# f.camera :none
# f.gyroscope :none
# f.microphone :none
# f.usb :none
# f.fullscreen :self
# f.payment :self, "https://secure.example.com"
# end

View file

@ -0,0 +1,14 @@
# frozen_string_literal: true
ENV['REDIS_HOST'] ||= 'localhost'
#require 'hiredis'
require 'redis'
$redis = Redis.new(host: ENV['REDIS_HOST'])
if defined?(PhusionPassenger)
PhusionPassenger.on_event(:starting_worker_process) do |forked|
$redis.disconnect! if forked
end
end

33
config/locales/en.yml Normal file
View file

@ -0,0 +1,33 @@
# Files in the config/locales directory are used for internationalization
# and are automatically loaded by Rails. If you want to use locales other
# than English, add the necessary files in this directory.
#
# To use the locales, use `I18n.t`:
#
# I18n.t "hello"
#
# In views, this is aliased to just `t`:
#
# <%= t("hello") %>
#
# To use a different locale, set it with `I18n.locale`:
#
# I18n.locale = :es
#
# This would use the information in config/locales/es.yml.
#
# The following keys must be escaped otherwise they will not be retrieved by
# the default I18n backend:
#
# true, false, on, off, yes, no
#
# Instead, surround them with single quotes.
#
# en:
# "true": "foo"
#
# To learn more, please read the Rails Internationalization guide
# available at https://guides.rubyonrails.org/i18n.html.
en:
hello: "Hello world"

43
config/puma.rb Normal file
View file

@ -0,0 +1,43 @@
# Puma can serve each request in a thread from an internal thread pool.
# The `threads` method setting takes two numbers: a minimum and maximum.
# Any libraries that use thread pools should be configured to match
# the maximum value specified for Puma. Default is set to 5 threads for minimum
# and maximum; this matches the default thread size of Active Record.
#
max_threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 }
min_threads_count = ENV.fetch("RAILS_MIN_THREADS") { max_threads_count }
threads min_threads_count, max_threads_count
# Specifies the `worker_timeout` threshold that Puma will use to wait before
# terminating a worker in development environments.
#
worker_timeout 3600 if ENV.fetch("RAILS_ENV", "development") == "development"
# Specifies the `port` that Puma will listen on to receive requests; default is 3000.
#
port ENV.fetch("PORT") { 3000 }
# Specifies the `environment` that Puma will run in.
#
environment ENV.fetch("RAILS_ENV") { "development" }
# Specifies the `pidfile` that Puma will use.
pidfile ENV.fetch("PIDFILE") { "tmp/pids/server.pid" }
# Specifies the number of `workers` to boot in clustered mode.
# Workers are forked web server processes. If using threads and workers together
# the concurrency of the application would be max `threads` * `workers`.
# Workers do not work on JRuby or Windows (both of which do not support
# processes).
#
# workers ENV.fetch("WEB_CONCURRENCY") { 2 }
# Use the `preload_app!` method when specifying a `workers` number.
# This directive tells Puma to first boot the application and load code
# before forking the application. This takes advantage of Copy On Write
# process behavior so workers use less memory.
#
# preload_app!
# Allow puma to be restarted by `bin/rails restart` command.
plugin :tmp_restart

12
config/routes.rb Normal file
View file

@ -0,0 +1,12 @@
Rails.application.routes.draw do
root 'search#index'
post '/search' => 'search#search'
get '/search' => 'search#search'
get '/about' => 'static_pages#about'
get '/images' => 'images#show'
resources :authors, only: [:show]
resources :stories, only: [:show] do
resources :chapters, only: [:show]
end
end

View file

@ -0,0 +1,63 @@
class Initial < ActiveRecord::Migration[7.0]
def change
#create_enum :story_completion_status, %w[hiatus incomplete complete cancelled]
#create_enum :story_content_rating, %w[teen everyone mature]
create_table :authors do |t|
t.text :name, null: false
t.integer :num_blog_posts, null: false, default: 0
t.integer :num_followers, null: false, default: 0
t.text :avatar, null: true
t.text :bio_html, null: true
t.datetime :date_joined, null: false
t.text :url, null: false
end
create_table :stories do |t|
t.belongs_to :author, null: false
t.integer :color, null: true
# t.enum :completion_status, enum_type: 'story_completion_status', null: false
# t.enum :content_rating, enum_type: 'story_content_rating', null: false
t.text :completion_status
t.text :content_rating
t.text :cover_image, null: true
t.datetime :date_published, null: false
t.datetime :date_updated, null: true
t.datetime :date_modified, null: true
t.text :description_html, null: true
t.integer :num_comments, null: false, default: 0
t.integer :num_views, null: false, default: 0
t.integer :num_words, null: false
t.integer :prequel, null: true
t.integer :rating, null: false
t.text :short_description, null: true
t.text :title, null: false
t.integer :total_num_views, null: false, default: 0
t.text :url, null: false
end
create_table :chapters do |t|
t.belongs_to :story, null: false
t.integer :number, null: false, default: 1
t.datetime :date_published, null: false
t.datetime :date_modified, null: true
t.integer :num_views, null: false, default: 0
t.integer :num_words, null: false
t.text :title, null: false
t.text :url, null: false
t.text :body, null: false
end
create_table :tags do |t|
t.text :name, null: false
t.text :old_id, null: true
t.text :url, null: false
t.text :type, null: false
end
create_table :story_taggings, id: false, primary_key: [:story_id, :tag_id] do |t|
t.belongs_to :story, null: false
t.belongs_to :tag, null: false
end
end
end

View file

@ -0,0 +1,5 @@
class TempNullableBody < ActiveRecord::Migration[7.0]
def change
change_column_null :chapters, :body, true
end
end

View file

@ -0,0 +1,8 @@
class RemoveUrls < ActiveRecord::Migration[7.0]
def change
remove_column :stories, :url, :text
remove_column :chapters, :url, :text
remove_column :tags, :url, :text
remove_column :authors, :url, :text
end
end

77
db/schema.rb generated Normal file
View file

@ -0,0 +1,77 @@
# 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.
#
# This file is the source Rails uses to define your schema when running `bin/rails
# db:schema:load`. When creating a new database, `bin/rails db:schema:load` tends to
# be faster and is potentially less error prone than running all of your
# migrations from scratch. Old migrations may fail to apply correctly if those
# migrations use external dependencies or application code.
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema[7.0].define(version: 2024_04_02_172140) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
# Custom types defined in this database.
# Note that some types may not work with other database engines. Be careful if changing database.
create_enum "story_completion_status", ["hiatus", "incomplete", "complete", "cancelled"]
create_enum "story_content_rating", ["teen", "everyone", "mature"]
create_table "authors", force: :cascade do |t|
t.text "name", null: false
t.integer "num_blog_posts", default: 0, null: false
t.integer "num_followers", default: 0, null: false
t.text "avatar"
t.text "bio_html"
t.datetime "date_joined", null: false
end
create_table "chapters", force: :cascade do |t|
t.bigint "story_id", null: false
t.integer "number", default: 1, null: false
t.datetime "date_published", null: false
t.datetime "date_modified"
t.integer "num_views", default: 0, null: false
t.integer "num_words", null: false
t.text "title", null: false
t.text "body"
t.index ["story_id"], name: "index_chapters_on_story_id"
end
create_table "stories", force: :cascade do |t|
t.bigint "author_id", null: false
t.integer "color"
t.text "completion_status"
t.text "content_rating"
t.text "cover_image"
t.datetime "date_published", null: false
t.datetime "date_updated"
t.datetime "date_modified"
t.text "description_html"
t.integer "num_comments", default: 0, null: false
t.integer "num_views", default: 0, null: false
t.integer "num_words", null: false
t.integer "prequel"
t.integer "rating", null: false
t.text "short_description"
t.text "title", null: false
t.integer "total_num_views", default: 0, null: false
t.index ["author_id"], name: "index_stories_on_author_id"
end
create_table "story_taggings", id: false, force: :cascade do |t|
t.bigint "story_id", null: false
t.bigint "tag_id", null: false
t.index ["story_id"], name: "index_story_taggings_on_story_id"
t.index ["tag_id"], name: "index_story_taggings_on_tag_id"
end
create_table "tags", force: :cascade do |t|
t.text "name", null: false
t.text "old_id"
t.text "type", null: false
end
end

7
db/seeds.rb Normal file
View file

@ -0,0 +1,7 @@
# This file should contain all the record creation needed to seed the database with its default values.
# The data can then be loaded with the bin/rails db:seed command (or created alongside the database with db:setup).
#
# Examples:
#
# movies = Movie.create([{ name: "Star Wars" }, { name: "Lord of the Rings" }])
# Character.create(name: "Luke", movie: movies.first)

0
lib/assets/.keep Normal file
View file

0
lib/tasks/.keep Normal file
View file

0
log/.keep Normal file
View file

67
public/404.html Normal file
View file

@ -0,0 +1,67 @@
<!DOCTYPE html>
<html>
<head>
<title>The page you were looking for doesn't exist (404)</title>
<meta name="viewport" content="width=device-width,initial-scale=1">
<style>
.rails-default-error-page {
background-color: #EFEFEF;
color: #2E2F30;
text-align: center;
font-family: arial, sans-serif;
margin: 0;
}
.rails-default-error-page div.dialog {
width: 95%;
max-width: 33em;
margin: 4em auto 0;
}
.rails-default-error-page div.dialog > div {
border: 1px solid #CCC;
border-right-color: #999;
border-left-color: #999;
border-bottom-color: #BBB;
border-top: #B00100 solid 4px;
border-top-left-radius: 9px;
border-top-right-radius: 9px;
background-color: white;
padding: 7px 12% 0;
box-shadow: 0 3px 8px rgba(50, 50, 50, 0.17);
}
.rails-default-error-page h1 {
font-size: 100%;
color: #730E15;
line-height: 1.5em;
}
.rails-default-error-page div.dialog > p {
margin: 0 0 1em;
padding: 1em;
background-color: #F7F7F7;
border: 1px solid #CCC;
border-right-color: #999;
border-left-color: #999;
border-bottom-color: #999;
border-bottom-left-radius: 4px;
border-bottom-right-radius: 4px;
border-top-color: #DADADA;
color: #666;
box-shadow: 0 3px 8px rgba(50, 50, 50, 0.17);
}
</style>
</head>
<body class="rails-default-error-page">
<!-- This file lives in public/404.html -->
<div class="dialog">
<div>
<h1>The page you were looking for doesn't exist.</h1>
<p>You may have mistyped the address or the page may have moved.</p>
</div>
<p>If you are the application owner check the logs for more information.</p>
</div>
</body>
</html>

67
public/422.html Normal file
View file

@ -0,0 +1,67 @@
<!DOCTYPE html>
<html>
<head>
<title>The change you wanted was rejected (422)</title>
<meta name="viewport" content="width=device-width,initial-scale=1">
<style>
.rails-default-error-page {
background-color: #EFEFEF;
color: #2E2F30;
text-align: center;
font-family: arial, sans-serif;
margin: 0;
}
.rails-default-error-page div.dialog {
width: 95%;
max-width: 33em;
margin: 4em auto 0;
}
.rails-default-error-page div.dialog > div {
border: 1px solid #CCC;
border-right-color: #999;
border-left-color: #999;
border-bottom-color: #BBB;
border-top: #B00100 solid 4px;
border-top-left-radius: 9px;
border-top-right-radius: 9px;
background-color: white;
padding: 7px 12% 0;
box-shadow: 0 3px 8px rgba(50, 50, 50, 0.17);
}
.rails-default-error-page h1 {
font-size: 100%;
color: #730E15;
line-height: 1.5em;
}
.rails-default-error-page div.dialog > p {
margin: 0 0 1em;
padding: 1em;
background-color: #F7F7F7;
border: 1px solid #CCC;
border-right-color: #999;
border-left-color: #999;
border-bottom-color: #999;
border-bottom-left-radius: 4px;
border-bottom-right-radius: 4px;
border-top-color: #DADADA;
color: #666;
box-shadow: 0 3px 8px rgba(50, 50, 50, 0.17);
}
</style>
</head>
<body class="rails-default-error-page">
<!-- This file lives in public/422.html -->
<div class="dialog">
<div>
<h1>The change you wanted was rejected.</h1>
<p>Maybe you tried to change something you didn't have access to.</p>
</div>
<p>If you are the application owner check the logs for more information.</p>
</div>
</body>
</html>

66
public/500.html Normal file
View file

@ -0,0 +1,66 @@
<!DOCTYPE html>
<html>
<head>
<title>We're sorry, but something went wrong (500)</title>
<meta name="viewport" content="width=device-width,initial-scale=1">
<style>
.rails-default-error-page {
background-color: #EFEFEF;
color: #2E2F30;
text-align: center;
font-family: arial, sans-serif;
margin: 0;
}
.rails-default-error-page div.dialog {
width: 95%;
max-width: 33em;
margin: 4em auto 0;
}
.rails-default-error-page div.dialog > div {
border: 1px solid #CCC;
border-right-color: #999;
border-left-color: #999;
border-bottom-color: #BBB;
border-top: #B00100 solid 4px;
border-top-left-radius: 9px;
border-top-right-radius: 9px;
background-color: white;
padding: 7px 12% 0;
box-shadow: 0 3px 8px rgba(50, 50, 50, 0.17);
}
.rails-default-error-page h1 {
font-size: 100%;
color: #730E15;
line-height: 1.5em;
}
.rails-default-error-page div.dialog > p {
margin: 0 0 1em;
padding: 1em;
background-color: #F7F7F7;
border: 1px solid #CCC;
border-right-color: #999;
border-left-color: #999;
border-bottom-color: #999;
border-bottom-left-radius: 4px;
border-bottom-right-radius: 4px;
border-top-color: #DADADA;
color: #666;
box-shadow: 0 3px 8px rgba(50, 50, 50, 0.17);
}
</style>
</head>
<body class="rails-default-error-page">
<!-- This file lives in public/500.html -->
<div class="dialog">
<div>
<h1>We're sorry, but something went wrong.</h1>
</div>
<p>If you are the application owner check the logs for more information.</p>
</div>
</body>
</html>

View file

View file

View file

0
public/favicon.ico Normal file
View file

BIN
public/img/banner.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Some files were not shown because too many files have changed in this diff Show more