From 563aa6a0f8544499337f7455c059117c57383c37 Mon Sep 17 00:00:00 2001
From: Neetpone <132411956+Neetpone@users.noreply.github.com>
Date: Tue, 2 Apr 2024 13:33:19 -0400
Subject: [PATCH] initial: something resembling a minimum viable product
---
.gitattributes | 7 +
.gitignore | 28 +
.ruby-version | 1 +
Gemfile | 41 +
Gemfile.lock | 291 ++
LICENSE | 19 +
README.md | 2 +
Rakefile | 6 +
app/assets/config/manifest.js | 6 +
app/assets/images/.keep | 0
app/assets/javascripts/application.js | 98 +
app/assets/stylesheets/about.css | 151 +
app/assets/stylesheets/application.css | 50 +
app/assets/stylesheets/fimfetch.css | 3548 +++++++++++++++++
app/channels/application_cable/channel.rb | 4 +
app/channels/application_cable/connection.rb | 4 +
app/controllers/application_controller.rb | 21 +
app/controllers/authors_controller.rb | 5 +
app/controllers/chapters_controller.rb | 6 +
app/controllers/concerns/.keep | 0
app/controllers/images_controller.rb | 28 +
app/controllers/search_controller.rb | 125 +
app/controllers/static_pages_controller.rb | 4 +
app/controllers/stories_controller.rb | 9 +
app/helpers/application_helper.rb | 15 +
app/helpers/authors_helper.rb | 2 +
app/helpers/chapters_helper.rb | 2 +
app/helpers/images_helper.rb | 2 +
app/helpers/search_helper.rb | 47 +
app/helpers/static_pages_helper.rb | 2 +
app/helpers/stories_helper.rb | 2 +
app/indexes/story_index.rb | 61 +
app/jobs/application_job.rb | 7 +
app/jobs/index_update_job.rb | 11 +
app/models/application_record.rb | 3 +
app/models/author.rb | 3 +
app/models/chapter.rb | 3 +
app/models/concerns/.keep | 0
app/models/concerns/indexable.rb | 34 +
app/models/story.rb | 9 +
app/models/story/tagging.rb | 6 +
app/models/tag.rb | 2 +
app/views/chapters/show.html.slim | 29 +
app/views/kaminari/_first_page.html.slim | 8 +
app/views/kaminari/_gap.html.slim | 7 +
app/views/kaminari/_last_page.html.slim | 8 +
app/views/kaminari/_next_page.html.slim | 8 +
app/views/kaminari/_page.html.slim | 12 +
app/views/kaminari/_paginator.html.slim | 18 +
app/views/kaminari/_prev_page.html.slim | 8 +
app/views/layouts/_banner.html.slim | 3 +
app/views/layouts/application.html.slim | 35 +
app/views/search/_advanced.html.slim | 127 +
app/views/search/index.html.slim | 15 +
app/views/search/search.html.slim | 58 +
app/views/static_pages/about.html.slim | 25 +
app/views/stories/index.html.slim | 0
app/views/stories/show.html.slim | 52 +
bin/bundle | 109 +
bin/importmap | 4 +
bin/rails | 4 +
bin/rake | 4 +
bin/setup | 33 +
config.ru | 6 +
config/application.rb | 34 +
config/boot.rb | 3 +
config/cable.yml | 11 +
config/credentials.yml.enc | 1 +
config/database.yml | 86 +
config/environment.rb | 5 +
config/environments/development.rb | 62 +
config/environments/production.rb | 84 +
config/environments/test.rb | 50 +
config/importmap.rb | 7 +
config/initializers/assets.rb | 12 +
.../initializers/content_security_policy.rb | 25 +
config/initializers/elasticsearch.rb | 17 +
.../initializers/filter_parameter_logging.rb | 8 +
config/initializers/inflections.rb | 16 +
config/initializers/permissions_policy.rb | 11 +
config/initializers/redis.rb | 14 +
config/locales/en.yml | 33 +
config/puma.rb | 43 +
config/routes.rb | 12 +
db/migrate/20240401101354_initial.rb | 63 +
.../20240401121829_temp_nullable_body.rb | 5 +
db/migrate/20240402172140_remove_urls.rb | 8 +
db/schema.rb | 77 +
db/seeds.rb | 7 +
lib/assets/.keep | 0
lib/tasks/.keep | 0
log/.keep | 0
public/404.html | 67 +
public/422.html | 67 +
public/500.html | 66 +
public/apple-touch-icon-precomposed.png | 0
public/apple-touch-icon.png | 0
public/cached-images/.placeholder | 0
public/favicon.ico | 0
public/img/banner.png | Bin 0 -> 55351 bytes
public/img/logo.png | Bin 0 -> 21429 bytes
public/img/logo_small.png | Bin 0 -> 16298 bytes
public/robots.txt | 1 +
test/application_system_test_case.rb | 5 +
.../application_cable/connection_test.rb | 11 +
test/controllers/.keep | 0
test/controllers/authors_controller_test.rb | 7 +
test/controllers/chapters_controller_test.rb | 7 +
test/controllers/images_controller_test.rb | 7 +
test/controllers/search_controller_test.rb | 7 +
.../static_pages_controller_test.rb | 7 +
test/controllers/stories_controller_test.rb | 7 +
test/fixtures/authors.yml | 11 +
test/fixtures/chapters.yml | 11 +
test/fixtures/files/.keep | 0
test/fixtures/stories.yml | 11 +
test/fixtures/tags.yml | 11 +
test/helpers/.keep | 0
test/integration/.keep | 0
test/models/.keep | 0
test/models/author_test.rb | 7 +
test/models/chapter_test.rb | 7 +
test/models/story_test.rb | 7 +
test/models/tag_test.rb | 7 +
test/system/.keep | 0
test/test_helper.rb | 13 +
tmp/.keep | 0
tmp/pids/.keep | 0
vendor/.keep | 0
vendor/javascript/.keep | 0
130 files changed, 6276 insertions(+)
create mode 100644 .gitattributes
create mode 100644 .gitignore
create mode 100644 .ruby-version
create mode 100644 Gemfile
create mode 100644 Gemfile.lock
create mode 100644 LICENSE
create mode 100644 README.md
create mode 100644 Rakefile
create mode 100644 app/assets/config/manifest.js
create mode 100644 app/assets/images/.keep
create mode 100644 app/assets/javascripts/application.js
create mode 100644 app/assets/stylesheets/about.css
create mode 100644 app/assets/stylesheets/application.css
create mode 100644 app/assets/stylesheets/fimfetch.css
create mode 100644 app/channels/application_cable/channel.rb
create mode 100644 app/channels/application_cable/connection.rb
create mode 100644 app/controllers/application_controller.rb
create mode 100644 app/controllers/authors_controller.rb
create mode 100644 app/controllers/chapters_controller.rb
create mode 100644 app/controllers/concerns/.keep
create mode 100644 app/controllers/images_controller.rb
create mode 100644 app/controllers/search_controller.rb
create mode 100644 app/controllers/static_pages_controller.rb
create mode 100644 app/controllers/stories_controller.rb
create mode 100644 app/helpers/application_helper.rb
create mode 100644 app/helpers/authors_helper.rb
create mode 100644 app/helpers/chapters_helper.rb
create mode 100644 app/helpers/images_helper.rb
create mode 100644 app/helpers/search_helper.rb
create mode 100644 app/helpers/static_pages_helper.rb
create mode 100644 app/helpers/stories_helper.rb
create mode 100644 app/indexes/story_index.rb
create mode 100644 app/jobs/application_job.rb
create mode 100644 app/jobs/index_update_job.rb
create mode 100644 app/models/application_record.rb
create mode 100644 app/models/author.rb
create mode 100644 app/models/chapter.rb
create mode 100644 app/models/concerns/.keep
create mode 100644 app/models/concerns/indexable.rb
create mode 100644 app/models/story.rb
create mode 100644 app/models/story/tagging.rb
create mode 100644 app/models/tag.rb
create mode 100644 app/views/chapters/show.html.slim
create mode 100644 app/views/kaminari/_first_page.html.slim
create mode 100644 app/views/kaminari/_gap.html.slim
create mode 100644 app/views/kaminari/_last_page.html.slim
create mode 100644 app/views/kaminari/_next_page.html.slim
create mode 100644 app/views/kaminari/_page.html.slim
create mode 100644 app/views/kaminari/_paginator.html.slim
create mode 100644 app/views/kaminari/_prev_page.html.slim
create mode 100644 app/views/layouts/_banner.html.slim
create mode 100644 app/views/layouts/application.html.slim
create mode 100644 app/views/search/_advanced.html.slim
create mode 100644 app/views/search/index.html.slim
create mode 100644 app/views/search/search.html.slim
create mode 100644 app/views/static_pages/about.html.slim
create mode 100644 app/views/stories/index.html.slim
create mode 100644 app/views/stories/show.html.slim
create mode 100755 bin/bundle
create mode 100755 bin/importmap
create mode 100755 bin/rails
create mode 100755 bin/rake
create mode 100755 bin/setup
create mode 100644 config.ru
create mode 100644 config/application.rb
create mode 100644 config/boot.rb
create mode 100644 config/cable.yml
create mode 100644 config/credentials.yml.enc
create mode 100644 config/database.yml
create mode 100644 config/environment.rb
create mode 100644 config/environments/development.rb
create mode 100644 config/environments/production.rb
create mode 100644 config/environments/test.rb
create mode 100644 config/importmap.rb
create mode 100644 config/initializers/assets.rb
create mode 100644 config/initializers/content_security_policy.rb
create mode 100644 config/initializers/elasticsearch.rb
create mode 100644 config/initializers/filter_parameter_logging.rb
create mode 100644 config/initializers/inflections.rb
create mode 100644 config/initializers/permissions_policy.rb
create mode 100644 config/initializers/redis.rb
create mode 100644 config/locales/en.yml
create mode 100644 config/puma.rb
create mode 100644 config/routes.rb
create mode 100644 db/migrate/20240401101354_initial.rb
create mode 100644 db/migrate/20240401121829_temp_nullable_body.rb
create mode 100644 db/migrate/20240402172140_remove_urls.rb
create mode 100644 db/schema.rb
create mode 100644 db/seeds.rb
create mode 100644 lib/assets/.keep
create mode 100644 lib/tasks/.keep
create mode 100644 log/.keep
create mode 100644 public/404.html
create mode 100644 public/422.html
create mode 100644 public/500.html
create mode 100644 public/apple-touch-icon-precomposed.png
create mode 100644 public/apple-touch-icon.png
create mode 100644 public/cached-images/.placeholder
create mode 100644 public/favicon.ico
create mode 100644 public/img/banner.png
create mode 100644 public/img/logo.png
create mode 100644 public/img/logo_small.png
create mode 100644 public/robots.txt
create mode 100644 test/application_system_test_case.rb
create mode 100644 test/channels/application_cable/connection_test.rb
create mode 100644 test/controllers/.keep
create mode 100644 test/controllers/authors_controller_test.rb
create mode 100644 test/controllers/chapters_controller_test.rb
create mode 100644 test/controllers/images_controller_test.rb
create mode 100644 test/controllers/search_controller_test.rb
create mode 100644 test/controllers/static_pages_controller_test.rb
create mode 100644 test/controllers/stories_controller_test.rb
create mode 100644 test/fixtures/authors.yml
create mode 100644 test/fixtures/chapters.yml
create mode 100644 test/fixtures/files/.keep
create mode 100644 test/fixtures/stories.yml
create mode 100644 test/fixtures/tags.yml
create mode 100644 test/helpers/.keep
create mode 100644 test/integration/.keep
create mode 100644 test/models/.keep
create mode 100644 test/models/author_test.rb
create mode 100644 test/models/chapter_test.rb
create mode 100644 test/models/story_test.rb
create mode 100644 test/models/tag_test.rb
create mode 100644 test/system/.keep
create mode 100644 test/test_helper.rb
create mode 100644 tmp/.keep
create mode 100644 tmp/pids/.keep
create mode 100644 vendor/.keep
create mode 100644 vendor/javascript/.keep
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..31eeee0
--- /dev/null
+++ b/.gitattributes
@@ -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
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..fd8892c
--- /dev/null
+++ b/.gitignore
@@ -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
+
diff --git a/.ruby-version b/.ruby-version
new file mode 100644
index 0000000..9e79f6c
--- /dev/null
+++ b/.ruby-version
@@ -0,0 +1 @@
+ruby-3.2.2
diff --git a/Gemfile b/Gemfile
new file mode 100644
index 0000000..55d20b7
--- /dev/null
+++ b/Gemfile
@@ -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
diff --git a/Gemfile.lock b/Gemfile.lock
new file mode 100644
index 0000000..a350854
--- /dev/null
+++ b/Gemfile.lock
@@ -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
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..4fb18ee
--- /dev/null
+++ b/LICENSE
@@ -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.
\ No newline at end of file
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..a969da6
--- /dev/null
+++ b/README.md
@@ -0,0 +1,2 @@
+# FoalFetch
+A replacement for FiMFetch.
\ No newline at end of file
diff --git a/Rakefile b/Rakefile
new file mode 100644
index 0000000..9a5ea73
--- /dev/null
+++ b/Rakefile
@@ -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
diff --git a/app/assets/config/manifest.js b/app/assets/config/manifest.js
new file mode 100644
index 0000000..2261605
--- /dev/null
+++ b/app/assets/config/manifest.js
@@ -0,0 +1,6 @@
+//= link_tree ../images
+//= link_directory ../stylesheets .css
+//= link_tree ../../javascript .js
+//= link_tree ../../../vendor/javascript .js
+
+//= link application.js
\ No newline at end of file
diff --git a/app/assets/images/.keep b/app/assets/images/.keep
new file mode 100644
index 0000000..e69de29
diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js
new file mode 100644
index 0000000..302ec77
--- /dev/null
+++ b/app/assets/javascripts/application.js
@@ -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(`
${tagName}
`);
+ 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('');
+ 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);
+}
diff --git a/app/assets/stylesheets/about.css b/app/assets/stylesheets/about.css
new file mode 100644
index 0000000..50b6570
--- /dev/null
+++ b/app/assets/stylesheets/about.css
@@ -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;
+}
diff --git a/app/assets/stylesheets/application.css b/app/assets/stylesheets/application.css
new file mode 100644
index 0000000..f46140a
--- /dev/null
+++ b/app/assets/stylesheets/application.css
@@ -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;
+}
diff --git a/app/assets/stylesheets/fimfetch.css b/app/assets/stylesheets/fimfetch.css
new file mode 100644
index 0000000..03bb38e
--- /dev/null
+++ b/app/assets/stylesheets/fimfetch.css
@@ -0,0 +1,3548 @@
+/* This entire file is just skidded from FiMFetch */
+.library h1 {
+ font: bold 34px "Times New Roman", Times, serif;
+ display: inline-block;
+ vertical-align: top;
+ margin: 0 0 0 10px;
+}
+.library > header > div {
+ display: inline-block;
+ padding: 0 40px;
+}
+.library > header {
+ margin-top: 10px;
+}
+.udot {
+ border-image-source: url(/img/dots.svg);
+ border-image-slice: 30%;
+ border-image-repeat: round;
+ border-color: green;
+ border-style: dotted;
+ border-width: 0 0 25px;
+}
+.library .avatar {
+ display: inline-block;
+ border: 1px solid #333;
+ -webkit-border-radius: 4px;
+ -moz-border-radius: 4px;
+ border-radius: 4px;
+ max-width: 80px;
+ max-height: 80px;
+ margin: 0 10px;
+ padding: 3px;
+}
+.library .avatar img {
+ max-width: 100%;
+ max-height: 100%;
+}
+.library ul {
+ list-style: none;
+ padding: 0;
+}
+.library li {
+ display: inline-block;
+ min-height: 200px;
+ width: 300px;
+ vertical-align: top;
+ padding: 6px;
+}
+.library li > section {
+ border: 1px solid #ccc;
+ border-radius: 4px;
+ -webkit-border-radius: 4px;
+ -moz-border-radius: 4px;
+ min-height: 200px;
+ background-color: #f3f2ed;
+ position: relative;
+}
+.spine header {
+ position: relative;
+ background-color: #585858;
+ border-radius: 4px 4px 0 0;
+ -webkit-border-radius: 4px 4px 0 0;
+ -moz-border-radius: 4px 4px 0 0;
+ min-height: 60px;
+ padding: 3px 0;
+}
+.spine header h2 {
+ font-size: 24px;
+ line-height: 26px;
+ color: #fff;
+ text-shadow: 0 0 6px #000, 1px 1px 3px #000, 2px 2px 2px #888;
+ margin: 0 45px 0 75px;
+}
+.spine header .fa {
+ position: absolute;
+ font-size: 20px;
+ height: 26px;
+ color: #e6dede;
+ display: block;
+ background-color: #333;
+ border-radius: 4px;
+ -webkit-border-radius: 4px;
+ -moz-border-radius: 4px;
+ padding: 3px 6px;
+}
+.spine header .fa.r {
+ top: 4px;
+ right: 4px;
+}
+.spine header .fa.l {
+ top: 2px;
+ left: 2px;
+ font-size: 50px;
+ height: auto;
+ min-width: 55px;
+}
+.spine header .fa.c {
+ color: #fff;
+ text-shadow: 0 0 1px #000, 1px 1px 4px #000, 1px 1px 6px #888;
+}
+.spine header a:hover {
+ text-decoration: none;
+}
+.spine section {
+ margin-bottom: 25px;
+ padding: 0 6px;
+}
+.spine footer {
+ position: absolute;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ text-align: right;
+ padding: 3px;
+}
+.spine footer * {
+ z-index: 1;
+}
+.spine footer > span:first-of-type {
+ float: left;
+ padding: 5px 0 0 3px;
+}
+.spine footer > span:nth-of-type(2) {
+ float: left;
+ font-size: 85%;
+ padding: 7px 0 0 15px;
+}
+.spine .count {
+ display: inline-block;
+ background-color: #fff;
+ border-radius: 4px;
+ -webkit-border-radius: 4px;
+ -moz-border-radius: 4px;
+ text-shadow: 0 0 1px #999, 1px 1px 2px #ccc, 0 0 4px #eee;
+ box-shadow: 0 0 3px #333 inset;
+ padding: 3px 6px;
+}
+.spine footer a,
+.spine footer a:hover {
+ color: #111;
+}
+.newlib {
+ max-width: 450px;
+ margin: 0 auto;
+ padding: 10px 5px;
+}
+.shelf-menu {
+ border: 1px solid #333;
+ border-radius: 12px;
+ padding: 0 5px 10px;
+}
+.shelf-menu > header {
+ border-bottom: 1px solid;
+ font-size: 24px;
+ padding-bottom: 4px;
+ margin: 5px -5px 11px;
+}
+.shelf-menu .line {
+ display: block;
+ font-size: 22px;
+ margin: 3px;
+ padding: 2px 4px;
+}
+.shelf-menu .spine header input[type="text"] {
+ font-size: 22px;
+ padding: 2px 4px;
+}
+.shelf-menu .spine section > label {
+ display: inline-block;
+ width: 100%;
+ overflow: auto;
+ text-align: left;
+ margin: 6px 0;
+}
+.shelf-menu .spine section textarea {
+ width: 100%;
+ min-height: 60px;
+}
+.shelf-menu .toggleable-radio {
+ font-size: 14px;
+ border: 1px solid rgba(0, 0, 0, 0.2);
+ border-radius: 4px;
+ display: inline-block;
+ background: #e2dddd;
+ position: relative;
+ box-shadow: 1px 1px 1px rgba(0, 0, 0, 0.1) inset;
+ transition: background 0.2s;
+ -webkit-touch-callout: none;
+ -webkit-user-select: none;
+ -khtml-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ user-select: none;
+}
+.toggleable-radio input {
+ visibility: hidden;
+ position: absolute;
+ top: 0;
+ left: 0;
+ margin: 0;
+}
+.toggleable-radio label:hover {
+ background: #fff;
+ text-shadow: 1px 1px rgba(255, 255, 255, 0.2);
+}
+.toggleable-radio input:checked + label {
+ color: #111;
+ text-shadow: -1px -1px rgba(255, 255, 255, 0.15);
+ font-weight: 600;
+ z-index: 2;
+ border-right: none;
+ border-left: none;
+ background-color: #9a9a9a;
+}
+.toggleable-radio label:first-of-type {
+ border-left: none;
+ border-top-left-radius: 4px;
+ border-bottom-left-radius: 4px;
+}
+.toggleable-radio label:last-of-type {
+ border-right: none;
+ border-top-right-radius: 4px;
+ border-bottom-right-radius: 4px;
+}
+.toggleable-radio label {
+ width: 110px;
+ display: inline-block;
+ position: relative;
+ text-align: center;
+ vertical-align: top;
+ line-height: 32px;
+ height: 32px;
+ color: #333;
+ cursor: pointer;
+ text-shadow: 1px 1px rgba(255, 255, 255, 0.5);
+ border-right: 1px solid rgba(0, 0, 0, 0.2);
+}
+.shelf-menu .iconselect .toggleable-radio,
+.shelf-menu .colorselect .toggleable-radio {
+ background: #f9f9f9;
+ max-width: 350px;
+ padding: 2px;
+}
+.iconselect .toggleable-radio label,
+.colorselect .toggleable-radio label {
+ width: 32px;
+ height: 32px;
+ box-shadow: 0 0 1px #000;
+ border-radius: 3px;
+ border: 1px solid rgba(60, 60, 60, 0.2);
+ margin: 3px 2px;
+}
+.iconselect .toggleable-radio input:checked + label,
+.colorselect .toggleable-radio input:checked + label {
+ border: 3px solid #666;
+ border-radius: 5px;
+ box-shadow: 0 0 1px #fff inset;
+}
+.iconselect .toggleable-radio input:checked + label i {
+ top: -2px;
+ position: relative;
+}
+.iconselect .toggleable-radio {
+ max-height: 150px;
+ overflow-y: scroll;
+ overflow-x: hidden;
+}
+.iconselect i.fa {
+ color: #fff;
+ text-shadow: 0 0 1px #000, 1px 1px 4px #000, 1px 1px 6px #888;
+}
+.iconselect i.fa.pony {
+ color: #000;
+ text-shadow: 1px 1px 4px rgba(208, 208, 208, 0.5);
+}
+.toggleable-radio::-webkit-scrollbar {
+ width: 8px;
+}
+.toggleable-radio::-webkit-scrollbar-thumb {
+ background-color: #aaa;
+}
+.toggleable-radio::-webkit-scrollbar-track {
+ background-color: #eee;
+ box-shadow: 0 0 2px rgba(0, 0, 0, 0.35) inset;
+}
+#status.ok,
+#status.error {
+ display: block;
+ text-align: center;
+ padding: 14px 0 0;
+}
+.shelf-menu .ok,
+.shelf-menu .error {
+ display: block;
+ padding: 2px 0 14px;
+}
+.shelf-menu .submit2,
+.shelf .submit2 {
+ font-size: 16px;
+ background: #bfb;
+ padding: 10px;
+}
+.shelf-menu .submit2:hover,
+.shelf .submit2:hover {
+ background: #7f7;
+}
+.shelf-menu .submit2:active,
+.shelf .submit2:active {
+ background: #4f4;
+}
+a.submit2.cancel {
+ color: #000;
+ background: #faa;
+}
+a.submit2.cancel:hover {
+ color: #000;
+ background: #f55;
+ text-decoration: none;
+}
+a.submit2.cancel:active {
+ background: #f33;
+}
+.delete {
+ display: inline-block;
+ font-weight: 700;
+ line-height: 30px;
+ color: #b11;
+ padding: 2px 0 12px;
+}
+.shelf > header h1 {
+ display: block;
+ font: bold 34px "Times New Roman", Times, serif;
+}
+.shelf > header h2 {
+ font-size: 20px;
+ margin: 0 0 15px;
+}
+.shelf .title i {
+ display: inline-block;
+ float: left;
+ background-color: #333;
+ border-radius: 4px;
+ -webkit-border-radius: 4px;
+ -moz-border-radius: 4px;
+ box-shadow: 0 1px 1px 1px #272727;
+ margin: 5px 10px 0 0;
+ padding: 3px 6px;
+}
+.shelf .title > div > div {
+ display: inline-block;
+ float: left;
+ text-align: left;
+}
+.shelf .title .fa {
+ color: #fff;
+ top: 2px;
+ left: 2px;
+ font-size: 50px;
+ height: auto;
+ min-width: 55px;
+ text-shadow: 0 0 1px #000, 1px 1px 4px #000, 1px 1px 6px #888;
+}
+.shelf .stats {
+ display: inline-block;
+ text-align: left;
+}
+.shelf > header {
+ max-width: 650px;
+ margin: 10px auto 0;
+ padding: 0 10px;
+}
+.shelf .shelfpage .result {
+ margin-top: 20px;
+ display: block;
+}
+.shelfpage .fic-cell {
+ box-shadow: none;
+}
+.shelfpage .fic-cell,
+.comment-cell {
+ position: relative;
+ background: #fff;
+}
+.shelfpage .fic-cell:before,
+.comment-cell:before {
+ content: "";
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ z-index: -1;
+ border-radius: 4px;
+ -webkit-border-radius: 4px;
+ -moz-border-radius: 4px;
+ box-shadow: 5px 5px #ccc;
+}
+.comment-cell {
+ border: 1px solid #ccc;
+ font-family: Arial, Helvetica, sans-serif;
+ font-size: 16px;
+ line-height: 1.3em;
+ text-align: left;
+ margin: -14px 0 15px;
+ padding: 5px 10px;
+}
+.comment-cell .title {
+ font-size: 14px;
+ width: 95%;
+ border-bottom: 1px solid #ddd;
+ margin-bottom: 8px;
+ display: block;
+ text-align: left;
+ padding: 0 5px;
+}
+.review {
+ display: block;
+ max-height: 200px;
+ overflow-y: auto;
+}
+.comment-cell p {
+ border: 0;
+ margin: 0;
+ padding: 0;
+}
+.comment-cell p.double {
+ margin-top: 1.5em;
+}
+.comment-cell a.submit2 {
+ font-size: 14px;
+ margin: 6px 4px 2px;
+ padding: 2px 10px;
+}
+.comment-cell textarea {
+ display: block;
+ width: 100%;
+ max-width: 100%;
+ min-height: 140px;
+}
+.shelfpage {
+ display: inline-block;
+ margin-top: 5px;
+ padding: 0 15px 0 10px;
+}
+.shelf .result.user {
+ position: relative;
+ padding-bottom: 18px;
+ margin-top: 14px;
+}
+.revedit,
+.ordedit {
+ position: absolute;
+ font-size: 12px;
+}
+.revedit {
+ bottom: 5px;
+ left: 10px;
+}
+.ordedit {
+ bottom: 0;
+ right: 2px;
+}
+.ordedit input {
+ max-width: 60px;
+}
+.ordedit .submit2 {
+ margin-left: 2px;
+ font-size: 12px;
+ padding: 2px 4px 2px 2px;
+}
+.fic-cell {
+ text-align: left;
+ border: 1px solid #ccc;
+ -webkit-border-radius: 4px;
+ -moz-border-radius: 4px;
+ border-radius: 4px;
+ box-shadow: 5px 5px #ccc;
+ font-size: 17px;
+ line-height: 22px;
+ position: relative;
+ width: 100%;
+ overflow: visible;
+ margin: 0 0 10px;
+ padding: 5px 10px;
+}
+.fic-cell a,
+.fic-cell a:visited {
+ color: #66f;
+ text-decoration: none;
+}
+.fic-cell a:hover {
+ color: #282;
+ text-decoration: underline;
+}
+.fic-cell header img {
+ display: block;
+ float: left;
+ max-height: 150px;
+ max-width: 150px;
+ background-color: #fff;
+ border: 1px solid rgba(0, 0, 0, 0.2);
+ -webkit-border-radius: 4px;
+ -moz-border-radius: 4px;
+ border-radius: 4px;
+ margin: 0 15px 5px 0;
+ padding: 1px;
+}
+.fic-cell h1,
+.fic-cell h2 {
+ font-size: 26px;
+ line-height: 30px;
+ display: inline-block;
+ color: #366;
+ margin: 0;
+}
+.fic-cell .details h2 {
+ display: inline;
+}
+.fic-cell header a,
+.fic-cell header a:visited {
+ color: #366;
+ text-decoration: none;
+}
+.fic-cell header a:hover {
+ color: #366;
+ text-decoration: underline;
+}
+.fic-cell .author {
+ color: #666;
+ display: inline-block;
+ margin: 0 0 0 5px;
+}
+.fic-cell .popular {
+ display: block;
+ float: left;
+ overflow: hidden;
+ padding: 5px 10px;
+}
+.fic-cell .description {
+ overflow: hidden;
+ border: solid #adc 1px;
+ -webkit-border-radius: 4px;
+ -moz-border-radius: 4px;
+ border-radius: 4px;
+ font-family: Arial, Helvetica, sans-serif;
+ margin: 10px 0 0;
+ padding: 4px 12px 5px;
+}
+.fic-cell br {
+ line-height: 0;
+}
+.rating,
+.characters,
+.rating *,
+.characters * {
+ display: inline-block;
+ vertical-align: top;
+ margin-bottom: 2px;
+}
+.frating,
+.fstatus,
+.ftag {
+ font-size: 12px;
+ line-height: 16px;
+ border-radius: 4px;
+ -webkit-border-radius: 4px;
+ -moz-border-radius: 4px;
+ color: #000;
+ font-weight: 400;
+ font-family: Arial, Helvetica, sans-serif;
+ cursor: default;
+ text-align: center;
+ padding: 3px 8px;
+}
+.frating {
+ border: 1px solid rgba(0, 0, 0, 0.2);
+ text-shadow: 0 0 4px rgba(0, 0, 0, 0.4);
+}
+.frating.ev {
+ background: #3a3;
+ box-shadow: 0 1px 0 #292 inset;
+}
+.frating.ev:hover {
+ background: #4b4;
+}
+.frating.tn {
+ background: #ca0;
+ box-shadow: 0 1px 0 #b90 inset;
+}
+.frating.tn:hover {
+ background: #db1;
+}
+.frating.ma {
+ background: #b57;
+ box-shadow: 0 1px 0 #a46 inset;
+}
+.frating.ma:hover {
+ background: #c68;
+}
+.frating.ya {
+ background: #b73;
+ box-shadow: 0 1px 0 #a62 inset;
+}
+.frating.ya:hover {
+ background: #c84;
+}
+.frating.ad {
+ background: #88f;
+ box-shadow: 0 1px 0 #77e inset;
+}
+.frating.ad:hover {
+ background: #99f;
+}
+.frating.cl {
+ background: #d33;
+ box-shadow: 0 1px 0 #b11 inset;
+}
+.frating.cl:hover {
+ background: #e44;
+}
+.frating.cl2 {
+ background: #f66;
+ box-shadow: 0 1px 0 #d44 inset;
+}
+.frating.cl2:hover {
+ background: #f77;
+}
+.frating.db {
+ background: #ea7;
+ box-shadow: 0 1px 0 #d96 inset;
+}
+.frating.db:hover {
+ background: #fb8;
+}
+.frating.old {
+ font-size: 11px;
+ opacity: 0.6;
+ text-decoration: line-through;
+}
+.fstatus {
+ text-shadow: 0 0 8px rgba(255, 255, 255, 0.9);
+ border: 1px solid rgba(0, 0, 0, 0.2);
+}
+.fstatus:before {
+ color: #000;
+ width: 16px;
+ margin-right: 5px;
+ font-family: FontAwesome, "Trebuchet MS", Helvetica, sans-serif;
+}
+.fstatus.sc {
+ background: #5a5;
+ box-shadow: 0 1px 0 #494 inset;
+}
+.fstatus.sc:before {
+ content: "\f00c";
+}
+.fstatus.sc:hover {
+ background: #6b6;
+}
+.fstatus.si {
+ background: #fa1;
+ box-shadow: 0 1px 0 #fd0 inset;
+}
+.fstatus.si:before {
+ content: "\f040";
+}
+.fstatus.si:hover {
+ background: #fc0;
+}
+.fstatus.sh {
+ background: #b74;
+}
+.fstatus.sh:before {
+ content: "\f04c";
+}
+.fstatus.sh:hover {
+ background: #c85;
+}
+.fstatus.sn {
+ background: #b33;
+}
+.fstatus.sn:before {
+ content: "\f05e";
+}
+.fstatus.sn:hover {
+ background: #c44;
+}
+.ftag {
+ color: #fff;
+ text-shadow: 0 0 6px rgba(0, 0, 0, 0.9);
+ letter-spacing: 1px;
+}
+.ftag.adv {
+ background-color: #394;
+ box-shadow: 0 1px 0 #5b6 inset;
+ text-shadow: 0 0 3px #172;
+ border: 1px solid #172;
+}
+.ftag.adv:hover {
+ background-color: #283;
+}
+.ftag.rom {
+ background-color: #94f;
+ box-shadow: 0 1px 0 #b6f inset;
+ text-shadow: 0 0 3px #72d;
+ border: 1px solid #72d;
+}
+.ftag.rom:hover {
+ background-color: #83e;
+}
+.ftag.rnd {
+ background-color: #37c;
+ box-shadow: 0 1px 0 #59f inset;
+ text-shadow: 0 0 3px #15a;
+ border: 1px solid #15a;
+}
+.ftag.rnd:hover {
+ background-color: #26b;
+}
+.ftag.com {
+ background-color: #ca2;
+ box-shadow: 0 1px 0 #ec4 inset;
+ text-shadow: 0 0 3px #a80;
+ border: 1px solid #a80;
+}
+.ftag.com:hover {
+ background-color: #b91;
+}
+.ftag.lif {
+ background-color: #48f;
+ box-shadow: 0 1px 0 #6af inset;
+ text-shadow: 0 0 3px #26d;
+ border: 1px solid #26d;
+}
+.ftag.lif:hover {
+ background-color: #37e;
+}
+.ftag.trg {
+ background-color: #fb4;
+ box-shadow: 0 1px 0 #fd6 inset;
+ text-shadow: 0 0 3px #d92;
+ border: 1px solid #d92;
+}
+.ftag.trg:hover {
+ background-color: #ea3;
+}
+.ftag.sad {
+ background-color: #949;
+ box-shadow: 0 1px 0 #b6b inset;
+ text-shadow: 0 0 3px #727;
+ border: 1px solid #727;
+}
+.ftag.sad:hover {
+ background-color: #838;
+}
+.ftag.drk {
+ background-color: #b33;
+ box-shadow: 0 1px 0 #d55 inset;
+ text-shadow: 0 0 3px #911;
+ border: 1px solid #911;
+}
+.ftag.drk:hover {
+ background-color: #a22;
+}
+.ftag.alt {
+ background-color: #888;
+ box-shadow: 0 1px 0 #aaa inset;
+ text-shadow: 0 0 3px #666;
+ border: 1px solid #666;
+}
+.ftag.alt:hover {
+ background-color: #777;
+}
+.ftag.crs {
+ background-color: #3a9;
+ box-shadow: 0 1px 0 #5cb inset;
+ text-shadow: 0 0 3px #187;
+ border: 1px solid #187;
+}
+.ftag.crs:hover {
+ background-color: #298;
+}
+.ftag.hum {
+ background-color: #b85;
+ box-shadow: 0 1px 0 #da7 inset;
+ text-shadow: 0 0 3px #963;
+ border: 1px solid #963;
+}
+.ftag.hum:hover {
+ background-color: #a74;
+}
+.ftag.ath {
+ background-color: #b65;
+ box-shadow: 0 1px 0 #d87 inset;
+ text-shadow: 0 0 3px #943;
+ border: 1px solid #943;
+}
+.ftag.ath:hover {
+ background-color: #a54;
+}
+.ftag.gor {
+ background-color: #722;
+ box-shadow: 0 1px 0 #944 inset;
+ text-shadow: 0 0 3px #500;
+ border: 1px solid #500;
+}
+.ftag.gor:hover {
+ background-color: #611;
+}
+.ftag.sex {
+ background-color: #c49;
+ box-shadow: 0 1px 0 #e6b inset;
+ text-shadow: 0 0 3px #a27;
+ border: 1px solid #a27;
+}
+.ftag.sex:hover {
+ background-color: #b38;
+}
+.ftag.p2 {
+ background-color: #48a;
+ box-shadow: 0 1px 0 #6ac inset;
+ text-shadow: 0 0 3px #268;
+ border: 1px solid #268;
+}
+.ftag.p2:hover {
+ background-color: #59b;
+}
+.ftag.thr {
+ background-color: #c36;
+ box-shadow: 0 1px 0 #e58 inset;
+ text-shadow: 0 0 3px #a14;
+ border: 1px solid #a14;
+}
+.ftag.thr:hover {
+ background-color: #b25;
+}
+.ftag.dra {
+ background-color: #d5d;
+ box-shadow: 0 1px 0 #f7f inset;
+ text-shadow: 0 0 3px #c3b;
+ border: 1px solid #c3b;
+}
+.ftag.dra:hover {
+ background-color: #d4c;
+}
+.ftag.hor {
+ background-color: #622;
+ box-shadow: 0 1px 0 #844 inset;
+ text-shadow: 0 0 3px #400;
+ border: 1px solid #400;
+}
+.ftag.hor:hover {
+ background-color: #511;
+}
+.ftag.eqg {
+ background-color: #538;
+ box-shadow: 0 1px 0 #75a inset;
+ text-shadow: 0 0 3px #316;
+ border: 1px solid #316;
+}
+.ftag.eqg:hover {
+ background-color: #427;
+}
+.ftag.mys {
+ background-color: #444;
+ box-shadow: 0 1px 0 #666 inset;
+ text-shadow: 0 0 3px #222;
+ border: 1px solid #222;
+}
+.ftag.mys:hover {
+ background-color: #333;
+}
+.ftag.sci {
+ background-color: #66a;
+ box-shadow: 0 1px 0 #88c inset;
+ text-shadow: 0 0 3px #448;
+ border: 1px solid #448;
+}
+.ftag.sci:hover {
+ background-color: #559;
+}
+.ftag.pro {
+ background-color: #c62;
+ box-shadow: 0 1px 0 #e84 inset;
+ text-shadow: 0 0 3px #a40;
+ border: 1px solid #a40;
+}
+.ftag.pro:hover {
+ background-color: #b51;
+}
+.ftag.vil {
+ background-color: #d33;
+ box-shadow: 0 1px 0 #f55 inset;
+ text-shadow: 0 0 3px #b11;
+ border: 1px solid #b11;
+}
+.ftag.vil:hover {
+ background-color: #c22;
+}
+.ftag.dth {
+ background-color: #333;
+ box-shadow: 0 1px 0 #555 inset;
+ text-shadow: 0 0 3px #111;
+ border: 1px solid #111;
+}
+.ftag.dth:hover {
+ background-color: #222;
+}
+.ftag.hrm {
+ background-color: #c22;
+ box-shadow: 0 1px 0 #e44 inset;
+ text-shadow: 0 0 3px #a00;
+ border: 1px solid #a00;
+}
+.ftag.hrm:hover {
+ background-color: #b11;
+}
+.character {
+ -webkit-border-radius: 6px;
+ -moz-border-radius: 6px;
+ border-radius: 6px;
+ display: inline-block;
+ max-height: 24px;
+}
+.published,
+.updated {
+ color: #888;
+ font: 10pt "Lucida Sans Unicode", "Lucida Grande", sans-serif;
+ line-height: 10pt;
+ white-space: pre-wrap;
+}
+.ficstats {
+ display: inline-block;
+ padding: 5px 0;
+}
+span.star-rating {
+ font-size: 13px;
+ line-height: 13px;
+ overflow: auto;
+ margin: 4px 0 0;
+}
+span.star-rating .stars-sm {
+ float: left;
+ margin-right: 4px;
+}
+.stars-txt {
+ display: inline-block;
+ line-height: 23px;
+}
+.stars {
+ width: 190px;
+ display: inline-block;
+}
+.stars-sm {
+ display: inline-block;
+ padding: 1px 0 0 4px;
+}
+label.star {
+ float: right;
+ font-size: 30px;
+ color: #444;
+ transition: all 0.2s;
+ padding: 10px 5px;
+}
+.stars-sm label.star {
+ font-size: 20px;
+ padding: 2px;
+}
+input.star:checked ~ label.star:before {
+ content: "\f005";
+ color: #fd4;
+ transition: all 0.25s;
+}
+input.star-5:checked ~ label.star:before {
+ color: #fe7;
+ text-shadow: 0 0 20px #952;
+}
+.stars-sm input.star-5:checked ~ label.star:before {
+ text-shadow: 0 0 10px #952;
+}
+label.star:hover {
+ transform: rotate(-15deg) scale(1.3);
+}
+label.star:before {
+ content: "\f006";
+ font-family: FontAwesome;
+}
+span.star {
+ float: right;
+ width: 20px;
+ height: 20px;
+ line-height: 20px;
+ font-size: 20px;
+ position: relative;
+ margin: 2px;
+ padding: 0;
+}
+span.star:before {
+ position: absolute;
+ width: 100%;
+ height: 100%;
+ top: 0;
+ left: 0;
+ font-family: FontAwesome;
+ content: "\f006";
+ color: #444;
+}
+span.star.star-filled:before,
+span.star.star-filled ~ span.star:before,
+span.star.star-half ~ span.star:before {
+ content: "\f005";
+ color: #fd4;
+ text-shadow: 1px 1px 1px #220, 0 0 1px #220;
+}
+span.star.star-half:before {
+ content: "\f005";
+ color: #888;
+ text-shadow: 1px 1px 1px #220, 0 0 1px #220;
+}
+span.star.star-half:after {
+ position: absolute;
+ font-family: FontAwesome;
+ content: "\f005";
+ color: #fd4;
+ width: 40%;
+ overflow: hidden;
+}
+span.star.star-filled.star-5:before,
+span.star.star-filled.star-5 ~ span.star:before {
+ content: "\f005";
+ color: #fe7;
+ text-shadow: 0 0 20px #952;
+}
+.stars-sm span.star.star-filled.star-5:before,
+.stars-sm span.star.star-filled.star-5 ~ span.star:before {
+ text-shadow: 1px 1px 1px #220, 0 0 1px #220, 0 0 5px #f86, 0 0 10px #952;
+}
+.fic-tiles {
+ display: block;
+ text-align: center;
+}
+.fic-tiles .fic-cell {
+ width: 340px;
+ display: inline-block;
+ overflow: hidden;
+ font-size: 13px;
+ line-height: 13px;
+ box-shadow: 2px 2px #ccc;
+ vertical-align: top;
+ margin: 3px;
+ padding: 4px;
+}
+.fic-tiles .fic-cell h1,
+.fic-tiles .fic-cell h2 {
+ font-size: 22px;
+ line-height: 22px;
+ padding: 2px 8px 3px 0;
+}
+.fic-tiles .fic-cell header img {
+ max-height: 90px;
+ max-width: 90px;
+ margin: 0 8px 5px 0;
+}
+.fic-tiles .popular .stats {
+ display: none !important;
+}
+.fic-tiles .fic-cell .popular {
+ line-height: 0;
+ padding: 2px 0;
+}
+.fic-tiles .fic-cell .star-rating {
+ padding: 1px 0 0;
+}
+.fic-tiles span.star {
+ width: 16px;
+ height: 16px;
+ line-height: 16px;
+ font-size: 16px;
+}
+.fic-tiles .fic-cell .description {
+ margin: 2px 0;
+}
+.fic-tiles .ficsrc,
+.fic-tiles .character {
+ max-height: 20px;
+}
+.fic-tiles .frating,
+.fic-tiles .fstatus,
+.fic-tiles .ftag {
+ font-size: 10px;
+ line-height: 14px;
+ color: transparent;
+ width: 25px;
+ height: 20px;
+ overflow: hidden;
+ padding: 2px 0;
+}
+.fic-tiles .frating:before,
+.fic-tiles .ftag:before,
+.fic-tiles .fstatus:before {
+ color: #000;
+ width: 16px;
+ display: inline-block;
+ text-align: center;
+ font-weight: 700;
+ margin: 0;
+}
+.fic-tiles .frating.ev:before {
+ content: "E";
+}
+.fic-tiles .frating.tn:before {
+ content: "T";
+}
+.fic-tiles .frating.ma:before {
+ content: "M";
+}
+.fic-tiles .frating.ya:before {
+ content: "YA";
+}
+.fic-tiles .frating.ad:before {
+ content: "AD";
+}
+.fic-tiles .frating.cl:before {
+ content: "X";
+}
+.fic-tiles .frating.db:before {
+ content: "?";
+}
+.fic-tiles .ftag.adv:before {
+ content: "Ad";
+}
+.fic-tiles .ftag.rom:before {
+ content: "Ro";
+}
+.fic-tiles .ftag.rnd:before {
+ content: "Ra";
+}
+.fic-tiles .ftag.com:before {
+ content: "Co";
+}
+.fic-tiles .ftag.lif:before {
+ content: "Li";
+}
+.fic-tiles .ftag.trg:before {
+ content: "Tr";
+}
+.fic-tiles .ftag.sad:before {
+ content: "Sa";
+}
+.fic-tiles .ftag.drk:before {
+ content: "Da";
+}
+.fic-tiles .ftag.alt:before {
+ content: "AU";
+}
+.fic-tiles .ftag.crs:before {
+ content: "Cr";
+}
+.fic-tiles .ftag.hum:before {
+ content: "Hu";
+}
+.fic-tiles .ftag.ath:before {
+ content: "An";
+}
+.fic-tiles .ftag.gor:before {
+ content: "Go";
+}
+.fic-tiles .ftag.sex:before {
+ content: "Se";
+}
+.fic-tiles .ftag.p2:before {
+ content: "2p";
+}
+.fic-tiles .ftag.thr:before {
+ content: "Th";
+}
+.fic-tiles .ftag.dra:before {
+ content: "Dr";
+}
+.fic-tiles .ftag.hor:before {
+ content: "Ho";
+}
+.fic-tiles .ftag.eqg:before {
+ content: "Eq";
+}
+.fic-tiles .ftag.mys:before {
+ content: "My";
+}
+.fic-tiles .ftag.sci:before {
+ content: "Sc";
+}
+.fic-tiles .ftag.pro:before {
+ content: "Pf";
+}
+.fic-tiles .ftag.vil:before {
+ content: "Vi";
+}
+.fic-tiles .ftag.dth:before {
+ content: "De";
+}
+.fic-tiles .ftag.hrm:before {
+ content: "Ha";
+}
+.fic-tiles .ficstats {
+ width: 100%;
+ color: #616161;
+ padding: 5px 0 0;
+}
+.fic-tiles .ficstats .chapters {
+ float: left;
+ margin-right: 3px;
+}
+.fic-tiles .readrev {
+ position: absolute;
+ right: 6px;
+ bottom: 3px;
+}
+body > footer {
+ min-height: 165px;
+ background-color: #e9e9e9;
+ color: #777;
+}
+body > footer section {
+ text-align: left;
+ display: inline-block;
+ vertical-align: top;
+ width: 200px;
+ min-height: 150px;
+ position: relative;
+ padding: 0 10px 10px;
+}
+body > footer section:nth-child(2) {
+ width: 300px;
+}
+body > footer section + section:before {
+ position: absolute;
+ display: block;
+ content: "";
+ top: 15px;
+ left: -3px;
+ height: 130px;
+ border-left: 1px solid #bbb;
+}
+body > footer .stat {
+ display: block;
+ font-size: 80%;
+ margin-bottom: 4px;
+}
+body > footer h4 {
+ border-bottom: 1px solid #bbb;
+ padding-bottom: 10px;
+ margin-bottom: 14px;
+}
+body > footer .media-links svg {
+ max-height: 40px;
+ max-width: 40px;
+ display: block;
+ float: left;
+ margin: 2px 5px;
+}
+body > footer .media-links {
+ height: 40px;
+}
+body > footer .fb-logo {
+ background-color: #fff;
+}
+body > footer input[type="image"] {
+ margin: 6px 0 0;
+}
+body.fotd .result {
+ display: inline-block;
+ width: 100%;
+ max-width: 700px;
+ padding: 0;
+}
+body.fotd .pagenav {
+ display: inline-block;
+ padding: 0;
+}
+.fotd.sm {
+ max-width: 575px;
+ text-align: center;
+ margin: 90px auto 10px;
+ padding: 0 15px 0 10px;
+}
+.fotd.sm .fic-cell .popular.popsmall {
+ display: block !important;
+ font-size: 14px;
+ line-height: 14px;
+}
+article.fotd {
+ display: inline-block;
+ width: 100%;
+ position: relative;
+ -webkit-box-shadow: 0 3px 10px rgba(0, 0, 0, 0.7);
+ box-shadow: 0 3px 10px rgba(0, 0, 0, 0.7);
+ background-color: #fff;
+ padding: 8px 10px;
+}
+article.fotd > header {
+ font: normal 50px / normal Helvetica, sans-serif;
+ color: #0d242d;
+ text-shadow: 0 1px 0 #ccc, 0 2px 0 #c9c9c9;
+ margin: 0;
+ padding: 0 0 10px;
+}
+article.fotd > footer {
+ width: 90%;
+ border-top: 1px solid #000;
+ border-right: 20px solid #fff;
+ border-left: 20px solid #fff;
+ margin: 10px auto 0;
+ padding: 10px 0;
+}
+article.fotd .fic-cell {
+ border: none;
+ -webkit-border-radius: 0;
+ -moz-border-radius: 0;
+ border-radius: 0;
+ box-shadow: none;
+ min-height: 0;
+ margin: 0;
+ padding: 0;
+}
+.fic-cell .fotd-day {
+ display: block;
+ -webkit-border-top-left-radius: 4px;
+ -moz-border-radius-topleft: 4px;
+ border-top-left-radius: 4px;
+ -webkit-border-top-right-radius: 4px;
+ -moz-border-radius-topright: 4px;
+ border-top-right-radius: 4px;
+ text-align: center;
+ border-bottom: 1px solid #ccc;
+ box-shadow: 0 0 4px #777 inset;
+ font: bold 23px "Palatino Linotype", "Book Antiqua", Palatino, serif;
+ margin: -5px -10px 10px;
+ padding: 10px 5px;
+}
+.loginbox {
+ position: fixed;
+ z-index: 1001;
+ top: 10%;
+ left: 50%;
+ width: 360px;
+ height: auto;
+ display: block;
+ background: #fff;
+ border-radius: 5px;
+ box-shadow: 0 0 10px rgba(0, 0, 0, 0.4);
+ text-align: center;
+ border: 1px solid #b4b1b1;
+ visibility: hidden;
+ line-height: 1em;
+ -webkit-transform: translate(-50%, -50%);
+ -moz-transform: translate(-50%, -50%);
+ -ms-transform: translate(-50%, -50%);
+ -o-transform: translate(-50%, -50%);
+ transform: translate(-50%, -50%);
+ -webkit-transition: opacity 0.5s, top 0.5s;
+ -moz-transition: opacity 0.5s, top 0.5s;
+ -ms-transition: opacity 0.5s, top 0.5s;
+ -o-transition: opacity 0.5s, top 0.5s;
+ transition: opacity 0.5s, top 0.5s;
+ padding: 15px;
+}
+.overlay:target + .loginbox {
+ top: 50%;
+ opacity: 1;
+ visibility: visible;
+}
+.overlay:target {
+ visibility: visible;
+ opacity: 1;
+}
+.overlay {
+ position: fixed;
+ opacity: 0;
+ visibility: hidden;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ left: 0;
+ background-color: rgba(0, 0, 0, 0.8);
+ cursor: default;
+ z-index: 1000;
+ -webkit-transition: opacity 0.5s;
+ -moz-transition: opacity 0.5s;
+ -ms-transition: opacity 0.5s;
+ -o-transition: opacity 0.5s;
+ transition: opacity 0.5s;
+}
+.loginbox h1 {
+ font-family: Montserrat, sans-serif;
+ color: #803271;
+ line-height: 1.5em;
+ font-weight: 700;
+ font-size: 190%;
+ margin: 0 0 5px;
+}
+.loginbox .sep {
+ position: relative;
+ border-top: 2px solid #888;
+ border-left: 5px solid transparent;
+ border-right: 5px solid transparent;
+ margin: 15px;
+}
+.loginbox .sep > div {
+ position: absolute;
+ top: -8px;
+ left: 50%;
+ background: linear-gradient(90deg, rgba(255, 255, 255, 0.1), #fff, #fff, #fff, rgba(255, 255, 255, 0.1));
+ width: 180px;
+ margin-left: -90px;
+}
+.loginbox .fb-logo {
+ height: 32px;
+ width: 32px;
+ border-radius: 2px;
+ background: #4c69ba;
+ margin: 2px 5px;
+ padding: 4px;
+}
+.loginbox .fb-logo:hover {
+ background: #5b7bd5;
+ cursor: pointer;
+}
+.loginbox .fblogin > div {
+ display: inline-block;
+ vertical-align: middle;
+}
+.loginbox .fbprompt {
+ margin-right: 10px;
+ padding-right: 13px;
+ border-right: 1px solid #888;
+ border-top: 10px solid transparent;
+ border-bottom: 10px solid transparent;
+}
+.loginbox .fflogin {
+ padding-top: 10px;
+}
+.loginbox .info {
+ text-align: right;
+ width: 250px;
+ font-size: 13px;
+ margin: 0 auto;
+}
+.loginbox .info input {
+ display: inline-block;
+ width: 160px;
+ border: 1px solid #c0c2c7;
+ -webkit-border-radius: 2px;
+ -moz-border-radius: 2px;
+ border-radius: 2px;
+ color: #161717;
+ font-style: none;
+ margin-bottom: 12px;
+ background-color: #fff;
+ -moz-box-shadow: inset 0 1px 3px -1px #b4b1b1;
+ -webkit-box-shadow: inset 0 1px 3px -1px #b4b1b1;
+ box-shadow: inset 0 1px 3px -1px #b4b1b1;
+ padding: 6px;
+}
+.loginbox .info input:focus {
+ outline: none;
+ box-shadow: 0 0 4px rgba(0, 0, 0, 0.2), 0 0 5px 1px #51cbee;
+}
+.fflogin.loginerror .info input {
+ box-shadow: 0 0 6px 1px red, inset 0 0 3px 1px red;
+}
+.lerrbox {
+ border: 1px solid #000;
+ width: 90%;
+ margin: -10px auto 15px;
+ padding: 10px 5px;
+}
+.loginbox .loginbuttons {
+ vertical-align: top;
+}
+.loginbox input.login {
+ border: 1px solid;
+ font-size: 14px;
+ cursor: pointer;
+ color: #fff;
+ background: #09f;
+ -webkit-border-radius: 5px;
+ -moz-border-radius: 5px;
+ border-radius: 5px;
+ padding: 6px 28px;
+}
+.loginbox a.register {
+ color: #666 !important;
+ font: 11pt Verdana, Geneva, sans-serif;
+ display: inline-block;
+ padding: 6px 8px 0;
+}
+.loginremember {
+ height: 25px;
+ line-height: 25px;
+ font-family: LatoRegular;
+ color: #7e7e7e;
+ font-size: 12px;
+ margin-bottom: -8px;
+}
+.alogin a {
+ position: absolute;
+ display: block;
+ top: 0;
+ right: 0;
+ z-index: 1;
+ color: #33f;
+ text-decoration: none;
+ font-family: Verdana, Geneva, sans-serif;
+ padding: 10px 10px 15px 15px;
+}
+.alogin a:visited {
+ color: #33f;
+}
+.alogin a:hover {
+ color: #00f;
+ font-weight: 700;
+}
+@font-face {
+ font-family: PonyEmoji;
+ src: url(/fonts/ponyemoji.ttf);
+ font-weight: 400;
+ font-style: normal;
+}
+.fa-pony-rd-happy:before {
+ font-family: PonyEmoji;
+ content: "\1f600";
+}
+.fa-pony-ab-beam:before {
+ font-family: PonyEmoji;
+ content: "\1f601";
+}
+.fa-pony-scoots-happy:before {
+ font-family: PonyEmoji;
+ content: "\1f602";
+}
+.fa-pony-twi-happy:before {
+ font-family: PonyEmoji;
+ content: "\1f603";
+}
+.fa-pony-rd-grin:before {
+ font-family: PonyEmoji;
+ content: "\1f604";
+}
+.fa-pony-lyra-unsure:before {
+ font-family: PonyEmoji;
+ content: "\1f605";
+}
+.fa-pony-flutter-yay:before {
+ font-family: PonyEmoji;
+ content: "\1f606";
+}
+.fa-pony-sweetie-angel:before {
+ font-family: PonyEmoji;
+ content: "\1f607";
+}
+.fa-pony-twi-crazy:before {
+ font-family: PonyEmoji;
+ content: "\1f608";
+}
+.fa-pony-flutter-wink:before {
+ font-family: PonyEmoji;
+ content: "\1f609";
+}
+.fa-pony-lyra-happy:before {
+ font-family: PonyEmoji;
+ content: "\1f60a";
+}
+.fa-pony-pinkie-silly:before {
+ font-family: PonyEmoji;
+ content: "\1f60b";
+}
+.fa-pony-rd-beam:before {
+ font-family: PonyEmoji;
+ content: "\1f60c";
+}
+.fa-pony-scoots-love:before {
+ font-family: PonyEmoji;
+ content: "\1f60d";
+}
+.fa-pony-rd-cool:before {
+ font-family: PonyEmoji;
+ content: "\1f60e";
+}
+.fa-pony-twi-smirk:before {
+ font-family: PonyEmoji;
+ content: "\1f60f";
+}
+.fa-pony-flutter-stare:before {
+ font-family: PonyEmoji;
+ content: "\1f610";
+}
+.fa-pony-aj-unsure:before {
+ font-family: PonyEmoji;
+ content: "\1f611";
+}
+.fa-pony-rd-unamused:before {
+ font-family: PonyEmoji;
+ content: "\1f612";
+}
+.fa-pony-ab-unsure:before {
+ font-family: PonyEmoji;
+ content: "\1f613";
+}
+.fa-pony-rd-pensive:before {
+ font-family: PonyEmoji;
+ content: "\1f614";
+}
+.fa-pony-zecora-confused:before {
+ font-family: PonyEmoji;
+ content: "\1f615";
+}
+.fa-pony-aj-disgust:before {
+ font-family: PonyEmoji;
+ content: "\1f616";
+}
+.fa-pony-rd-soawesome:before {
+ font-family: PonyEmoji;
+ content: "\1f617";
+}
+.fa-pony-aj-jewel-kiss:before {
+ font-family: PonyEmoji;
+ content: "\1f618";
+}
+.fa-pony-rd-startled:before {
+ font-family: PonyEmoji;
+ content: "\1f619";
+}
+.fa-pony-aj-pensive:before {
+ font-family: PonyEmoji;
+ content: "\1f61a";
+}
+.fa-pony-fluffles:before {
+ font-family: PonyEmoji;
+ content: "\1f61b";
+}
+.fa-pony-flutter-grin:before {
+ font-family: PonyEmoji;
+ content: "\1f61c";
+}
+.fa-pony-aj-cheer:before {
+ font-family: PonyEmoji;
+ content: "\1f61d";
+}
+.fa-pony-rd-disappointed:before {
+ font-family: PonyEmoji;
+ content: "\1f61e";
+}
+.fa-pony-pinkie-worried:before {
+ font-family: PonyEmoji;
+ content: "\1f61f";
+}
+.fa-pony-rarity-angry:before {
+ font-family: PonyEmoji;
+ content: "\1f620";
+}
+.fa-pony-aj-angry:before {
+ font-family: PonyEmoji;
+ content: "\1f621";
+}
+.fa-pony-ab-sad:before {
+ font-family: PonyEmoji;
+ content: "\1f622";
+}
+.fa-pony-twi-angry:before {
+ font-family: PonyEmoji;
+ content: "\1f623";
+}
+.fa-pony-pinkie-angry:before {
+ font-family: PonyEmoji;
+ content: "\1f624";
+}
+.fa-pony-scoots-sad:before {
+ font-family: PonyEmoji;
+ content: "\1f625";
+}
+.fa-pony-rarity-shocked:before {
+ font-family: PonyEmoji;
+ content: "\1f626";
+}
+.fa-pony-aj-scared:before {
+ font-family: PonyEmoji;
+ content: "\1f627";
+}
+.fa-pony-rarity-scared:before {
+ font-family: PonyEmoji;
+ content: "\1f628";
+}
+.fa-pony-pinkie-cry:before {
+ font-family: PonyEmoji;
+ content: "\1f629";
+}
+.fa-pony-twi-scared:before {
+ font-family: PonyEmoji;
+ content: "\1f62a";
+}
+.fa-pony-flutter-angry:before {
+ font-family: PonyEmoji;
+ content: "\1f62b";
+}
+.fa-pony-aj-shocked:before {
+ font-family: PonyEmoji;
+ content: "\1f62c";
+}
+.fa-pony-flutter-cry:before {
+ font-family: PonyEmoji;
+ content: "\1f62d";
+}
+.fa-pony-rd-shocked:before {
+ font-family: PonyEmoji;
+ content: "\1f62e";
+}
+.fa-pony-twi-unsure:before {
+ font-family: PonyEmoji;
+ content: "\1f62f";
+}
+.fa-pony-ab-scared:before {
+ font-family: PonyEmoji;
+ content: "\1f630";
+}
+.fa-pony-rd-scared:before {
+ font-family: PonyEmoji;
+ content: "\1f631";
+}
+.fa-pony-spike-worried:before {
+ font-family: PonyEmoji;
+ content: "\1f632";
+}
+.fa-pony-flutter-blush:before {
+ font-family: PonyEmoji;
+ content: "\1f633";
+}
+.fa-pony-ab-sleep:before {
+ font-family: PonyEmoji;
+ content: "\1f634";
+}
+.fa-pony-ab-cry:before {
+ font-family: PonyEmoji;
+ content: "\1f635";
+}
+.fa-pony-mac:before {
+ font-family: PonyEmoji;
+ content: "\1f636";
+}
+.fa-pony-mask:before {
+ font-family: PonyEmoji;
+ content: "\1f637";
+}
+.fa-pony-ab-happy:before {
+ font-family: PonyEmoji;
+ content: "\1f638";
+}
+.fa-pony-twi-laugh:before {
+ font-family: PonyEmoji;
+ content: "\1f639";
+}
+.fa-pony-rarity-happy:before {
+ font-family: PonyEmoji;
+ content: "\1f63a";
+}
+.fa-pony-ab-love:before {
+ font-family: PonyEmoji;
+ content: "\1f63b";
+}
+.fa-pony-flutter-wrygrin:before {
+ font-family: PonyEmoji;
+ content: "\1f63c";
+}
+.fa-pony-pinkie-duckface:before {
+ font-family: PonyEmoji;
+ content: "\1f63d";
+}
+.fa-pony-pinkie-glare:before {
+ font-family: PonyEmoji;
+ content: "\1f63e";
+}
+.fa-pony-sweetie-sad:before {
+ font-family: PonyEmoji;
+ content: "\1f63f";
+}
+.fa-pony-twi-sad:before {
+ font-family: PonyEmoji;
+ content: "\1f640";
+}
+.fa-pony-flutter-worried:before {
+ font-family: PonyEmoji;
+ content: "\1f641";
+}
+.fa-pony-pinkie:before {
+ font-family: PonyEmoji;
+ content: "\1f642";
+}
+.fa-pony-pinkie-party:before {
+ font-family: PonyEmoji;
+ content: "\1f644";
+}
+.user {
+ display: inline-block;
+ max-width: 1100px;
+ width: 100%;
+ text-align: left;
+ overflow: visible;
+ margin: 30px 0 10px;
+ padding: 0 15px;
+}
+.user .name h1 {
+ font: bold 34px "Times New Roman", Times, serif;
+ word-break: break-all;
+ display: inline-block;
+ margin: 2px;
+}
+.user .name img {
+ -webkit-border-radius: 10px;
+ -moz-border-radius: 10px;
+ border-radius: 10px;
+ float: left;
+ top: 2px;
+ position: relative;
+ margin-right: 5px;
+}
+.user .avatar {
+ position: relative;
+ float: left;
+ border: 1px solid #333;
+ -webkit-border-radius: 4px;
+ -moz-border-radius: 4px;
+ border-radius: 4px;
+ overflow: hidden;
+ margin: 0 15px 5px 0;
+ padding: 3px;
+}
+.user .avatar:after {
+ content: "";
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+}
+.user .avatar img {
+ display: block;
+ background-color: #cacaca;
+ max-height: 200px;
+ max-width: 200px;
+ width: 100%;
+ height: 100%;
+}
+.user .avatar img.preview {
+ max-height: none;
+ max-width: none;
+ width: auto !important;
+ height: auto !important;
+ z-index: 1;
+}
+.user .avatar a {
+ -webkit-border-radius: 4px;
+ -moz-border-radius: 4px;
+ border-radius: 4px;
+ background-color: rgba(255, 255, 255, 0.7);
+ -webkit-transition-delay: 0.5s;
+ transition-delay: 0.5s;
+ transition: opacity 0.4s ease-out, padding 0.4s ease-out;
+ z-index: 99;
+ padding: 1px 5px;
+}
+.user .avatar a:hover {
+ opacity: 1 !important;
+ text-decoration: none;
+ padding: 4px 5px 1px 9px;
+}
+.user .avatar a.change {
+ position: absolute;
+ right: 5px;
+ bottom: 5px;
+ opacity: 0.5;
+}
+.user .avatar a.save {
+ position: absolute;
+ left: 5px;
+ bottom: 5px;
+ visibility: hidden;
+}
+.user .avatar a.save:hover {
+ padding: 4px 9px 1px 5px;
+}
+body.droppable .avatar {
+ border: 5px dashed #add8e6;
+ z-index: 9999;
+}
+.user.patreon .info .name,
+.user.admin .info .name {
+ display: inline-block;
+ position: relative;
+}
+.user.patreon .info .name:after {
+ content: "Patreon Supporter!";
+ line-height: 30px;
+ background: 3px 1px url(/img/patreon-logo.svg) no-repeat;
+ background-size: 30px;
+ display: block;
+ height: 30px;
+ width: min-content;
+ white-space: nowrap;
+ overflow: hidden;
+ top: 2px;
+ right: 4px;
+ z-index: 2;
+ background-color: #ffe971;
+ box-shadow: 0 1px 1px 1px #555;
+ margin: 0 0 5px 3px;
+ padding: 2px 2px 1px 40px;
+}
+.user.admin .info .name:after {
+ content: "Admin";
+ line-height: 30px;
+ background: 3px 1px url(/img/logo-admin.svg) no-repeat;
+ background-size: 30px;
+ display: block;
+ height: 30px;
+ width: min-content;
+ white-space: nowrap;
+ overflow: hidden;
+ top: 2px;
+ right: 4px;
+ z-index: 2;
+ background-color: #ffd578;
+ box-shadow: 0 1px 1px 1px #555;
+ margin: 0 0 5px 3px;
+ padding: 2px 8px 1px 36px;
+}
+.avatar a.rot {
+ position: absolute;
+ top: 2px;
+ visibility: hidden;
+ padding: 5px;
+}
+.user .avatar a.rot:hover {
+ padding: 5px;
+}
+.avatar .rot.rotr {
+ right: 2px;
+}
+.avatar .rot.rotl {
+ left: 2px;
+ -moz-transform: scaleX(-1);
+ -o-transform: scaleX(-1);
+ -webkit-transform: scaleX(-1);
+ transform: scaleX(-1);
+ filter: FlipH;
+ -ms-filter: FlipH;
+}
+.avatar .rot.rotm {
+ left: 50%;
+ margin-left: -30px;
+}
+#profile-loading {
+ position: absolute;
+ top: 0;
+ visibility: hidden;
+ z-index: 2;
+}
+.loading {
+ background-color: transparent !important;
+ -webkit-animation: spin 2s linear infinite;
+ -moz-animation: spin 2s linear infinite;
+ animation: spin 2s linear infinite;
+}
+.user header {
+ display: block;
+ overflow: auto;
+}
+.user .bio {
+ overflow: hidden;
+ border: solid #adc 1px;
+ -webkit-border-radius: 4px;
+ -moz-border-radius: 4px;
+ border-radius: 4px;
+ margin: 5px 0;
+ padding: 4px 12px 5px;
+}
+.user .info label {
+ color: #666;
+}
+.user .online {
+ color: #2a2;
+}
+.user .online:before {
+ width: 16px;
+ font-size: 16px;
+ display: inline-block;
+ text-align: center;
+ font-weight: 700;
+ content: "â—";
+ margin: 0;
+}
+.comment-cell.review-popup {
+ margin: -4px 10px 15px 18px;
+}
+body.main {
+ text-align: left;
+}
+.searchbox {
+ background: #fff;
+ height: 34px;
+ box-sizing: border-box;
+ -webkit-box-sizing: border-box;
+ border: 1px solid #ddd;
+ border-top-color: #ccc;
+ -webkit-transition: all 0.3s ease-in-out;
+ -moz-transition: all 0.3s ease-in-out;
+ -ms-transition: all 0.3s ease-in-out;
+ -o-transition: all 0.3s ease-in-out;
+ margin: 12px 0;
+}
+.searchbox:hover {
+ border: 1px solid #999;
+}
+.searchboxactive {
+ box-shadow: 0 0 5px #51cbee !important;
+ border: 1px solid #51cbee !important;
+}
+.searchbox input {
+ font-weight: lighter;
+ font-family: arial, sans-serif;
+ font-size: 14pt;
+ width: 98%;
+ border: none;
+ outline: none;
+ background-color: inherit;
+ margin: 0;
+ padding: 5px;
+}
+.search input[type="submit"] {
+ min-width: 150px;
+ background-color: #eee;
+ border: 1px solid #999;
+ color: #333;
+ margin: 10px 5px;
+ padding: 5px;
+}
+.search input[type="submit"]:hover {
+ background: #f3f9e7;
+}
+.search input[type="submit"]:active {
+ background: #badc74;
+}
+.search-adv-link {
+ display: block;
+ font: 10pt Verdana, Geneva, sans-serif;
+ margin: 8px 5px;
+}
+.search-adv-link.active a {
+ background-color: #ff8;
+ padding: 0 4px;
+}
+.search-adv-link.active.expanded a {
+ background-color: transparent;
+}
+.search-adv-link a,
+.search-adv-link a:visited {
+ font-size: 9pt;
+ line-height: 12pt;
+ color: #33f;
+}
+.search-adv-link a:hover {
+ color: #009;
+}
+.search-adv-link.expanded {
+ font-size: 12pt;
+ font-weight: 700;
+ margin: 8px 5px 2px;
+}
+.search-adv-link.expanded a,
+.search-adv-link.expanded a:visited {
+ font-size: 12pt;
+ color: #000;
+ text-decoration: none;
+}
+.search-adv {
+ max-width: 600px;
+ border: 1px #333 solid;
+ margin: 0 0 10px;
+ padding: 5px;
+}
+.search-adv div.opt {
+ text-align: left;
+ float: left;
+ min-height: 168px;
+ padding: 2px 4px;
+}
+.search-adv div.opt2 {
+ min-height: 90px;
+}
+.search-adv div.opt > div {
+ padding-bottom: 2px;
+ margin-bottom: 10px;
+ min-height: 0;
+}
+.search-adv div.opt div:last-of-type {
+ min-height: 0;
+ margin-bottom: 0;
+}
+.search-adv div.opt.full {
+ float: none;
+ width: 100%;
+ clear: both;
+ min-height: 0;
+}
+.search-adv div.opt.bottom {
+ min-height: 0;
+}
+.search-adv div.opt b {
+ display: inline-block;
+ padding-top: 3px;
+ margin-left: -2px;
+ text-shadow: 0 0 4px #ccc;
+ font-variant: small-caps;
+ font-family: sans-serif;
+}
+.search-adv input[type="number"],
+.search-adv .number {
+ max-width: 100px;
+ margin: 0 3px;
+}
+.search-adv div.opt input[type="text"],
+.search-adv div.opt input[type="number"] {
+ line-height: 18px;
+ padding: 0 2px;
+}
+.search-adv a.remove {
+ position: absolute;
+ top: 50%;
+ font-size: 12px;
+ text-decoration: none;
+ font-family: FontAwesome;
+ vertical-align: middle;
+ width: 20px;
+ height: 20px;
+ text-align: center;
+ border-radius: 3px;
+ background-clip: padding-box;
+ color: rgba(0, 0, 0, 0.7);
+ background-color: #fff;
+ border: 1px solid rgba(0, 0, 0, 0.15);
+ margin: -12px 0 0 5px;
+ padding: 0;
+}
+.search-adv a.remove:before {
+ content: "\f00d";
+ line-height: 19px;
+}
+.search-adv .selected-tags > div {
+ position: relative;
+ height: 24px;
+ border: 1px solid #ccc;
+ border-radius: 3px;
+ float: left;
+ vertical-align: middle;
+ display: table;
+ cursor: pointer;
+ background: #cf9;
+ margin: 4px 2px;
+ padding: 2px 30px 2px 2px;
+}
+.search-adv img.icon {
+ -webkit-border-radius: 6px;
+ -moz-border-radius: 6px;
+ border-radius: 6px;
+ max-height: 24px;
+ margin-bottom: -3px;
+ padding: 0 2px;
+}
+.search-l {
+ text-align: center;
+ min-height: 100px;
+ max-width: 600px;
+ margin: 150px auto 30px;
+}
+.search-s {
+ overflow: auto;
+ width: 100%;
+ border-bottom: 1px solid #ddd;
+ margin: 0 0 20px;
+ padding: 0 0 5px;
+}
+.search-s a.logo img {
+ position: absolute;
+ top: 5px;
+ left: 8px;
+}
+.search-s form {
+ position: relative;
+ width: 100%;
+ box-sizing: border-box;
+ padding: 0 60px 0 140px;
+}
+.search-s form,
+.search-s .searchbox {
+ display: inline-block;
+ margin: 0;
+}
+.search-s .searchbox {
+ width: 100%;
+ max-width: 350px;
+ margin: 10px 0 0;
+}
+.search-s .searchbox:after {
+ content: "";
+ clear: both;
+}
+.search-s .search-adv {
+ margin: 10px 0 10px -116px;
+}
+.result {
+ max-width: 750px;
+ margin: 10px 0;
+ padding: 0 20px 0 60px;
+}
+.result section {
+ display: inline-block;
+ width: 100%;
+}
+.searchtime {
+ padding: 0 60px 0 0;
+}
+.searchnav {
+ max-width: 750px;
+ text-align: center;
+ margin: 10px 0;
+ padding: 0 0 0 65px;
+}
+.searchnav ul {
+ display: inline-block;
+ max-width: 90%;
+ padding: 0 50px 0 0;
+}
+body.story #wrap {
+ padding-left: 10px;
+ padding-right: 15px;
+}
+.fic-cell.large {
+ display: inline-block;
+ max-width: 950px;
+ margin-top: 20px;
+}
+.story .notice {
+ margin: 15px 0 0;
+}
+.story404 {
+ text-align: center;
+ margin: 15px 0;
+ padding: 10px;
+}
+.story404 > div {
+ background: #500200 0 0 / cover url(/img/destruction_by_adiwan-sm.jpg);
+ min-height: 400px;
+ max-width: 800px;
+ margin: 0 auto;
+ padding: 0;
+}
+.story404 h1 {
+ font-weight: 700;
+ text-shadow: 1px 1px 2px #ff6c6c, 0 0 9px #000, 0 1px 9px #000;
+ font-size: 42px;
+ color: #fafafa;
+ display: inline-block;
+ box-shadow: 0 1px 16px red;
+ background: rgba(255, 255, 255, 0.7);
+ padding: 10px 20px;
+}
+.story404 h1:after {
+ content: "";
+ display: block;
+}
+.story404 p {
+ display: inline-block;
+ background: rgba(255, 255, 255, 0.9);
+ margin: 15px;
+ padding: 15px 25px;
+}
+.desc_short {
+ max-width: 700px;
+ font-size: 90%;
+ color: #333;
+}
+.dl-links {
+ height: 55px;
+ padding: 9px 4px;
+}
+.dl-links > span {
+ font: normal 17px "Times New Roman", serif;
+ display: block;
+ float: left;
+}
+.dl-links a {
+ display: block;
+ float: left;
+ margin-left: 5px;
+}
+.description span.spoiler {
+ background-color: #333;
+ color: #333;
+ text-shadow: none;
+}
+.description span.spoiler:hover {
+ background-color: inherit;
+ color: inherit;
+ text-shadow: inherit;
+}
+.description.full,
+.chapterlist.full {
+ max-height: none !important;
+}
+.fic-cell .chapterlist {
+ overflow: hidden;
+}
+.chapters h3 {
+ font-weight: 700;
+ margin: 0;
+}
+.chapterlist h3 {
+ margin: 18px 0 10px;
+}
+.chapter_title {
+ font: normal 14pt "Trebuchet MS", Helvetica, sans-serif;
+ color: #111;
+}
+.chapterlist a {
+ font-size: 0.8em;
+}
+.chapterlist .date {
+ font-size: 0.7em;
+}
+.chapterlist .word_count {
+ font-size: 0.7em;
+ float: right;
+ right: 10px;
+}
+.fa-plus-square {
+ border-radius: 4px;
+ -webkit-border-radius: 4px;
+ -moz-border-radius: 4px;
+ margin-right: 5px;
+}
+.fa-plus-square:before {
+ font: bold 14px FontAwesome;
+ content: "\f0fe";
+}
+.fa-minus-square:before {
+ font: bold 14px FontAwesome;
+ content: "\f146";
+}
+.featured {
+ margin: 0 0 15px;
+}
+.featured > span:first-of-type {
+ font-weight: 700;
+ font-size: 18px;
+}
+.featured a {
+ font: normal 14px Verdana, Geneva, sans-serif;
+ display: block;
+ width: 150px;
+ text-align: center;
+ border: 1px solid rgba(0, 0, 0, 0.2);
+ box-shadow: 0 0 8px #ccc;
+ border-radius: 4px;
+ -webkit-border-radius: 4px;
+ -moz-border-radius: 4px;
+ margin: 15px 0 0 10px;
+ padding: 4px 12px 5px;
+}
+.featured a:hover {
+ background: #ded;
+}
+.fic-series h3 {
+ font: bold 23px "Times New Roman", Times, serif;
+}
+.fic-series ol {
+ max-width: 940px;
+ display: inline-block;
+ list-style-type: none;
+ margin: 0 15px 10px;
+ padding: 0;
+}
+.fic-series li {
+ counter-increment: series-counter;
+ position: relative;
+ margin-bottom: 10px;
+ padding: 0 0 0 50px;
+}
+.fic-series li:before {
+ content: counter(series-counter);
+ position: absolute;
+ display: block;
+ top: 0;
+ left: 0;
+ font-size: 16pt;
+ width: 40px;
+ text-align: right;
+ padding: 10px 0;
+}
+.fic-series li.current:before {
+ content: ">";
+}
+.fic-series li.current .fic-cell {
+ box-shadow: 0 0 5px #09a209;
+}
+.fic-series .fic-cell {
+ font-size: 11pt;
+ box-shadow: none;
+ max-width: 100%;
+ margin: 0;
+}
+.fic-series header img {
+ max-height: 120px;
+ max-width: 120px;
+}
+.fic-series .title .name {
+ font-family: Verdana, Geneva, sans-serif;
+ font-size: 18pt;
+}
+.fic-series .tag-adv,
+.fic-series .tag-rom,
+.fic-series .tag-rnd,
+.fic-series .tag-com,
+.fic-series .tag-lif,
+.fic-series .tag-trg,
+.fic-series .tag-sad,
+.fic-series .tag-drk,
+.fic-series .tag-alt,
+.fic-series .tag-crs,
+.fic-series .tag-hum,
+.fic-series .tag-ath,
+.fic-series .tag-gor,
+.fic-series .tag-sex,
+.fic-series .tag-2p,
+.fic-series .tag-thr,
+.fic-series .tag-dra,
+.fic-series .tag-hor,
+.fic-series .tag-eqg,
+.fic-series .tag-mys,
+.fic-series .tag-sci,
+.fic-series .status-c,
+.fic-series .status-i,
+.fic-series .status-h,
+.fic-series .status-n,
+.fic-series .rating-ev,
+.fic-series .rating-tn,
+.fic-series .rating-ma,
+.fic-series .rating-ya,
+.fic-series .rating-ad,
+.fic-series .rating-cl,
+.fic-series .rating-db {
+ font-size: 12px;
+ line-height: 14px;
+ margin: 2px 0;
+ padding: 2px 4px;
+}
+.fic-series .ficsrc {
+ padding: 0 1px;
+}
+.fic-series .fic-cell br {
+ line-height: 24px;
+}
+.rev-tags {
+ overflow: auto;
+ padding: 2px 2px 8px;
+}
+.rev-tags > div {
+ position: relative;
+ height: 28px;
+ border: 1px solid #ccc;
+ border-radius: 3px;
+ float: left;
+ vertical-align: middle;
+ display: table;
+ background: #cf9;
+ margin: 4px 2px;
+ padding: 2px 30px 2px 2px;
+}
+.rev-tags > div.custom {
+ background: #ff9;
+}
+.rev-tags a.remove {
+ position: absolute;
+ top: 50%;
+ font-size: 12px;
+ text-decoration: none;
+ font-family: FontAwesome;
+ width: 20px;
+ height: 20px;
+ border-radius: 3px;
+ background-clip: padding-box;
+ color: rgba(0, 0, 0, 0.7);
+ background-color: #fff;
+ border: 1px solid rgba(0, 0, 0, 0.15);
+ margin: -10px 0 0 7px;
+ padding: 0;
+}
+.rev-tags a.remove:before {
+ content: "\f00d";
+ position: absolute;
+ display: inline-block;
+ top: 0;
+ right: 0;
+ margin: -2px 4px 0;
+}
+#report-tags-status.ok,
+#report-tags-status.error {
+ display: block;
+ text-align: left;
+ margin: 4px 0 2px;
+}
+#tag-new {
+ max-width: 100px;
+ margin: 10px 3px 3px;
+ padding: 3px 4px;
+}
+.fic-cell .report-link {
+ font-size: 14px;
+ position: relative;
+}
+.fic-cell .report-form {
+ display: display;
+ position: absolute;
+ left: 20px;
+ background: rgba(255, 255, 255, 0.8);
+ -webkit-border-radius: 8px;
+ -moz-border-radius: 8px;
+ border-radius: 8px;
+ padding: 0 10px 10px 0;
+}
+.fic-cell .report-form .rbox {
+ min-height: 100px;
+ min-width: 400px;
+ background: #fff;
+ -webkit-border-radius: 8px;
+ -moz-border-radius: 8px;
+ border-radius: 8px;
+ border: 1px solid #999;
+ box-shadow: 0 0 2px #aaa;
+ text-align: center;
+ z-index: 5;
+ position: relative;
+ padding: 10px;
+}
+.fic-cell .report-form h3 {
+ text-align: center;
+ font-weight: 700;
+ margin: 2px 5px 5px;
+}
+.report-form a.submit {
+ color: #000;
+ display: inline-block;
+ background: #fff;
+ border: 1px solid;
+ font-size: 16px;
+ cursor: pointer;
+ -webkit-border-radius: 5px;
+ -moz-border-radius: 5px;
+ border-radius: 5px;
+ margin: 8px 5px;
+ padding: 8px 15px;
+}
+a.submit2 {
+ color: #000;
+ display: inline-block;
+ background: #fff;
+ border: 1px solid;
+ font-size: 12px;
+ cursor: pointer;
+ -webkit-border-radius: 3px;
+ -moz-border-radius: 3px;
+ border-radius: 3px;
+ margin: 0;
+ padding: 0 5px;
+}
+.report-form a.submit:hover,
+a.submit2:hover {
+ color: #000;
+ background: #8f9;
+ text-decoration: none;
+}
+.report-form a.submit:active,
+a.submit2:active {
+ background: #3d4;
+}
+.report-form .cselect {
+ display: inline-block;
+ position: relative;
+ min-width: 160px;
+ background: #fff;
+ -webkit-border-radius: 3px;
+ -moz-border-radius: 3px;
+ border-radius: 3px;
+ border: 1px solid #999;
+ box-shadow: 0 1px 1px #ddd;
+ cursor: pointer;
+ outline: none;
+ color: #245;
+ margin: 0 0 4px;
+ padding: 3px 10px;
+}
+.report-form .cselect span:first-of-type {
+ display: block;
+ padding: 2px;
+}
+.report-form .cselect .dropdown {
+ position: absolute;
+ left: 0;
+ right: 0;
+ margin-top: 12px;
+ background: #fff;
+ border-radius: inherit;
+ border: 1px solid #999;
+ box-shadow: 0 0 5px #eee;
+ font-weight: 400;
+ transition: all 0.2s ease-in;
+ list-style: none;
+ opacity: 0;
+ pointer-events: none;
+ padding: 1px;
+}
+.cselect.active .dropdown {
+ opacity: 1;
+ pointer-events: auto;
+}
+.report-form .cselect:after {
+ content: "";
+ position: absolute;
+ right: 15px;
+ top: 50%;
+ margin-top: -3px;
+ border-color: #8aa8bd transparent;
+ border-style: solid;
+ border-width: 6px 6px 0;
+}
+.report-form .dropdown:before {
+ content: "";
+ position: absolute;
+ bottom: 100%;
+ right: 13px;
+ border-color: rgba(0, 0, 0, 0.1) transparent;
+ border-style: solid;
+ border-width: 0 8px 8px;
+}
+.report-form .dropdown:after {
+ content: "";
+ position: absolute;
+ bottom: 100%;
+ right: 15px;
+ border-color: #fff transparent;
+ border-style: solid;
+ border-width: 0 6px 6px;
+}
+.report-form .dropdown div {
+ float: none;
+ width: 100%;
+ margin: 0;
+}
+.report-form .dropdown li {
+ border-bottom: 1px solid #bbb;
+ transition: all 0.3s ease-out;
+ padding: 5px 7px 2px;
+}
+.report-form .dropdown li:hover {
+ background: #f3f8f8;
+}
+#report-status.ok,
+#report-status.error {
+ display: block;
+ text-align: left;
+ margin: 4px 0 2px;
+}
+.loading.sm {
+ max-height: 40px;
+ max-width: 40px;
+}
+.mystar {
+ display: inline-block;
+ float: left;
+ position: relative;
+ box-shadow: 0 0 2px #059c5d;
+ width: 0;
+ overflow: hidden;
+ margin: 5px 5px 0;
+ padding: 0;
+}
+.mystar > div {
+ position: relative;
+ width: 200px;
+ display: none;
+ top: 0;
+}
+.mystar .ok {
+ color: #0b980b;
+ font-weight: 700;
+ text-align: center;
+ font-size: 30px;
+ padding: 15px 5px 0;
+}
+label.popout.star:hover:before,
+label.popout.star:hover ~ label.star:before {
+ content: "\f005";
+ color: #fd4;
+}
+label.popout.star-5:hover,
+label.popout.star-5:hover ~ label.star:before {
+ content: "\f005";
+ color: #fe4;
+ text-shadow: 0 0 10px #952;
+}
+a.read {
+ color: #000 !important;
+ font-size: 15px;
+ display: inline-block;
+ float: left;
+ background-image: linear-gradient(rgba(17, 153, 0, 0.5), rgba(0, 221, 34, 0.3), rgba(0, 221, 34, 0.3), rgba(17, 153, 0, 0.5)), linear-gradient(90deg, #190, #0d2, #0d2, #0d2, #0d2, #190);
+ -webkit-border-radius: 3px;
+ -moz-border-radius: 3px;
+ border-radius: 3px;
+ -webkit-transition: all 0.3s ease-in-out;
+ -moz-transition: all 0.3s ease-in-out;
+ -ms-transition: all 0.3s ease-in-out;
+ -o-transition: all 0.3s ease-in-out;
+ margin: 5px 0 0;
+ padding: 2px 10px;
+}
+a.read:hover {
+ background-image: linear-gradient(rgba(17, 153, 0, 0.4), rgba(0, 221, 34, 0.2), rgba(0, 221, 34, 0.2), rgba(17, 153, 0, 0.4)), linear-gradient(90deg, #3a3, #3e5, #3e5, #3e5, #3e5, #3a3);
+ text-decoration: none !important;
+ padding: 2px 14px;
+}
+a.read.noanimate {
+ overflow: hidden;
+ -webkit-transition: none;
+ -moz-transition: none;
+ -ms-transition: none;
+ -o-transition: none;
+}
+a.read.remove {
+ margin-left: 10px;
+ background-image: linear-gradient(rgba(153, 50, 50, 0.3), rgba(221, 50, 50, 0.1), rgba(221, 50, 50, 0.1), rgba(153, 50, 50, 0.3)), linear-gradient(90deg, #d00, #d44, #d44, #d44, #d44, #d00);
+}
+a.read.remove:hover {
+ background-image: linear-gradient(rgba(153, 100, 100, 0.5), rgba(221, 100, 100, 0.5), rgba(221, 100, 100, 0.5), rgba(153, 100, 100, 0.5)), linear-gradient(90deg, #f00, #e33, #e33, #e33, #e33, #f00);
+}
+#starsrem a {
+ float: none;
+}
+.ficshelves {
+ display: block;
+ float: left;
+ overflow: hidden;
+ min-width: 260px;
+ text-align: center;
+ margin: 5px 10px;
+}
+.ficshelves > span:first-of-type {
+ display: block;
+ padding-top: 2px;
+ box-shadow: 0 0 1px 1px #4c6d46;
+ border-top-left-radius: 15px;
+ border-top-right-radius: 15px;
+ border-bottom: 1px solid #adadad;
+ margin: 3px;
+}
+.ficshelves .inshelves.none {
+ font-size: 85%;
+}
+.ficshelves ul {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+}
+.ficshelves li {
+ display: inline-block;
+ position: relative;
+ width: 95%;
+ vertical-align: top;
+ box-shadow: inset 0 0 4px 1px rgba(35, 35, 35, 0.17), 1px 1px 1px #6b6b6b;
+ margin: 3px 0;
+ padding: 1px 5px;
+}
+.ficshelves li > span:first-of-type {
+ margin-right: 14px;
+}
+.ficshelves span a:hover {
+ color: #224;
+}
+.ficshelves span a:active {
+ color: #222;
+}
+.ficshelves .rem {
+ position: absolute;
+ top: 0;
+ right: 3px;
+}
+.ficshelves .rem a {
+ color: #532;
+}
+.ficshelves select {
+ font-size: 14px;
+ margin-top: 5px;
+}
+.removed.dcma {
+ border: red solid;
+ display: block;
+ text-align: center;
+ margin: 10px auto;
+ padding: 35px 20px;
+}
+.removed {
+ border: red solid;
+ display: inline-block;
+ margin-bottom: 8px;
+ background: #fff;
+ color: #c00;
+ box-shadow: 0 0 8px red inset;
+ text-shadow: 0 0 9px #fff;
+ font: bold 12pt arial, sans-serif;
+ padding: 8px 14px;
+}
+.fic-data {
+ position: relative;
+ font-family: Georgia, serif;
+ top: 15px;
+ text-align: left;
+ overflow-wrap: break-word;
+ word-wrap: break-word;
+ -webkit-hyphens: auto;
+ -ms-hyphens: auto;
+ -moz-hyphens: auto;
+ hyphens: auto;
+ box-shadow: 0 0 20px rgba(0, 0, 0, 0.6);
+ -webkit-border-radius: 5px;
+ -moz-border-radius: 5px;
+ border-radius: 5px;
+ max-width: 900px;
+ margin: 10px auto;
+ padding: 5px 20px;
+}
+.fic-data header {
+ position: relative;
+ margin-bottom: 30px;
+}
+.fic-data h1 {
+ font-size: 32px;
+ font-weight: 400;
+ margin: 20px 0;
+}
+.fic-data h2 {
+ margin: 20px 0;
+}
+.fic-data h1:first-of-type,
+.fic-data h2:first-of-type {
+ text-align: center;
+ color: #333;
+}
+.fic-data h1 a,
+.fic-data h1 a:visited,
+.fic-data h2 a,
+.fic-data h2 a:visited {
+ text-decoration: none;
+ color: inherit;
+}
+.fic-data h1 a:hover,
+.fic-data h2 a:hover {
+ text-decoration: underline;
+ color: #009;
+}
+.fic-data ul {
+ font-family: "Times New Roman", Times, serif;
+}
+.fic-data .author {
+ display: block;
+ text-align: center;
+ font-size: 18px;
+ margin: -14px 0 0;
+}
+.fic-data header .chapnav {
+ position: absolute;
+ bottom: 3px;
+}
+.fic-data .chapnav.next {
+ right: 5px;
+}
+.fic-data .chapnav.prev {
+ left: 5px;
+}
+.fic-data .chapnav.bot {
+ font-size: 26px;
+ text-align: center;
+ display: block;
+ padding: 14px 0 18px;
+}
+.fic-data .timeremest {
+ display: block;
+ text-align: center;
+ color: #444;
+ font-family: sans-serif;
+ font-size: 13px;
+}
+.fic-data h3 {
+ margin-left: 20px;
+ font: bold 24px "Times New Roman", Times, serif;
+}
+.fic-data ul:first-of-type li {
+ list-style-type: decimal;
+ padding: 3px 0;
+}
+.fic-data p {
+ -webkit-hyphens: auto;
+ -moz-hyphens: auto;
+ -ms-hyphens: auto;
+ hyphens: auto;
+}
+.fic-data .double {
+ margin-top: 1em;
+}
+.fic-data .i {
+ font-style: italic;
+}
+.fic-data img.emoticon {
+ border: 0;
+ height: 1em;
+ margin: 0;
+ padding: 0;
+}
+.fic-data hr {
+ margin-top: 12px;
+}
+.fic-data blockquote {
+ border-left: 5px solid #aaa;
+ background: #eee;
+ margin: 10px;
+ padding: 5px 10px;
+}
+a.back {
+ display: inline-block;
+ margin: 5px;
+ padding: 20px;
+}
+.woverlay {
+ position: fixed;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ left: 0;
+ cursor: default;
+ z-index: 1000;
+ -webkit-transition: opacity 0.3s;
+ -moz-transition: opacity 0.3s;
+ -ms-transition: opacity 0.3s;
+ -o-transition: opacity 0.3s;
+ transition: opacity 0.3s;
+}
+.woverlay > iframe {
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ overflow: hidden;
+}
+.rwarn {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background-color: rgba(146, 146, 146, 0.46);
+ z-index: 1001;
+}
+.rwarn > div {
+ position: absolute;
+ left: 50%;
+ margin-left: -180px;
+ top: 25%;
+ width: 360px;
+ height: auto;
+ border-radius: 5px;
+ box-shadow: 0 0 10px rgba(0, 0, 0, 0.4);
+ text-align: center;
+ border: 1px solid #b4b1b1;
+ background-color: #fffeae;
+ padding: 10px 8px;
+}
+.rwarn .title {
+ font-size: 16px;
+ color: #666;
+}
+.rwarn .confirm {
+ display: inline-block;
+ background-color: #43ce43;
+ color: #000;
+ border-radius: 5px;
+ box-shadow: 1px 1px 2px rgba(0, 0, 0, 0.4);
+ border: 1px solid #292;
+ margin: 5px 0 20px;
+ padding: 5px 8px;
+}
+.rwarn .confirm:hover {
+ background-color: #5d5;
+ text-decoration: none;
+}
+* {
+ box-sizing: border-box;
+}
+body {
+ font-family: sans-serif;
+ background-color: #fff;
+ text-align: center;
+ margin: 0;
+ padding: 0;
+}
+#wrap {
+ min-height: 100vh;
+ margin: 0 0 -165px;
+ padding: 0 0 220px;
+}
+h1,
+h2,
+h3,
+h4 {
+ font-weight: 400;
+}
+.error {
+ text-align: center;
+ color: #d20e0e;
+ font-weight: 700;
+}
+a,
+a:visited {
+ color: #33f;
+ text-decoration: none;
+}
+a:active,
+a:hover {
+ cursor: pointer;
+ color: #33c;
+ text-decoration: underline;
+}
+a label:hover {
+ cursor: pointer;
+}
+input:-webkit-autofill {
+ -webkit-box-shadow: 0 0 0 1000px #fff inset;
+}
+nav.home {
+ color: #778;
+ font: 15px "Trebuchet MS", Helvetica, sans-serif;
+ text-align: center;
+}
+nav.home a.current {
+ font-size: 18px;
+ font-weight: 700;
+}
+nav.home a {
+ display: inline-block;
+ margin: 3px;
+ padding: 3px 5px;
+}
+header.main {
+ text-align: center;
+ padding: 4px;
+}
+header.main img {
+ max-width: 100%;
+ padding: 0 60px;
+}
+img.emoticon {
+ max-height: 27px;
+}
+.more_span {
+ display: none;
+ background-image: url(/img/fo-20.png);
+ background-repeat: repeat-x;
+ position: relative;
+ top: -20px;
+ margin-bottom: -10px;
+ padding: 25px 10px 5px;
+}
+.more_span.less {
+ background-image: none;
+}
+.more_button {
+ display: inline-block;
+ font: normal 14px Verdana, Geneva, sans-serif;
+ background: #555;
+ color: #fff;
+ border: 1px solid rgba(0, 0, 0, 0.2);
+ border-radius: 4px;
+ -webkit-border-radius: 4px;
+ -moz-border-radius: 4px;
+ text-decoration: none;
+ margin: 0;
+ padding: 4px 12px 5px;
+}
+a.more_button:hover,
+.fic-cell a.more_button:active {
+ color: #fff;
+ text-decoration: none;
+ background: #666;
+ text-shadow: 0 0 1px #777;
+ box-shadow: 0 0 3px #000;
+}
+.notice {
+ display: inline-block;
+ background: #dff;
+ color: #3a1;
+ border: #185 solid;
+ box-shadow: 0 0 8px #358 inset;
+ text-shadow: 0 0 9px #fff;
+ font: bold 12pt arial, sans-serif;
+ -webkit-border-radius: 5px;
+ -moz-border-radius: 5px;
+ border-radius: 5px;
+ margin: 0;
+ padding: 8px 20px;
+}
+.notice.error {
+ box-shadow: 0 0 8px #e88 inset;
+ border: #d25555 solid;
+ background: #fdd;
+ color: #a50303;
+}
+.pagenav li {
+ vertical-align: top;
+ zoom: 1;
+ display: inline;
+ padding: 0 10px;
+}
+.pagenav li a,
+.pagenav li a:visited {
+ color: #333;
+ font-size: 130%;
+ font-weight: 700;
+}
+.pagenav li a:hover {
+ color: #666;
+ letter-spacing: 1px;
+}
+img.loading {
+ -webkit-animation: spin 1s linear infinite;
+ -moz-animation: spin 1s linear infinite;
+ animation: spin 1s linear infinite;
+}
+.usersection {
+ position: absolute;
+ display: block;
+ top: 0;
+ right: 0;
+ text-align: right;
+ z-index: 10;
+ padding: 10px 10px 15px 15px;
+}
+.usermenu {
+ width: 65px;
+ height: 65px;
+ position: relative;
+ text-align: center;
+ background: #fff;
+ -webkit-border-radius: 40px;
+ -moz-border-radius: 40px;
+ border-radius: 40px;
+ border: 1px solid #ddd;
+ box-shadow: 0 0 40px #6b5 inset;
+ cursor: pointer;
+ outline: none;
+ transition: all 0.3s ease-out;
+ margin: 0 0 0 auto;
+ padding: 5px;
+}
+.usermenu .dropdown {
+ text-align: left;
+ position: absolute;
+ top: 80%;
+ width: 145px;
+ left: -81px;
+ right: 0;
+ background: #fff;
+ -webkit-border-radius: 20px 0 20px 20px;
+ -moz-border-radius: 20px 0 20px 20px;
+ border-radius: 20px 0 20px 20px;
+ border: 1px solid rgba(0, 0, 0, 0.8);
+ border-top: none;
+ border-bottom: none;
+ list-style: none;
+ transition: all 0.3s ease-out;
+ max-height: 0;
+ overflow: hidden;
+ margin: 16px 0 0;
+ padding: 0;
+}
+.usermenu .dropdown li {
+ border-bottom: 1px solid #e6e8ea;
+ padding: 10px;
+}
+.usermenu .dropdown li a {
+ display: block;
+ text-decoration: none;
+ color: #333;
+ transition: all 0.3s ease-out;
+ margin: -10px;
+ padding: 10px;
+}
+.usermenu .dropdown li i {
+ margin-right: 5px;
+ color: inherit;
+ vertical-align: middle;
+}
+.usermenu .dropdown li:hover a {
+ color: #57a9d9;
+}
+.usermenu.active {
+ border-radius: 30px 30px 0 0;
+ background: #bdf;
+ box-shadow: none;
+ border-bottom: none;
+}
+.usermenu.active .dropdown {
+ max-height: 400px;
+ border: 1px solid rgba(0, 0, 0, 0.8);
+}
+.spine header .fa.c.pony,
+.shelf .title .fa.pony {
+ color: #000;
+ text-shadow: 1px 1px 4px rgba(208, 208, 208, 0.5);
+ font-size: 40px;
+ overflow: hidden;
+ padding: 8px 6px 3px;
+}
+.spine footer a:hover,
+.fic-data ul:first-of-type a,
+.fic-data .b {
+ font-weight: 700;
+}
+.shelf-menu .spine section,
+.chapterlist li {
+ margin-bottom: 10px;
+}
+.iconselect .toggleable-radio label:first-of-type,
+.colorselect .toggleable-radio label:first-of-type,
+.iconselect .toggleable-radio label:last-of-type,
+.colorselect .toggleable-radio label:last-of-type {
+ border-radius: 3px;
+}
+#status.ok,
+#report-tags-status.ok,
+#report-status.ok {
+ color: #2ea93f;
+ font-weight: 700;
+}
+.shelf > header h1,
+.shelf > header h2,
+.chapterlist ol,
+.usersection form {
+ margin: 0;
+}
+.shelf .title,
+body > footer > div,
+body.fotd,
+body.story,
+.mystar .prompt,
+#starsrem,
+.fic-data .c {
+ text-align: center;
+}
+.shelf .title > div,
+.fic-cell .popular span,
+.ficstats span,
+.user .info {
+ display: inline-block;
+}
+.comment-cell p.indented,
+.fic-data .indented {
+ text-indent: 3em;
+}
+.fic-cell a.more_button,
+.fic-tiles .ftag:before {
+ color: #fff;
+}
+.fic-cell header,
+.fic-cell .details,
+.user .name {
+ overflow: auto;
+}
+.fic-cell .description a,
+.fic-data .u {
+ text-decoration: underline;
+}
+.rating,
+.characters,
+#opt-tags {
+ padding: 2px;
+}
+.characters,
+.search-adv input[type="submit"] {
+ float: right;
+}
+.published span,
+.updated span,
+.fic-tiles .frating.cl2,
+.ficshelves span a {
+ color: #000;
+}
+input.star,
+.fic-tiles .published,
+.fic-tiles .updated,
+.fotd.sm .fic-cell .popular,
+.fic-data > header p,
+.fic-data > header img {
+ display: none;
+}
+input.star-1:checked ~ label.star:before,
+span.star.star-filled.star-1,
+label.popout.star-1:hover:before {
+ color: #f62;
+}
+.fic-tiles .ficstats .words,
+.fic-cell .popular .star-rating,
+header,
+section,
+footer,
+aside,
+nav,
+main,
+article,
+figure {
+ display: block;
+}
+.fotd.sm .fic-cell .description,
+.fotd.sm .fic-cell .desc_short,
+.ficblock {
+ clear: both;
+}
+.loginbox a:link,
+.loginbox a:visited,
+nav.home a:link,
+nav.home a:visited {
+ color: inherit;
+ text-decoration: none;
+}
+.loginbox a:hover,
+nav.home a:hover {
+ color: inherit;
+ text-decoration: underline;
+}
+.loginbox input.login:hover,
+.loginbox input.login:active {
+ background: #06f;
+}
+.loginbox a.register:hover,
+.loginremember a:hover {
+ color: #333 !important;
+}
+.search .buttons,
+.fic-cell.large .popular {
+ padding: 5px 0;
+}
+#search-adv:after,
+.search-adv div.opt.full::after {
+ content: " ";
+ display: block;
+ height: 0;
+ clear: both;
+}
+.selected-tags .excluded,
+.rev-tags .excluded {
+ background: #f99 !important;
+}
+.search-adv .selected-tags > div > span,
+.rev-tags > div > span {
+ display: table-cell;
+ vertical-align: middle;
+ padding: 0 3px 0 6px;
+}
+.search-l img,
+.story404 *,
+.fic-data img,
+.usermenu img {
+ max-width: 100%;
+}
+.report-form .rem,
+.fic-data .s {
+ text-decoration: line-through;
+}
+.report-form .dropdown li:last-of-type,
+.usermenu .dropdown li:last-of-type {
+ border: none;
+}
+@media (max-width: 600px) {
+ .fic-cell.large .popular {
+ display: block;
+ }
+ .fic-cell .popular.popsmall {
+ display: block !important;
+ font-size: 14px;
+ line-height: 14px;
+ }
+ .fic-cell .details br {
+ line-height: 20px;
+ }
+ article.fotd > header {
+ font-size: 38px;
+ }
+ .user .avatar img {
+ max-height: 130px;
+ max-width: 130px;
+ }
+ .user .avatar .jwc_frame {
+ height: 130px !important;
+ width: 130px !important;
+ }
+ .user .avatar.preview .jwc_frame {
+ height: 200px !important;
+ width: 200px !important;
+ }
+ .user .name h1 {
+ font-size: 28px;
+ }
+ .user .name img {
+ max-height: 28px;
+ -webkit-border-radius: 6px;
+ -moz-border-radius: 6px;
+ border-radius: 6px;
+ }
+ .user .info {
+ font-size: 14px;
+ }
+ .search-l {
+ margin-top: 80px;
+ max-width: 90%;
+ }
+ .search-s #logo2 {
+ display: block !important;
+ }
+ .search-s form {
+ padding-left: 100px;
+ }
+ .search-s .search-adv {
+ margin: 10px -50px 10px -90px;
+ }
+ .result {
+ padding: 0 10px 0 5px;
+ }
+ .searchnav ul {
+ padding: 0 10px;
+ }
+ .fic-series .fic-cell {
+ position: relative;
+ top: 30px;
+ margin: 0 0 50px;
+ }
+ .fic-series li:before {
+ text-align: left;
+ padding: 0;
+ }
+ nav.home {
+ padding-right: 45px;
+ }
+ .usermenu {
+ width: 45px;
+ height: 45px;
+ }
+ .usermenu .dropdown {
+ left: -91px;
+ }
+ .fic-cell .description,
+ .user .bio,
+ .fic-cell .desc_short {
+ clear: both;
+ }
+ .fic-cell .popular,
+ .search-s #logo1 {
+ display: none;
+ }
+ .searchnav,
+ .fic-series li {
+ padding: 0;
+ }
+}
+@media (max-width: 450px) {
+ .fic-tiles {
+ padding: 0 10px;
+ }
+ .fic-tiles .fic-cell {
+ width: 100%;
+ }
+ .user .avatar img {
+ max-height: 80px;
+ max-width: 80px;
+ }
+ .user .avatar .jwc_frame {
+ height: 80px !important;
+ width: 80px !important;
+ }
+ .user .avatar.preview .jwc_frame {
+ height: 200px !important;
+ width: 200px !important;
+ }
+ .user .name h1 {
+ font-size: 20px;
+ }
+ .user .name img {
+ max-height: 20px;
+ -webkit-border-radius: 4px;
+ -moz-border-radius: 4px;
+ border-radius: 4px;
+ }
+ .fic-cell .report-form .rbox {
+ min-height: 100px;
+ min-width: 200px;
+ width: 100%;
+ font-size: 14px;
+ }
+ .fic-cell .report-form a.submit {
+ font-size: 14px;
+ }
+ .fic-cell .report-form {
+ left: 10px;
+ right: 0;
+ }
+}
+@media (max-width: 800px) {
+ body > footer section {
+ width: 300px;
+ display: block;
+ min-height: 0;
+ margin: 0 auto;
+ padding: 10px;
+ }
+ body > footer section:last-of-type {
+ min-height: 150px;
+ }
+ body > footer section:before {
+ visibility: hidden;
+ }
+ body > footer h4 {
+ margin: 5px 0 10px;
+ }
+}
diff --git a/app/channels/application_cable/channel.rb b/app/channels/application_cable/channel.rb
new file mode 100644
index 0000000..d672697
--- /dev/null
+++ b/app/channels/application_cable/channel.rb
@@ -0,0 +1,4 @@
+module ApplicationCable
+ class Channel < ActionCable::Channel::Base
+ end
+end
diff --git a/app/channels/application_cable/connection.rb b/app/channels/application_cable/connection.rb
new file mode 100644
index 0000000..0ff5442
--- /dev/null
+++ b/app/channels/application_cable/connection.rb
@@ -0,0 +1,4 @@
+module ApplicationCable
+ class Connection < ActionCable::Connection::Base
+ end
+end
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
new file mode 100644
index 0000000..841d3da
--- /dev/null
+++ b/app/controllers/application_controller.rb
@@ -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
diff --git a/app/controllers/authors_controller.rb b/app/controllers/authors_controller.rb
new file mode 100644
index 0000000..14b2cd8
--- /dev/null
+++ b/app/controllers/authors_controller.rb
@@ -0,0 +1,5 @@
+class AuthorsController < ApplicationController
+ def show
+ @author = Author.find(params[:id])
+ end
+end
diff --git a/app/controllers/chapters_controller.rb b/app/controllers/chapters_controller.rb
new file mode 100644
index 0000000..a612712
--- /dev/null
+++ b/app/controllers/chapters_controller.rb
@@ -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
diff --git a/app/controllers/concerns/.keep b/app/controllers/concerns/.keep
new file mode 100644
index 0000000..e69de29
diff --git a/app/controllers/images_controller.rb b/app/controllers/images_controller.rb
new file mode 100644
index 0000000..d385737
--- /dev/null
+++ b/app/controllers/images_controller.rb
@@ -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
diff --git a/app/controllers/search_controller.rb b/app/controllers/search_controller.rb
new file mode 100644
index 0000000..7b55ee9
--- /dev/null
+++ b/app/controllers/search_controller.rb
@@ -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
diff --git a/app/controllers/static_pages_controller.rb b/app/controllers/static_pages_controller.rb
new file mode 100644
index 0000000..6fd914a
--- /dev/null
+++ b/app/controllers/static_pages_controller.rb
@@ -0,0 +1,4 @@
+class StaticPagesController < ApplicationController
+ def about
+ end
+end
diff --git a/app/controllers/stories_controller.rb b/app/controllers/stories_controller.rb
new file mode 100644
index 0000000..cce75ed
--- /dev/null
+++ b/app/controllers/stories_controller.rb
@@ -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
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
new file mode 100644
index 0000000..9747f8a
--- /dev/null
+++ b/app/helpers/application_helper.rb
@@ -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
diff --git a/app/helpers/authors_helper.rb b/app/helpers/authors_helper.rb
new file mode 100644
index 0000000..f22e1f9
--- /dev/null
+++ b/app/helpers/authors_helper.rb
@@ -0,0 +1,2 @@
+module AuthorsHelper
+end
diff --git a/app/helpers/chapters_helper.rb b/app/helpers/chapters_helper.rb
new file mode 100644
index 0000000..063540b
--- /dev/null
+++ b/app/helpers/chapters_helper.rb
@@ -0,0 +1,2 @@
+module ChaptersHelper
+end
diff --git a/app/helpers/images_helper.rb b/app/helpers/images_helper.rb
new file mode 100644
index 0000000..7b3a8bc
--- /dev/null
+++ b/app/helpers/images_helper.rb
@@ -0,0 +1,2 @@
+module ImagesHelper
+end
diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb
new file mode 100644
index 0000000..343cd26
--- /dev/null
+++ b/app/helpers/search_helper.rb
@@ -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
diff --git a/app/helpers/static_pages_helper.rb b/app/helpers/static_pages_helper.rb
new file mode 100644
index 0000000..2d63e79
--- /dev/null
+++ b/app/helpers/static_pages_helper.rb
@@ -0,0 +1,2 @@
+module StaticPagesHelper
+end
diff --git a/app/helpers/stories_helper.rb b/app/helpers/stories_helper.rb
new file mode 100644
index 0000000..43e5cd8
--- /dev/null
+++ b/app/helpers/stories_helper.rb
@@ -0,0 +1,2 @@
+module StoriesHelper
+end
diff --git a/app/indexes/story_index.rb b/app/indexes/story_index.rb
new file mode 100644
index 0000000..ad0f1fa
--- /dev/null
+++ b/app/indexes/story_index.rb
@@ -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
\ No newline at end of file
diff --git a/app/jobs/application_job.rb b/app/jobs/application_job.rb
new file mode 100644
index 0000000..d394c3d
--- /dev/null
+++ b/app/jobs/application_job.rb
@@ -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
diff --git a/app/jobs/index_update_job.rb b/app/jobs/index_update_job.rb
new file mode 100644
index 0000000..258868d
--- /dev/null
+++ b/app/jobs/index_update_job.rb
@@ -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
diff --git a/app/models/application_record.rb b/app/models/application_record.rb
new file mode 100644
index 0000000..b63caeb
--- /dev/null
+++ b/app/models/application_record.rb
@@ -0,0 +1,3 @@
+class ApplicationRecord < ActiveRecord::Base
+ primary_abstract_class
+end
diff --git a/app/models/author.rb b/app/models/author.rb
new file mode 100644
index 0000000..3b39b4e
--- /dev/null
+++ b/app/models/author.rb
@@ -0,0 +1,3 @@
+class Author < ApplicationRecord
+ has_many :stories
+end
diff --git a/app/models/chapter.rb b/app/models/chapter.rb
new file mode 100644
index 0000000..a40e143
--- /dev/null
+++ b/app/models/chapter.rb
@@ -0,0 +1,3 @@
+class Chapter < ApplicationRecord
+ belongs_to :story
+end
diff --git a/app/models/concerns/.keep b/app/models/concerns/.keep
new file mode 100644
index 0000000..e69de29
diff --git a/app/models/concerns/indexable.rb b/app/models/concerns/indexable.rb
new file mode 100644
index 0000000..bd00205
--- /dev/null
+++ b/app/models/concerns/indexable.rb
@@ -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
diff --git a/app/models/story.rb b/app/models/story.rb
new file mode 100644
index 0000000..136fbd2
--- /dev/null
+++ b/app/models/story.rb
@@ -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
diff --git a/app/models/story/tagging.rb b/app/models/story/tagging.rb
new file mode 100644
index 0000000..2349210
--- /dev/null
+++ b/app/models/story/tagging.rb
@@ -0,0 +1,6 @@
+class Story::Tagging < ApplicationRecord
+ belongs_to :story
+ belongs_to :tag
+
+ validates :tag, uniqueness: { scope: [:story_id] }
+end
\ No newline at end of file
diff --git a/app/models/tag.rb b/app/models/tag.rb
new file mode 100644
index 0000000..7b23605
--- /dev/null
+++ b/app/models/tag.rb
@@ -0,0 +1,2 @@
+class Tag < ApplicationRecord
+end
diff --git a/app/views/chapters/show.html.slim b/app/views/chapters/show.html.slim
new file mode 100644
index 0000000..76945a9
--- /dev/null
+++ b/app/views/chapters/show.html.slim
@@ -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)
+
diff --git a/app/views/kaminari/_first_page.html.slim b/app/views/kaminari/_first_page.html.slim
new file mode 100644
index 0000000..62e158d
--- /dev/null
+++ b/app/views/kaminari/_first_page.html.slim
@@ -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
diff --git a/app/views/kaminari/_gap.html.slim b/app/views/kaminari/_gap.html.slim
new file mode 100644
index 0000000..09f9d83
--- /dev/null
+++ b/app/views/kaminari/_gap.html.slim
@@ -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 …
diff --git a/app/views/kaminari/_last_page.html.slim b/app/views/kaminari/_last_page.html.slim
new file mode 100644
index 0000000..f4501e9
--- /dev/null
+++ b/app/views/kaminari/_last_page.html.slim
@@ -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
diff --git a/app/views/kaminari/_next_page.html.slim b/app/views/kaminari/_next_page.html.slim
new file mode 100644
index 0000000..f55f6c1
--- /dev/null
+++ b/app/views/kaminari/_next_page.html.slim
@@ -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'
diff --git a/app/views/kaminari/_page.html.slim b/app/views/kaminari/_page.html.slim
new file mode 100644
index 0000000..7ffe399
--- /dev/null
+++ b/app/views/kaminari/_page.html.slim
@@ -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'
diff --git a/app/views/kaminari/_paginator.html.slim b/app/views/kaminari/_paginator.html.slim
new file mode 100644
index 0000000..007006d
--- /dev/null
+++ b/app/views/kaminari/_paginator.html.slim
@@ -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?
diff --git a/app/views/kaminari/_prev_page.html.slim b/app/views/kaminari/_prev_page.html.slim
new file mode 100644
index 0000000..f35c308
--- /dev/null
+++ b/app/views/kaminari/_prev_page.html.slim
@@ -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'
diff --git a/app/views/layouts/_banner.html.slim b/app/views/layouts/_banner.html.slim
new file mode 100644
index 0000000..55bc14f
--- /dev/null
+++ b/app/views/layouts/_banner.html.slim
@@ -0,0 +1,3 @@
+header.banner.main
+ = link_to '/'
+ = image_tag '/img/banner.png'
\ No newline at end of file
diff --git a/app/views/layouts/application.html.slim b/app/views/layouts/application.html.slim
new file mode 100644
index 0000000..cea3f37
--- /dev/null
+++ b/app/views/layouts/application.html.slim
@@ -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
diff --git a/app/views/search/_advanced.html.slim b/app/views/search/_advanced.html.slim
new file mode 100644
index 0000000..7f30a47
--- /dev/null
+++ b/app/views/search/_advanced.html.slim
@@ -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'
\ No newline at end of file
diff --git a/app/views/search/index.html.slim b/app/views/search/index.html.slim
new file mode 100644
index 0000000..f237dc7
--- /dev/null
+++ b/app/views/search/index.html.slim
@@ -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'
\ No newline at end of file
diff --git a/app/views/search/search.html.slim b/app/views/search/search.html.slim
new file mode 100644
index 0000000..37e714e
--- /dev/null
+++ b/app/views/search/search.html.slim
@@ -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.
\ No newline at end of file
diff --git a/app/views/static_pages/about.html.slim b/app/views/static_pages/about.html.slim
new file mode 100644
index 0000000..6a881e8
--- /dev/null
+++ b/app/views/static_pages/about.html.slim
@@ -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 uncensored 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.
\ No newline at end of file
diff --git a/app/views/stories/index.html.slim b/app/views/stories/index.html.slim
new file mode 100644
index 0000000..e69de29
diff --git a/app/views/stories/show.html.slim b/app/views/stories/show.html.slim
new file mode 100644
index 0000000..4b90e79
--- /dev/null
+++ b/app/views/stories/show.html.slim
@@ -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
+
diff --git a/bin/bundle b/bin/bundle
new file mode 100755
index 0000000..42c7fd7
--- /dev/null
+++ b/bin/bundle
@@ -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
diff --git a/bin/importmap b/bin/importmap
new file mode 100755
index 0000000..36502ab
--- /dev/null
+++ b/bin/importmap
@@ -0,0 +1,4 @@
+#!/usr/bin/env ruby
+
+require_relative "../config/application"
+require "importmap/commands"
diff --git a/bin/rails b/bin/rails
new file mode 100755
index 0000000..efc0377
--- /dev/null
+++ b/bin/rails
@@ -0,0 +1,4 @@
+#!/usr/bin/env ruby
+APP_PATH = File.expand_path("../config/application", __dir__)
+require_relative "../config/boot"
+require "rails/commands"
diff --git a/bin/rake b/bin/rake
new file mode 100755
index 0000000..4fbf10b
--- /dev/null
+++ b/bin/rake
@@ -0,0 +1,4 @@
+#!/usr/bin/env ruby
+require_relative "../config/boot"
+require "rake"
+Rake.application.run
diff --git a/bin/setup b/bin/setup
new file mode 100755
index 0000000..ec47b79
--- /dev/null
+++ b/bin/setup
@@ -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
diff --git a/config.ru b/config.ru
new file mode 100644
index 0000000..4a3c09a
--- /dev/null
+++ b/config.ru
@@ -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
diff --git a/config/application.rb b/config/application.rb
new file mode 100644
index 0000000..604ffe7
--- /dev/null
+++ b/config/application.rb
@@ -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
diff --git a/config/boot.rb b/config/boot.rb
new file mode 100644
index 0000000..2820116
--- /dev/null
+++ b/config/boot.rb
@@ -0,0 +1,3 @@
+ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__)
+
+require "bundler/setup" # Set up gems listed in the Gemfile.
diff --git a/config/cable.yml b/config/cable.yml
new file mode 100644
index 0000000..9a968cb
--- /dev/null
+++ b/config/cable.yml
@@ -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
diff --git a/config/credentials.yml.enc b/config/credentials.yml.enc
new file mode 100644
index 0000000..c79bad0
--- /dev/null
+++ b/config/credentials.yml.enc
@@ -0,0 +1 @@
+tL8+fuPHdTfbSgNCZVdXNdQ+wKg+87PpLyl5vgFSiqX1lg0WBNv43li9xM4kUU4Zj7YmjS49yqsyOFMhrW+g31Hh+cLZ3fx97XPVV97tu6/l8Dg9Ornldm6+wxg5zpOCrQyvF66DFouH9wMgw0iaEM3wWsvZjFDtsaa/kpgpPqGNUWhMFF6RNnbLeuGN22sm3d6FKEOFa0Ekl+YnKoxnbywshyG3+mAgpLAMt73u62KjiZsgIOku81k44WKDFLqqIeQdEumrWD8IxGFVIVPPm8xzGYq+K5LnIIIF0cw5coEVHZGU904fVvrVxmb0sMU5ZmozM6e98D86SpAQYj6LLFGbKFcZFBgiLknlLnkXDtwIN6chSUNT9lQmSqN4cL8byDwXI2X9hjugIcV7Qn/nGRJ1b4tOrWV/tWo/--CmilwP4O5Ch/FIvK--JD2c8xj2FnJJsSsRAU8HoQ==
\ No newline at end of file
diff --git a/config/database.yml b/config/database.yml
new file mode 100644
index 0000000..e6b4e29
--- /dev/null
+++ b/config/database.yml
@@ -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"] %>
diff --git a/config/environment.rb b/config/environment.rb
new file mode 100644
index 0000000..cac5315
--- /dev/null
+++ b/config/environment.rb
@@ -0,0 +1,5 @@
+# Load the Rails application.
+require_relative "application"
+
+# Initialize the Rails application.
+Rails.application.initialize!
diff --git a/config/environments/development.rb b/config/environments/development.rb
new file mode 100644
index 0000000..5ab2549
--- /dev/null
+++ b/config/environments/development.rb
@@ -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
diff --git a/config/environments/production.rb b/config/environments/production.rb
new file mode 100644
index 0000000..d84da77
--- /dev/null
+++ b/config/environments/production.rb
@@ -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
diff --git a/config/environments/test.rb b/config/environments/test.rb
new file mode 100644
index 0000000..eb2f171
--- /dev/null
+++ b/config/environments/test.rb
@@ -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
diff --git a/config/importmap.rb b/config/importmap.rb
new file mode 100644
index 0000000..909dfc5
--- /dev/null
+++ b/config/importmap.rb
@@ -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"
diff --git a/config/initializers/assets.rb b/config/initializers/assets.rb
new file mode 100644
index 0000000..2eeef96
--- /dev/null
+++ b/config/initializers/assets.rb
@@ -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 )
diff --git a/config/initializers/content_security_policy.rb b/config/initializers/content_security_policy.rb
new file mode 100644
index 0000000..54f47cf
--- /dev/null
+++ b/config/initializers/content_security_policy.rb
@@ -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
diff --git a/config/initializers/elasticsearch.rb b/config/initializers/elasticsearch.rb
new file mode 100644
index 0000000..3223147
--- /dev/null
+++ b/config/initializers/elasticsearch.rb
@@ -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)
diff --git a/config/initializers/filter_parameter_logging.rb b/config/initializers/filter_parameter_logging.rb
new file mode 100644
index 0000000..adc6568
--- /dev/null
+++ b/config/initializers/filter_parameter_logging.rb
@@ -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
+]
diff --git a/config/initializers/inflections.rb b/config/initializers/inflections.rb
new file mode 100644
index 0000000..3860f65
--- /dev/null
+++ b/config/initializers/inflections.rb
@@ -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
diff --git a/config/initializers/permissions_policy.rb b/config/initializers/permissions_policy.rb
new file mode 100644
index 0000000..00f64d7
--- /dev/null
+++ b/config/initializers/permissions_policy.rb
@@ -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
diff --git a/config/initializers/redis.rb b/config/initializers/redis.rb
new file mode 100644
index 0000000..df64f5d
--- /dev/null
+++ b/config/initializers/redis.rb
@@ -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
diff --git a/config/locales/en.yml b/config/locales/en.yml
new file mode 100644
index 0000000..8ca56fc
--- /dev/null
+++ b/config/locales/en.yml
@@ -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"
diff --git a/config/puma.rb b/config/puma.rb
new file mode 100644
index 0000000..daaf036
--- /dev/null
+++ b/config/puma.rb
@@ -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
diff --git a/config/routes.rb b/config/routes.rb
new file mode 100644
index 0000000..8fbba7c
--- /dev/null
+++ b/config/routes.rb
@@ -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
diff --git a/db/migrate/20240401101354_initial.rb b/db/migrate/20240401101354_initial.rb
new file mode 100644
index 0000000..eafa3a3
--- /dev/null
+++ b/db/migrate/20240401101354_initial.rb
@@ -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
diff --git a/db/migrate/20240401121829_temp_nullable_body.rb b/db/migrate/20240401121829_temp_nullable_body.rb
new file mode 100644
index 0000000..aee79b1
--- /dev/null
+++ b/db/migrate/20240401121829_temp_nullable_body.rb
@@ -0,0 +1,5 @@
+class TempNullableBody < ActiveRecord::Migration[7.0]
+ def change
+ change_column_null :chapters, :body, true
+ end
+end
diff --git a/db/migrate/20240402172140_remove_urls.rb b/db/migrate/20240402172140_remove_urls.rb
new file mode 100644
index 0000000..1ec40cb
--- /dev/null
+++ b/db/migrate/20240402172140_remove_urls.rb
@@ -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
diff --git a/db/schema.rb b/db/schema.rb
new file mode 100644
index 0000000..8dbebf4
--- /dev/null
+++ b/db/schema.rb
@@ -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
diff --git a/db/seeds.rb b/db/seeds.rb
new file mode 100644
index 0000000..bc25fce
--- /dev/null
+++ b/db/seeds.rb
@@ -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)
diff --git a/lib/assets/.keep b/lib/assets/.keep
new file mode 100644
index 0000000..e69de29
diff --git a/lib/tasks/.keep b/lib/tasks/.keep
new file mode 100644
index 0000000..e69de29
diff --git a/log/.keep b/log/.keep
new file mode 100644
index 0000000..e69de29
diff --git a/public/404.html b/public/404.html
new file mode 100644
index 0000000..2be3af2
--- /dev/null
+++ b/public/404.html
@@ -0,0 +1,67 @@
+
+
+
+ The page you were looking for doesn't exist (404)
+
+
+
+
+
+
+
+
+
The page you were looking for doesn't exist.
+
You may have mistyped the address or the page may have moved.
+
+
If you are the application owner check the logs for more information.
+
+
+
diff --git a/public/422.html b/public/422.html
new file mode 100644
index 0000000..c08eac0
--- /dev/null
+++ b/public/422.html
@@ -0,0 +1,67 @@
+
+
+
+ The change you wanted was rejected (422)
+
+
+
+
+
+
+
+
+
The change you wanted was rejected.
+
Maybe you tried to change something you didn't have access to.
+
+
If you are the application owner check the logs for more information.
+
+
+
diff --git a/public/500.html b/public/500.html
new file mode 100644
index 0000000..78a030a
--- /dev/null
+++ b/public/500.html
@@ -0,0 +1,66 @@
+
+
+
+ We're sorry, but something went wrong (500)
+
+
+
+
+
+
+
+
+
We're sorry, but something went wrong.
+
+
If you are the application owner check the logs for more information.
+
+
+
diff --git a/public/apple-touch-icon-precomposed.png b/public/apple-touch-icon-precomposed.png
new file mode 100644
index 0000000..e69de29
diff --git a/public/apple-touch-icon.png b/public/apple-touch-icon.png
new file mode 100644
index 0000000..e69de29
diff --git a/public/cached-images/.placeholder b/public/cached-images/.placeholder
new file mode 100644
index 0000000..e69de29
diff --git a/public/favicon.ico b/public/favicon.ico
new file mode 100644
index 0000000..e69de29
diff --git a/public/img/banner.png b/public/img/banner.png
new file mode 100644
index 0000000000000000000000000000000000000000..de885b7af1b8ca98ca33a48d533152aa84e04a4c
GIT binary patch
literal 55351
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