commit 563aa6a0f8544499337f7455c059117c57383c37 Author: Neetpone <132411956+Neetpone@users.noreply.github.com> Date: Tue Apr 2 13:33:19 2024 -0400 initial: something resembling a minimum viable product 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 0000000..de885b7 Binary files /dev/null and b/public/img/banner.png differ diff --git a/public/img/logo.png b/public/img/logo.png new file mode 100644 index 0000000..05c73d0 Binary files /dev/null and b/public/img/logo.png differ diff --git a/public/img/logo_small.png b/public/img/logo_small.png new file mode 100644 index 0000000..54551d3 Binary files /dev/null and b/public/img/logo_small.png differ diff --git a/public/robots.txt b/public/robots.txt new file mode 100644 index 0000000..c19f78a --- /dev/null +++ b/public/robots.txt @@ -0,0 +1 @@ +# See https://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file diff --git a/test/application_system_test_case.rb b/test/application_system_test_case.rb new file mode 100644 index 0000000..d19212a --- /dev/null +++ b/test/application_system_test_case.rb @@ -0,0 +1,5 @@ +require "test_helper" + +class ApplicationSystemTestCase < ActionDispatch::SystemTestCase + driven_by :selenium, using: :chrome, screen_size: [1400, 1400] +end diff --git a/test/channels/application_cable/connection_test.rb b/test/channels/application_cable/connection_test.rb new file mode 100644 index 0000000..800405f --- /dev/null +++ b/test/channels/application_cable/connection_test.rb @@ -0,0 +1,11 @@ +require "test_helper" + +class ApplicationCable::ConnectionTest < ActionCable::Connection::TestCase + # test "connects with cookies" do + # cookies.signed[:user_id] = 42 + # + # connect + # + # assert_equal connection.user_id, "42" + # end +end diff --git a/test/controllers/.keep b/test/controllers/.keep new file mode 100644 index 0000000..e69de29 diff --git a/test/controllers/authors_controller_test.rb b/test/controllers/authors_controller_test.rb new file mode 100644 index 0000000..088844c --- /dev/null +++ b/test/controllers/authors_controller_test.rb @@ -0,0 +1,7 @@ +require "test_helper" + +class AuthorsControllerTest < ActionDispatch::IntegrationTest + # test "the truth" do + # assert true + # end +end diff --git a/test/controllers/chapters_controller_test.rb b/test/controllers/chapters_controller_test.rb new file mode 100644 index 0000000..9c91965 --- /dev/null +++ b/test/controllers/chapters_controller_test.rb @@ -0,0 +1,7 @@ +require "test_helper" + +class ChaptersControllerTest < ActionDispatch::IntegrationTest + # test "the truth" do + # assert true + # end +end diff --git a/test/controllers/images_controller_test.rb b/test/controllers/images_controller_test.rb new file mode 100644 index 0000000..086c20d --- /dev/null +++ b/test/controllers/images_controller_test.rb @@ -0,0 +1,7 @@ +require "test_helper" + +class ImagesControllerTest < ActionDispatch::IntegrationTest + # test "the truth" do + # assert true + # end +end diff --git a/test/controllers/search_controller_test.rb b/test/controllers/search_controller_test.rb new file mode 100644 index 0000000..3b2d011 --- /dev/null +++ b/test/controllers/search_controller_test.rb @@ -0,0 +1,7 @@ +require "test_helper" + +class SearchControllerTest < ActionDispatch::IntegrationTest + # test "the truth" do + # assert true + # end +end diff --git a/test/controllers/static_pages_controller_test.rb b/test/controllers/static_pages_controller_test.rb new file mode 100644 index 0000000..bbd5bd9 --- /dev/null +++ b/test/controllers/static_pages_controller_test.rb @@ -0,0 +1,7 @@ +require "test_helper" + +class StaticPagesControllerTest < ActionDispatch::IntegrationTest + # test "the truth" do + # assert true + # end +end diff --git a/test/controllers/stories_controller_test.rb b/test/controllers/stories_controller_test.rb new file mode 100644 index 0000000..5c4a3da --- /dev/null +++ b/test/controllers/stories_controller_test.rb @@ -0,0 +1,7 @@ +require "test_helper" + +class StoriesControllerTest < ActionDispatch::IntegrationTest + # test "the truth" do + # assert true + # end +end diff --git a/test/fixtures/authors.yml b/test/fixtures/authors.yml new file mode 100644 index 0000000..d7a3329 --- /dev/null +++ b/test/fixtures/authors.yml @@ -0,0 +1,11 @@ +# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html + +# This model initially had no columns defined. If you add columns to the +# model remove the "{}" from the fixture names and add the columns immediately +# below each fixture, per the syntax in the comments below +# +one: {} +# column: value +# +two: {} +# column: value diff --git a/test/fixtures/chapters.yml b/test/fixtures/chapters.yml new file mode 100644 index 0000000..d7a3329 --- /dev/null +++ b/test/fixtures/chapters.yml @@ -0,0 +1,11 @@ +# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html + +# This model initially had no columns defined. If you add columns to the +# model remove the "{}" from the fixture names and add the columns immediately +# below each fixture, per the syntax in the comments below +# +one: {} +# column: value +# +two: {} +# column: value diff --git a/test/fixtures/files/.keep b/test/fixtures/files/.keep new file mode 100644 index 0000000..e69de29 diff --git a/test/fixtures/stories.yml b/test/fixtures/stories.yml new file mode 100644 index 0000000..d7a3329 --- /dev/null +++ b/test/fixtures/stories.yml @@ -0,0 +1,11 @@ +# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html + +# This model initially had no columns defined. If you add columns to the +# model remove the "{}" from the fixture names and add the columns immediately +# below each fixture, per the syntax in the comments below +# +one: {} +# column: value +# +two: {} +# column: value diff --git a/test/fixtures/tags.yml b/test/fixtures/tags.yml new file mode 100644 index 0000000..d7a3329 --- /dev/null +++ b/test/fixtures/tags.yml @@ -0,0 +1,11 @@ +# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html + +# This model initially had no columns defined. If you add columns to the +# model remove the "{}" from the fixture names and add the columns immediately +# below each fixture, per the syntax in the comments below +# +one: {} +# column: value +# +two: {} +# column: value diff --git a/test/helpers/.keep b/test/helpers/.keep new file mode 100644 index 0000000..e69de29 diff --git a/test/integration/.keep b/test/integration/.keep new file mode 100644 index 0000000..e69de29 diff --git a/test/models/.keep b/test/models/.keep new file mode 100644 index 0000000..e69de29 diff --git a/test/models/author_test.rb b/test/models/author_test.rb new file mode 100644 index 0000000..b2d7df1 --- /dev/null +++ b/test/models/author_test.rb @@ -0,0 +1,7 @@ +require "test_helper" + +class AuthorTest < ActiveSupport::TestCase + # test "the truth" do + # assert true + # end +end diff --git a/test/models/chapter_test.rb b/test/models/chapter_test.rb new file mode 100644 index 0000000..a89eb86 --- /dev/null +++ b/test/models/chapter_test.rb @@ -0,0 +1,7 @@ +require "test_helper" + +class ChapterTest < ActiveSupport::TestCase + # test "the truth" do + # assert true + # end +end diff --git a/test/models/story_test.rb b/test/models/story_test.rb new file mode 100644 index 0000000..275e648 --- /dev/null +++ b/test/models/story_test.rb @@ -0,0 +1,7 @@ +require "test_helper" + +class StoryTest < ActiveSupport::TestCase + # test "the truth" do + # assert true + # end +end diff --git a/test/models/tag_test.rb b/test/models/tag_test.rb new file mode 100644 index 0000000..1846cdb --- /dev/null +++ b/test/models/tag_test.rb @@ -0,0 +1,7 @@ +require "test_helper" + +class TagTest < ActiveSupport::TestCase + # test "the truth" do + # assert true + # end +end diff --git a/test/system/.keep b/test/system/.keep new file mode 100644 index 0000000..e69de29 diff --git a/test/test_helper.rb b/test/test_helper.rb new file mode 100644 index 0000000..d713e37 --- /dev/null +++ b/test/test_helper.rb @@ -0,0 +1,13 @@ +ENV["RAILS_ENV"] ||= "test" +require_relative "../config/environment" +require "rails/test_help" + +class ActiveSupport::TestCase + # Run tests in parallel with specified workers + parallelize(workers: :number_of_processors) + + # Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order. + fixtures :all + + # Add more helper methods to be used by all tests here... +end diff --git a/tmp/.keep b/tmp/.keep new file mode 100644 index 0000000..e69de29 diff --git a/tmp/pids/.keep b/tmp/pids/.keep new file mode 100644 index 0000000..e69de29 diff --git a/vendor/.keep b/vendor/.keep new file mode 100644 index 0000000..e69de29 diff --git a/vendor/javascript/.keep b/vendor/javascript/.keep new file mode 100644 index 0000000..e69de29