mirror of
https://github.com/Neetpone/foalfetch.git
synced 2025-03-11 14:10:07 +01:00
initial: something resembling a minimum viable product
This commit is contained in:
commit
563aa6a0f8
130 changed files with 6276 additions and 0 deletions
7
.gitattributes
vendored
Normal file
7
.gitattributes
vendored
Normal 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
28
.gitignore
vendored
Normal 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
1
.ruby-version
Normal file
|
@ -0,0 +1 @@
|
||||||
|
ruby-3.2.2
|
41
Gemfile
Normal file
41
Gemfile
Normal 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
291
Gemfile.lock
Normal 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
19
LICENSE
Normal 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
2
README.md
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
# FoalFetch
|
||||||
|
A replacement for FiMFetch.
|
6
Rakefile
Normal file
6
Rakefile
Normal 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
|
6
app/assets/config/manifest.js
Normal file
6
app/assets/config/manifest.js
Normal 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
0
app/assets/images/.keep
Normal file
98
app/assets/javascripts/application.js
Normal file
98
app/assets/javascripts/application.js
Normal 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);
|
||||||
|
}
|
151
app/assets/stylesheets/about.css
Normal file
151
app/assets/stylesheets/about.css
Normal 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;
|
||||||
|
}
|
50
app/assets/stylesheets/application.css
Normal file
50
app/assets/stylesheets/application.css
Normal 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;
|
||||||
|
}
|
3548
app/assets/stylesheets/fimfetch.css
Normal file
3548
app/assets/stylesheets/fimfetch.css
Normal file
File diff suppressed because it is too large
Load diff
4
app/channels/application_cable/channel.rb
Normal file
4
app/channels/application_cable/channel.rb
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
module ApplicationCable
|
||||||
|
class Channel < ActionCable::Channel::Base
|
||||||
|
end
|
||||||
|
end
|
4
app/channels/application_cable/connection.rb
Normal file
4
app/channels/application_cable/connection.rb
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
module ApplicationCable
|
||||||
|
class Connection < ActionCable::Connection::Base
|
||||||
|
end
|
||||||
|
end
|
21
app/controllers/application_controller.rb
Normal file
21
app/controllers/application_controller.rb
Normal 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
|
5
app/controllers/authors_controller.rb
Normal file
5
app/controllers/authors_controller.rb
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
class AuthorsController < ApplicationController
|
||||||
|
def show
|
||||||
|
@author = Author.find(params[:id])
|
||||||
|
end
|
||||||
|
end
|
6
app/controllers/chapters_controller.rb
Normal file
6
app/controllers/chapters_controller.rb
Normal 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
|
0
app/controllers/concerns/.keep
Normal file
0
app/controllers/concerns/.keep
Normal file
28
app/controllers/images_controller.rb
Normal file
28
app/controllers/images_controller.rb
Normal 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
|
125
app/controllers/search_controller.rb
Normal file
125
app/controllers/search_controller.rb
Normal 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
|
4
app/controllers/static_pages_controller.rb
Normal file
4
app/controllers/static_pages_controller.rb
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
class StaticPagesController < ApplicationController
|
||||||
|
def about
|
||||||
|
end
|
||||||
|
end
|
9
app/controllers/stories_controller.rb
Normal file
9
app/controllers/stories_controller.rb
Normal 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
|
15
app/helpers/application_helper.rb
Normal file
15
app/helpers/application_helper.rb
Normal 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
|
2
app/helpers/authors_helper.rb
Normal file
2
app/helpers/authors_helper.rb
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
module AuthorsHelper
|
||||||
|
end
|
2
app/helpers/chapters_helper.rb
Normal file
2
app/helpers/chapters_helper.rb
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
module ChaptersHelper
|
||||||
|
end
|
2
app/helpers/images_helper.rb
Normal file
2
app/helpers/images_helper.rb
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
module ImagesHelper
|
||||||
|
end
|
47
app/helpers/search_helper.rb
Normal file
47
app/helpers/search_helper.rb
Normal 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
|
2
app/helpers/static_pages_helper.rb
Normal file
2
app/helpers/static_pages_helper.rb
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
module StaticPagesHelper
|
||||||
|
end
|
2
app/helpers/stories_helper.rb
Normal file
2
app/helpers/stories_helper.rb
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
module StoriesHelper
|
||||||
|
end
|
61
app/indexes/story_index.rb
Normal file
61
app/indexes/story_index.rb
Normal 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
|
7
app/jobs/application_job.rb
Normal file
7
app/jobs/application_job.rb
Normal 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
|
11
app/jobs/index_update_job.rb
Normal file
11
app/jobs/index_update_job.rb
Normal 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
|
3
app/models/application_record.rb
Normal file
3
app/models/application_record.rb
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
class ApplicationRecord < ActiveRecord::Base
|
||||||
|
primary_abstract_class
|
||||||
|
end
|
3
app/models/author.rb
Normal file
3
app/models/author.rb
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
class Author < ApplicationRecord
|
||||||
|
has_many :stories
|
||||||
|
end
|
3
app/models/chapter.rb
Normal file
3
app/models/chapter.rb
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
class Chapter < ApplicationRecord
|
||||||
|
belongs_to :story
|
||||||
|
end
|
0
app/models/concerns/.keep
Normal file
0
app/models/concerns/.keep
Normal file
34
app/models/concerns/indexable.rb
Normal file
34
app/models/concerns/indexable.rb
Normal 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
9
app/models/story.rb
Normal 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
|
6
app/models/story/tagging.rb
Normal file
6
app/models/story/tagging.rb
Normal 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
2
app/models/tag.rb
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
class Tag < ApplicationRecord
|
||||||
|
end
|
29
app/views/chapters/show.html.slim
Normal file
29
app/views/chapters/show.html.slim
Normal 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)
|
||||||
|
|
8
app/views/kaminari/_first_page.html.slim
Normal file
8
app/views/kaminari/_first_page.html.slim
Normal 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
|
7
app/views/kaminari/_gap.html.slim
Normal file
7
app/views/kaminari/_gap.html.slim
Normal 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 …
|
8
app/views/kaminari/_last_page.html.slim
Normal file
8
app/views/kaminari/_last_page.html.slim
Normal 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
|
8
app/views/kaminari/_next_page.html.slim
Normal file
8
app/views/kaminari/_next_page.html.slim
Normal 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'
|
12
app/views/kaminari/_page.html.slim
Normal file
12
app/views/kaminari/_page.html.slim
Normal 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'
|
18
app/views/kaminari/_paginator.html.slim
Normal file
18
app/views/kaminari/_paginator.html.slim
Normal 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?
|
8
app/views/kaminari/_prev_page.html.slim
Normal file
8
app/views/kaminari/_prev_page.html.slim
Normal 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'
|
3
app/views/layouts/_banner.html.slim
Normal file
3
app/views/layouts/_banner.html.slim
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
header.banner.main
|
||||||
|
= link_to '/'
|
||||||
|
= image_tag '/img/banner.png'
|
35
app/views/layouts/application.html.slim
Normal file
35
app/views/layouts/application.html.slim
Normal 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
|
127
app/views/search/_advanced.html.slim
Normal file
127
app/views/search/_advanced.html.slim
Normal 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'
|
15
app/views/search/index.html.slim
Normal file
15
app/views/search/index.html.slim
Normal 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'
|
58
app/views/search/search.html.slim
Normal file
58
app/views/search/search.html.slim
Normal 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') + ','
|
||||||
|
'
|
||||||
|
span.words
|
||||||
|
=<> rec.num_words
|
||||||
|
' Words:
|
||||||
|
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.
|
25
app/views/static_pages/about.html.slim
Normal file
25
app/views/static_pages/about.html.slim
Normal 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.
|
0
app/views/stories/index.html.slim
Normal file
0
app/views/stories/index.html.slim
Normal file
52
app/views/stories/show.html.slim
Normal file
52
app/views/stories/show.html.slim
Normal 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)
|
||||||
|
'
|
||||||
|
.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
109
bin/bundle
Executable 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
4
bin/importmap
Executable file
|
@ -0,0 +1,4 @@
|
||||||
|
#!/usr/bin/env ruby
|
||||||
|
|
||||||
|
require_relative "../config/application"
|
||||||
|
require "importmap/commands"
|
4
bin/rails
Executable file
4
bin/rails
Executable 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
4
bin/rake
Executable file
|
@ -0,0 +1,4 @@
|
||||||
|
#!/usr/bin/env ruby
|
||||||
|
require_relative "../config/boot"
|
||||||
|
require "rake"
|
||||||
|
Rake.application.run
|
33
bin/setup
Executable file
33
bin/setup
Executable 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
6
config.ru
Normal 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
34
config/application.rb
Normal 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
3
config/boot.rb
Normal 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
11
config/cable.yml
Normal 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
|
1
config/credentials.yml.enc
Normal file
1
config/credentials.yml.enc
Normal 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
86
config/database.yml
Normal 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
5
config/environment.rb
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
# Load the Rails application.
|
||||||
|
require_relative "application"
|
||||||
|
|
||||||
|
# Initialize the Rails application.
|
||||||
|
Rails.application.initialize!
|
62
config/environments/development.rb
Normal file
62
config/environments/development.rb
Normal 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
|
84
config/environments/production.rb
Normal file
84
config/environments/production.rb
Normal 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
|
50
config/environments/test.rb
Normal file
50
config/environments/test.rb
Normal 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
7
config/importmap.rb
Normal 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"
|
12
config/initializers/assets.rb
Normal file
12
config/initializers/assets.rb
Normal 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 )
|
25
config/initializers/content_security_policy.rb
Normal file
25
config/initializers/content_security_policy.rb
Normal 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
|
17
config/initializers/elasticsearch.rb
Normal file
17
config/initializers/elasticsearch.rb
Normal 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)
|
8
config/initializers/filter_parameter_logging.rb
Normal file
8
config/initializers/filter_parameter_logging.rb
Normal 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
|
||||||
|
]
|
16
config/initializers/inflections.rb
Normal file
16
config/initializers/inflections.rb
Normal 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
|
11
config/initializers/permissions_policy.rb
Normal file
11
config/initializers/permissions_policy.rb
Normal 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
|
14
config/initializers/redis.rb
Normal file
14
config/initializers/redis.rb
Normal 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
33
config/locales/en.yml
Normal 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
43
config/puma.rb
Normal 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
12
config/routes.rb
Normal 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
|
63
db/migrate/20240401101354_initial.rb
Normal file
63
db/migrate/20240401101354_initial.rb
Normal 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
|
5
db/migrate/20240401121829_temp_nullable_body.rb
Normal file
5
db/migrate/20240401121829_temp_nullable_body.rb
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
class TempNullableBody < ActiveRecord::Migration[7.0]
|
||||||
|
def change
|
||||||
|
change_column_null :chapters, :body, true
|
||||||
|
end
|
||||||
|
end
|
8
db/migrate/20240402172140_remove_urls.rb
Normal file
8
db/migrate/20240402172140_remove_urls.rb
Normal 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
77
db/schema.rb
generated
Normal 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
7
db/seeds.rb
Normal 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
0
lib/assets/.keep
Normal file
0
lib/tasks/.keep
Normal file
0
lib/tasks/.keep
Normal file
0
log/.keep
Normal file
0
log/.keep
Normal file
67
public/404.html
Normal file
67
public/404.html
Normal 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
67
public/422.html
Normal 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
66
public/500.html
Normal 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>
|
0
public/apple-touch-icon-precomposed.png
Normal file
0
public/apple-touch-icon-precomposed.png
Normal file
0
public/apple-touch-icon.png
Normal file
0
public/apple-touch-icon.png
Normal file
0
public/cached-images/.placeholder
Normal file
0
public/cached-images/.placeholder
Normal file
0
public/favicon.ico
Normal file
0
public/favicon.ico
Normal file
BIN
public/img/banner.png
Normal 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
Loading…
Add table
Reference in a new issue