diff --git a/.circleci/config.yml b/.circleci/config.yml new file mode 100644 index 000000000..6c179ed60 --- /dev/null +++ b/.circleci/config.yml @@ -0,0 +1,138 @@ +version: 2.1 + +orbs: + browser-tools: circleci/browser-tools@2.2.0 + node: circleci/node@7.1.0 + +jobs: + build: + parallelism: 1 + docker: + - image: cimg/ruby:4.0.2-browsers + environment: + BUNDLE_PATH: vendor/bundle + NODE_VERSION: 22.17.1 + PGHOST: 127.0.0.1 + PGUSER: postgres + RAILS_ENV: test + - image: cimg/postgres:10.18 + environment: + POSTGRES_USER: postgres + POSTGRES_DB: app_test + POSTGRES_PASSWORD: + + steps: + - checkout + + - node/install: + node-version: 25.8.1 + + - node/install-packages: + pkg-manager: pnpm + + - browser-tools/install_firefox + + - run: + name: Which versions? + command: | + bundle -v + node --version + pnpm --version + + # https://circleci.com/docs/2.0/caching/ + - restore_cache: + keys: + - bundle-v2-{{ checksum "Gemfile.lock" }} + - bundle-v2 + + - run: # Install Ruby dependencies + name: Bundle Install + command: | + bundle config set --local frozen 'true' + bundle install + bundle clean + + - save_cache: + key: bundle-v2-{{ checksum "Gemfile.lock" }} + paths: + - vendor/bundle + + - run: + name: Wait for DB + command: dockerize -wait tcp://localhost:5432 -timeout 1m + + - run: + name: Database setup + command: bin/rails db:setup --trace + + - run: + name: Typescript + command: pnpm tscheck + + - run: + name: Find Unused ESLint Rules + command: pnpm eslint_find_unused_rules + + - run: + name: ESLint + command: pnpm eslint + + - run: + name: Verify ESLint Autogen + command: bundle exec exe/eslint_autogen + + - run: + name: Vitest + command: pnpm vitest run --coverage + + - run: + name: Brakeman + command: bundle exec brakeman + + - run: + name: Stylelint + command: pnpm stylelint + + - run: + name: Verify Stylelint Autogen + command: bundle exec exe/stylelint_autogen + + - run: + name: Rubocop + command: bundle exec rubocop + + - run: + name: ✨ 🌈 ✨ Run Unit Tests ✨ 🌈 ✨ + command: | + TEST_FILES="$(circleci tests glob "spec/**/*_spec.rb" | \ + grep -v "spec/system" | \ + circleci tests split --split-by=timings)" + + echo "******************** TEST_FILES ************************" + echo "bundle exec rspec -s $TEST_FILES" + echo "********************************************************" + + COVERAGE=true bundle exec rspec \ + --format progress \ + --format RspecJunitFormatter \ + --out /tmp/test-results/rspec.xml \ + $TEST_FILES + + - run: + name: ✨ 🌈 ✨ Run System Tests ✨ 🌈 ✨ + command: | + TEST_FILES="$(circleci tests glob "spec/system/**/*_spec.rb" | \ + circleci tests split --split-by=timings)" + + echo "******************** TEST_FILES ************************" + echo "bundle exec rspec -s $TEST_FILES" + echo "********************************************************" + + COVERAGE=false xvfb-run -a bundle exec rspec \ + --format progress \ + --format RspecJunitFormatter \ + --out /tmp/test-results/rspec.xml \ + $TEST_FILES + + - store_test_results: + path: test_results diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 000000000..4a2324570 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,3 @@ +.git/* +.gitignore +log/* diff --git a/.env.development b/.env.development new file mode 100644 index 000000000..7ae8c1e97 --- /dev/null +++ b/.env.development @@ -0,0 +1,7 @@ +# you can use `rails secret` to generate this for production +SECRET_KEY_BASE=dev_secret + +# you can use `rails db:encryption:init` to generate these for production +ENCRYPTION_PRIMARY_KEY=dev_primary_key +ENCRYPTION_DETERMINISTIC_KEY=dev_deterministic_key +ENCRYPTION_KEY_DERIVATION_SALT=dev_derivation_salt diff --git a/.env.test b/.env.test new file mode 100644 index 000000000..b6f983942 --- /dev/null +++ b/.env.test @@ -0,0 +1,7 @@ +# you can use `rails secret` to generate this for production +SECRET_KEY_BASE=test_secret + +# you can use `rails db:encryption:init` to generate these for production +ENCRYPTION_PRIMARY_KEY=test_primary_key +ENCRYPTION_DETERMINISTIC_KEY=test_deterministic_key +ENCRYPTION_KEY_DERIVATION_SALT=test_derivation_salt diff --git a/.eslint_todo.ts b/.eslint_todo.ts new file mode 100644 index 000000000..58bbfd215 --- /dev/null +++ b/.eslint_todo.ts @@ -0,0 +1,598 @@ +// This configuration was generated by `exe/eslint_autogen` +// on 2026-03-11 18:22:32 UTC. +// The point is for the user to remove these configuration records +// one by one as the offenses are removed from the code base. + +import type {Linter} from "eslint"; + +const config: Linter.Config[] = [ + // Offense count: 6 + { + files: [ + "app/javascript/application.ts", + ], + rules: { + "@stylistic/comma-dangle": "off", + }, + }, + // Offense count: 3 + { + files: [ + "app/javascript/application.ts", + ], + rules: { + "@stylistic/key-spacing": "off", + }, + }, + // Offense count: 2 + { + files: [ + "app/javascript/application.ts", + ], + rules: { + "@stylistic/keyword-spacing": "off", + }, + }, + // Offense count: 19 + { + files: [ + "app/javascript/application.ts", + "spec/javascript/setup.ts", + "spec/javascript/spec/models/story_spec.ts", + "spec/javascript/spec/views/story_view_spec.ts", + "vitest.config.ts", + ], + rules: { + "@stylistic/max-len": "off", + }, + }, + // Offense count: 2 + { + files: [ + "spec/javascript/setup.ts", + ], + rules: { + "@stylistic/multiline-comment-style": "off", + }, + }, + // Offense count: 4 + { + files: [ + "app/javascript/application.ts", + "spec/javascript/spec/views/story_view_spec.ts", + ], + rules: { + "@stylistic/multiline-ternary": "off", + }, + }, + // Offense count: 3 + { + files: [ + "app/javascript/application.ts", + "spec/javascript/spec/models/story_spec.ts", + ], + rules: { + "@stylistic/no-extra-parens": "off", + }, + }, + // Offense count: 26 + { + files: [ + "app/javascript/application.ts", + "spec/javascript/setup.ts", + "spec/javascript/spec/models/story_spec.ts", + "spec/javascript/spec/views/story_view_spec.ts", + ], + rules: { + "@stylistic/object-curly-spacing": "off", + }, + }, + // Offense count: 3 + { + files: [ + "app/javascript/application.ts", + "stylelint.config.mjs", + ], + rules: { + "@stylistic/quote-props": "off", + }, + }, + // Offense count: 41 + { + files: [ + "app/javascript/application.ts", + "spec/javascript/setup.ts", + ], + rules: { + "@stylistic/quotes": "off", + }, + }, + // Offense count: 1 + { + files: [ + "app/javascript/application.ts", + ], + rules: { + "@stylistic/semi": "off", + }, + }, + // Offense count: 1 + { + files: [ + "app/javascript/application.ts", + ], + rules: { + "@stylistic/space-before-blocks": "off", + }, + }, + // Offense count: 43 + { + files: [ + "app/javascript/application.ts", + ], + rules: { + "@stylistic/space-before-function-paren": "off", + }, + }, + // Offense count: 4 + { + files: [ + "app/javascript/application.ts", + "spec/javascript/setup.ts", + "spec/javascript/spec/models/story_spec.ts", + "spec/javascript/spec/views/story_view_spec.ts", + ], + rules: { + "@typescript-eslint/ban-ts-comment": "off", + }, + }, + // Offense count: 7 + { + files: [ + "app/javascript/application.ts", + "spec/javascript/setup.ts", + "spec/javascript/spec/views/story_view_spec.ts", + ], + rules: { + "@typescript-eslint/explicit-function-return-type": "off", + }, + }, + // Offense count: 3 + { + files: [ + "spec/javascript/spec/views/story_view_spec.ts", + ], + rules: { + "@typescript-eslint/init-declarations": "off", + }, + }, + // Offense count: 2 + { + files: [ + "app/javascript/application.ts", + ], + rules: { + "@typescript-eslint/no-deprecated": "off", + }, + }, + // Offense count: 5 + { + files: [ + "spec/javascript/spec/models/story_spec.ts", + "spec/javascript/spec/views/story_view_spec.ts", + ], + rules: { + "@typescript-eslint/no-empty-function": "off", + }, + }, + // Offense count: 9 + { + files: [ + "app/javascript/application.ts", + "spec/javascript/spec/views/story_view_spec.ts", + ], + rules: { + "@typescript-eslint/no-unsafe-argument": "off", + }, + }, + // Offense count: 51 + { + files: [ + "app/javascript/application.ts", + "app/javascript/support/jquery-global.ts", + "spec/javascript/setup.ts", + "spec/javascript/spec/models/story_spec.ts", + "spec/javascript/spec/views/story_view_spec.ts", + ], + rules: { + "@typescript-eslint/no-unsafe-assignment": "off", + }, + }, + // Offense count: 171 + { + files: [ + "app/javascript/application.ts", + "spec/javascript/setup.ts", + "spec/javascript/spec/models/story_spec.ts", + "spec/javascript/spec/views/story_view_spec.ts", + ], + rules: { + "@typescript-eslint/no-unsafe-call": "off", + }, + }, + // Offense count: 262 + { + files: [ + "app/javascript/application.ts", + "spec/javascript/spec/models/story_spec.ts", + "spec/javascript/spec/views/story_view_spec.ts", + ], + rules: { + "@typescript-eslint/no-unsafe-member-access": "off", + }, + }, + // Offense count: 6 + { + files: [ + "app/javascript/application.ts", + ], + rules: { + "@typescript-eslint/no-unsafe-return": "off", + }, + }, + // Offense count: 2 + { + files: [ + "app/javascript/application.ts", + ], + rules: { + "@typescript-eslint/prefer-destructuring": "off", + }, + }, + // Offense count: 1 + { + files: [ + "app/javascript/application.ts", + ], + rules: { + "@typescript-eslint/prefer-nullish-coalescing": "off", + }, + }, + // Offense count: 1 + { + files: [ + "app/javascript/application.ts", + ], + rules: { + "@typescript-eslint/prefer-optional-chain": "off", + }, + }, + // Offense count: 20 + { + files: [ + "app/javascript/application.ts", + "spec/javascript/spec/models/story_spec.ts", + ], + rules: { + "@typescript-eslint/strict-boolean-expressions": "off", + }, + }, + // Offense count: 13 + { + files: [ + "app/javascript/application.ts", + "spec/javascript/spec/models/story_spec.ts", + "spec/javascript/spec/views/story_view_spec.ts", + ], + rules: { + "camelcase": "off", + }, + }, + // Offense count: 1 + { + files: [ + "spec/javascript/setup.ts", + ], + rules: { + "capitalized-comments": "off", + }, + }, + // Offense count: 11 + { + files: [ + "app/javascript/application.ts", + ], + rules: { + "curly": "off", + }, + }, + // Offense count: 2 + { + files: [ + "app/javascript/application.ts", + ], + rules: { + "eqeqeq": "off", + }, + }, + // Offense count: 107 + { + files: [ + "app/javascript/application.ts", + "spec/javascript/setup.ts", + "spec/javascript/spec/models/story_spec.ts", + "spec/javascript/spec/views/story_view_spec.ts", + ], + rules: { + "func-names": "off", + }, + }, + // Offense count: 3 + { + files: [ + "spec/javascript/spec/views/story_view_spec.ts", + ], + rules: { + "func-style": "off", + }, + }, + // Offense count: 16 + { + files: [ + "app/javascript/application.ts", + "app/javascript/support/jquery-global.ts", + "spec/javascript/setup.ts", + "spec/javascript/spec/views/story_view_spec.ts", + ], + rules: { + "id-length": "off", + }, + }, + // Offense count: 1 + { + files: [ + "app/javascript/application.ts", + ], + rules: { + "import/no-unresolved": "off", + }, + }, + // Offense count: 16 + { + files: [ + "app/javascript/application.ts", + "spec/javascript/setup.ts", + "spec/javascript/spec/views/story_view_spec.ts", + "vitest.config.ts", + ], + rules: { + "max-len": "off", + }, + }, + // Offense count: 1 + { + files: [ + "app/javascript/application.ts", + ], + rules: { + "max-lines": "off", + }, + }, + // Offense count: 3 + { + files: [ + "spec/javascript/spec/models/story_spec.ts", + "spec/javascript/spec/views/story_view_spec.ts", + ], + rules: { + "max-lines-per-function": "off", + }, + }, + // Offense count: 1 + { + files: [ + "spec/javascript/spec/views/story_view_spec.ts", + ], + rules: { + "max-statements": "off", + }, + }, + // Offense count: 4 + { + files: [ + "app/javascript/application.ts", + "spec/javascript/setup.ts", + "spec/javascript/spec/views/story_view_spec.ts", + ], + rules: { + "new-cap": "off", + }, + }, + // Offense count: 2 + { + files: [ + "spec/javascript/spec/models/story_spec.ts", + ], + rules: { + "no-implicit-coercion": "off", + }, + }, + // Offense count: 2 + { + files: [ + "spec/javascript/spec/models/story_spec.ts", + ], + rules: { + "no-multi-assign": "off", + }, + }, + // Offense count: 1 + { + files: [ + "spec/javascript/spec/views/story_view_spec.ts", + ], + rules: { + "no-negated-condition": "off", + }, + }, + // Offense count: 1 + { + files: [ + "spec/javascript/spec/views/story_view_spec.ts", + ], + rules: { + "no-param-reassign": "off", + }, + }, + // Offense count: 2 + { + files: [ + "app/javascript/application.ts", + ], + rules: { + "no-plusplus": "off", + }, + }, + // Offense count: 2 + { + files: [ + "app/javascript/application.ts", + "spec/javascript/spec/views/story_view_spec.ts", + ], + rules: { + "no-ternary": "off", + }, + }, + // Offense count: 40 + { + files: [ + "app/javascript/application.ts", + ], + rules: { + "object-shorthand": "off", + }, + }, + // Offense count: 1 + { + files: [ + "spec/javascript/spec/views/story_view_spec.ts", + ], + rules: { + "one-var": "off", + }, + }, + // Offense count: 62 + { + files: [ + "app/javascript/application.ts", + "spec/javascript/spec/models/story_spec.ts", + "spec/javascript/spec/views/story_view_spec.ts", + ], + rules: { + "prefer-arrow-callback": "off", + }, + }, + // Offense count: 4 + { + files: [ + "app/javascript/application.ts", + "spec/javascript/setup.ts", + ], + rules: { + "prefer-named-capture-group": "off", + }, + }, + // Offense count: 1 + { + files: [ + "app/javascript/application.ts", + ], + rules: { + "prefer-template": "off", + }, + }, + // Offense count: 4 + { + files: [ + "app/javascript/application.ts", + "spec/javascript/setup.ts", + ], + rules: { + "require-unicode-regexp": "off", + }, + }, + // Offense count: 1 + { + files: [ + "spec/javascript/setup.ts", + ], + rules: { + "sort-imports": "off", + }, + }, + // Offense count: 26 + { + files: [ + "app/javascript/application.ts", + "spec/javascript/setup.ts", + "spec/javascript/spec/models/story_spec.ts", + "spec/javascript/spec/views/story_view_spec.ts", + ], + rules: { + "sort-keys": "off", + }, + }, + // Offense count: 26 + { + files: [ + "app/javascript/application.ts", + "spec/javascript/setup.ts", + "spec/javascript/spec/models/story_spec.ts", + "spec/javascript/spec/views/story_view_spec.ts", + ], + rules: { + "sort-keys-fix/sort-keys-fix": "off", + }, + }, + // Offense count: 11 + { + files: [ + "app/javascript/application.ts", + "spec/javascript/spec/views/story_view_spec.ts", + ], + rules: { + "vars-on-top": "off", + }, + }, + // Offense count: 3 + { + files: [ + "spec/javascript/spec/views/story_view_spec.ts", + ], + rules: { + "vitest/no-hooks": "off", + }, + }, + // Offense count: 4 + { + files: [ + "spec/javascript/spec/models/story_spec.ts", + "spec/javascript/spec/views/story_view_spec.ts", + ], + rules: { + "vitest/prefer-lowercase-title": "off", + }, + }, + // Offense count: 4 + { + files: [ + "spec/javascript/setup.ts", + "spec/javascript/spec/views/story_view_spec.ts", + ], + rules: { + "vitest/require-hook": "off", + }, + }, +]; + +export default config; diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 000000000..31eeee0b6 --- /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/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 000000000..d916cd6ae --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1 @@ +github: mockdeep diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 000000000..937450997 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,57 @@ +name: Build Docker image and push to Dockerhub + +on: + push: + pull_request: + workflow_dispatch: + +env: + IMAGE_NAME: stringerrss/stringer + +jobs: + build_docker: + runs-on: ubuntu-22.04 + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + id: buildx + uses: docker/setup-buildx-action@v3 + + - name: Extract Docker sha tag + id: get-tag-sha + uses: docker/metadata-action@v4 + with: + images: ${{ env.IMAGE_NAME }} + tags: type=sha + + - name: Extract Docker latest tag + id: get-tag-latest + uses: docker/metadata-action@v5 + with: + images: ${{ env.IMAGE_NAME }} + tags: type=raw, value=latest + + - name: Log in to Docker Hub + if: ${{ github.ref_name == 'main' }} + id: login + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Build and Push + id: build-and-push + uses: docker/build-push-action@v6 + with: + context: . + file: ./Dockerfile + platforms: linux/amd64,linux/arm64 + push: ${{ github.ref_name == 'main' }} + tags: | + ${{ steps.get-tag-latest.outputs.tags }} + ${{ steps.get-tag-sha.outputs.tags }} diff --git a/.gitignore b/.gitignore index b67af0ebe..bc757bbf7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,24 +1,37 @@ -*.gem -*.rbc -.bundle -.config -.ruby-version -coverage -InstalledFiles -lib/bundler/man -pkg -rdoc -spec/reports -test/tmp -test/version_tmp -tmp +# 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' -# YARD artifacts -.yardoc -_yardoc -doc/ -bin/ +# Ignore bundler config. +/.bundle -db/*.sqlite -.DS_Store -.localeapp \ No newline at end of file +# Ignore all logfiles and tempfiles. +/log/* +/tmp/* +!/log/.keep +!/tmp/.keep + +# Ignore uploaded files in development. +/storage/* +!/storage/.keep + +/public/assets +.byebug_history + +# Ignore master key for decrypting credentials and more. +/config/master.key +spec/examples.txt +/coverage/* + +/public/packs +/public/packs-test +/node_modules +/app/assets/builds/* +!/app/assets/builds/.keep +tsconfig.tsbuildinfo +.eslintcache + +# Ignore local environment variable files. +.env.*.local diff --git a/.mcp.json b/.mcp.json new file mode 100644 index 000000000..6fdb146fc --- /dev/null +++ b/.mcp.json @@ -0,0 +1,16 @@ +{ + "mcpServers": { + "playwright": { + "type": "stdio", + "command": "npx", + "args": [ + "-y", + "@playwright/mcp@latest", + "--caps", + "core,tabs,vision", + "--output-dir", + ".playwright-mcp" + ] + } + } +} diff --git a/.rspec b/.rspec index cf6add7ea..a35c44f4b 100644 --- a/.rspec +++ b/.rspec @@ -1 +1 @@ ---colour \ No newline at end of file +--require rails_helper diff --git a/.rubocop.yml b/.rubocop.yml new file mode 100644 index 000000000..816628fa0 --- /dev/null +++ b/.rubocop.yml @@ -0,0 +1,77 @@ +inherit_from: .rubocop_todo.yml +inherit_mode: { merge: [Exclude] } + +plugins: + - rubocop-capybara + - rubocop-factory_bot + - rubocop-rails + - rubocop-rake + - rubocop-rspec + - rubocop-rspec_rails + +AllCops: + DisplayCopNames: true + EnabledByDefault: true + Exclude: + - 'db/schema.rb' + - 'vendor/**/*' + +Capybara/ClickLinkOrButtonStyle: { EnforcedStyle: link_or_button } +Layout/LineLength: { Max: 80, Exclude: [db/migrate/*.rb] } +Layout/RedundantLineBreak: { InspectBlocks: true } +Metrics/AbcSize: { Exclude: [db/migrate/*.rb], CountRepeatedAttributes: false } +Metrics/BlockLength: + Exclude: [config/**/*.rb, db/migrate/*.rb, spec/**/*.rb, db/seeds/**/*.rb] +Metrics/MethodLength: { Exclude: [db/migrate/*.rb] } +Rails/SkipsModelValidations: { AllowedMethods: [update_all] } +RSpec/DescribeClass: { Exclude: [spec/system/**/*] } +RSpec/MessageExpectation: + EnforcedStyle: expect + Exclude: [spec/support/matchers/**/*.rb] +RSpec/MessageSpies: { EnforcedStyle: receive } +RSpec/MultipleMemoizedHelpers: { AllowSubject: false, Max: 0 } +Style/ClassAndModuleChildren: { EnforcedStyle: compact } +Style/MethodCallWithArgsParentheses: + AllowedMethods: + - and + - to + - not_to + - describe + - require + - task + Exclude: + - db/**/*.rb +Style/StringLiterals: { EnforcedStyle: double_quotes } +Style/SymbolArray: { EnforcedStyle: brackets } +Style/WordArray: { EnforcedStyle: brackets } + +# want to enable these, but they don't work right when using `.rubocop_todo.yml` +Style/DocumentationMethod: { Enabled: false } +Style/Documentation: { Enabled: false } + +################################################################################ +# +# Rules we don't want to enable +# +################################################################################ + +Bundler/GemComment: { Enabled: false } +Bundler/GemVersion: { Enabled: false } +Capybara/AmbiguousClick: { Enabled: false } +Layout/SingleLineBlockChain: { Enabled: false } +Lint/ConstantResolution: { Enabled: false } +Rails/BulkChangeTable: { Enabled: false } +Rails/RedundantPresenceValidationOnBelongsTo: { Enabled: false } +RSpec/AlignLeftLetBrace: { Enabled: false } +RSpec/AlignRightLetBrace: { Enabled: false } +Rails/HasManyOrHasOneDependent: { Enabled: false } +RSpec/IndexedLet: { Enabled: false } +RSpec/StubbedMock: { Enabled: false } +Rails/SchemaComment: { Enabled: false } +Style/ConstantVisibility: { Enabled: false } +Style/Copyright: { Enabled: false } +Style/InlineComment: { Enabled: false } +Style/MissingElse: { Enabled: false } +Style/RequireOrder: { Enabled: false } +Style/SafeNavigation: { Enabled: false } +Style/StringHashKeys: { Enabled: false } diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml new file mode 100644 index 000000000..48f278ad1 --- /dev/null +++ b/.rubocop_todo.yml @@ -0,0 +1,324 @@ +# This configuration was generated by +# `rubocop --auto-gen-config --auto-gen-only-exclude --exclude-limit 400` +# on 2026-03-11 01:58:30 UTC using RuboCop version 1.85.1. +# The point is for the user to remove these configuration records +# one by one as the offenses are removed from the code base. +# Note that changes in the inspected code, or installation of new +# versions of RuboCop, may require this file to be generated again. + +# Offense count: 3 +Capybara/NegationMatcherAfterVisit: + Exclude: + - 'spec/system/account_setup_spec.rb' + - 'spec/system/stories_index_spec.rb' + +# Offense count: 6 +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: AllowMultipleStyles, EnforcedHashRocketStyle, EnforcedColonStyle, EnforcedLastArgumentHashStyle. +# SupportedHashRocketStyles: key, separator, table +# SupportedColonStyles: key, separator, table +# SupportedLastArgumentHashStyles: always_inspect, always_ignore, ignore_implicit, ignore_explicit +Layout/HashAlignment: + Exclude: + - 'db/migrate/20240314031219_recreate_good_job_cron_indexes_with_conditional.rb' + - 'db/migrate/20240314031221_create_good_job_labels_index.rb' + - 'db/migrate/20240314031223_create_index_good_job_jobs_for_candidate_lookup.rb' + +# Offense count: 19 +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: AllowMultilineFinalElement. +Layout/MultilineMethodArgumentLineBreaks: + Exclude: + - 'db/migrate/20240314031219_recreate_good_job_cron_indexes_with_conditional.rb' + - 'db/migrate/20240314031221_create_good_job_labels_index.rb' + - 'db/migrate/20240314031223_create_index_good_job_jobs_for_candidate_lookup.rb' + +# Offense count: 10 +# This cop supports unsafe autocorrection (--autocorrect-all). +# Configuration parameters: AllowedMethods, AllowedPatterns, IgnoredClasses. +# AllowedMethods: ago, from_now, second, seconds, minute, minutes, hour, hours, day, days, week, weeks, fortnight, fortnights, in_milliseconds +# IgnoredClasses: Time, DateTime +Lint/NumberConversion: + Exclude: + - 'Rakefile' + - 'app/commands/fever_api/authentication.rb' + - 'app/commands/story/mark_group_as_read.rb' + - 'app/models/feed.rb' + - 'app/models/story.rb' + - 'app/repositories/story_repository.rb' + - 'spec/models/feed_spec.rb' + - 'spec/models/story_spec.rb' + +# Offense count: 1 +# Configuration parameters: AllowedMethods, AllowedPatterns, Max. +Metrics/CyclomaticComplexity: + Exclude: + - 'db/migrate/20240314031219_recreate_good_job_cron_indexes_with_conditional.rb' + +# Offense count: 5 +# Configuration parameters: CountComments, Max, CountAsOne, AllowedMethods, AllowedPatterns. +Metrics/MethodLength: + Exclude: + - 'app/models/story.rb' + - 'app/repositories/story_repository.rb' + - 'app/utils/opml_parser.rb' + - 'app/utils/sample_story.rb' + +# Offense count: 1 +# Configuration parameters: AllowedMethods, AllowedPatterns, Max. +Metrics/PerceivedComplexity: + Exclude: + - 'db/migrate/20240314031219_recreate_good_job_cron_indexes_with_conditional.rb' + +# Offense count: 3 +# Configuration parameters: Mode, AllowedMethods, AllowedPatterns, AllowBangMethods, WaywardPredicates. +# AllowedMethods: call +# WaywardPredicates: infinite?, nonzero? +Naming/PredicateMethod: + Exclude: + - 'app/utils/sample_story.rb' + +# Offense count: 2 +# Configuration parameters: NamePrefix, ForbiddenPrefixes, AllowedMethods, MethodDefinitionMacros, UseSorbetSigs. +# NamePrefix: is_, has_, have_, does_ +# ForbiddenPrefixes: is_, has_, have_, does_ +# AllowedMethods: is_a? +# MethodDefinitionMacros: define_method, define_singleton_method +Naming/PredicatePrefix: + Exclude: + - 'app/utils/sample_story.rb' + +# Offense count: 4 +RSpec/Be: + Exclude: + - 'spec/commands/feed/import_from_opml_spec.rb' + +# Offense count: 13 +# Configuration parameters: Prefixes, AllowedPatterns. +# Prefixes: when, with, without +RSpec/ContextWording: + Exclude: + - 'spec/commands/feed/create_spec.rb' + - 'spec/commands/feed/fetch_one_spec.rb' + - 'spec/commands/feed/find_new_stories_spec.rb' + - 'spec/commands/feed/import_from_opml_spec.rb' + - 'spec/integration/feed_importing_spec.rb' + +# Offense count: 1 +# Configuration parameters: IgnoredMetadata. +RSpec/DescribeClass: + Exclude: + - 'spec/integration/feed_importing_spec.rb' + +# Offense count: 54 +# Configuration parameters: Max, CountAsOne. +RSpec/ExampleLength: + Exclude: + - 'spec/commands/feed/create_spec.rb' + - 'spec/commands/feed/export_to_opml_spec.rb' + - 'spec/commands/fever_api/read_favicons_spec.rb' + - 'spec/commands/fever_api/read_feeds_groups_spec.rb' + - 'spec/commands/fever_api/read_items_spec.rb' + - 'spec/helpers/url_helpers_spec.rb' + - 'spec/integration/feed_importing_spec.rb' + - 'spec/models/feed_spec.rb' + - 'spec/models/migration_status_spec.rb' + - 'spec/models/story_spec.rb' + - 'spec/repositories/group_repository_spec.rb' + - 'spec/repositories/story_repository_spec.rb' + - 'spec/system/add_feed_spec.rb' + - 'spec/system/archive_spec.rb' + - 'spec/system/feed_edit_spec.rb' + - 'spec/system/feed_show_spec.rb' + - 'spec/system/feeds_index_spec.rb' + - 'spec/system/good_job_spec.rb' + - 'spec/system/starred_spec.rb' + - 'spec/system/stories_index_spec.rb' + - 'spec/tasks/remove_old_stories_spec.rb' + - 'spec/utils/feed_discovery_spec.rb' + - 'spec/utils/opml_parser_spec.rb' + +# Offense count: 16 +RSpec/LeakyLocalVariable: + Exclude: + - 'spec/commands/feed/import_from_opml_spec.rb' + - 'spec/integration/feed_importing_spec.rb' + - 'spec/requests/feeds_controller_spec.rb' + - 'spec/requests/imports_controller_spec.rb' + - 'spec/requests/stories_controller_spec.rb' + - 'spec/utils/feed_discovery_spec.rb' + +# Offense count: 17 +# Configuration parameters: EnforcedStyle. +# SupportedStyles: allow, expect +RSpec/MessageExpectation: + Exclude: + - 'spec/commands/feed/fetch_one_spec.rb' + - 'spec/models/migration_status_spec.rb' + - 'spec/repositories/story_repository_spec.rb' + - 'spec/tasks/remove_old_stories_spec.rb' + - 'spec/utils/i18n_support_spec.rb' + +# Offense count: 28 +# Configuration parameters: Max. +RSpec/MultipleExpectations: + Exclude: + - 'spec/commands/feed/create_spec.rb' + - 'spec/commands/feed/export_to_opml_spec.rb' + - 'spec/commands/feed/import_from_opml_spec.rb' + - 'spec/repositories/feed_repository_spec.rb' + - 'spec/repositories/story_repository_spec.rb' + - 'spec/system/add_feed_spec.rb' + - 'spec/system/starred_spec.rb' + - 'spec/tasks/remove_old_stories_spec.rb' + - 'spec/utils/feed_discovery_spec.rb' + - 'spec/utils/i18n_support_spec.rb' + - 'spec/utils/opml_parser_spec.rb' + +# Offense count: 5 +# Configuration parameters: EnforcedStyle, IgnoreSharedExamples. +# SupportedStyles: always, named_only +RSpec/NamedSubject: + Exclude: + - 'spec/commands/fever_api/write_mark_item_spec.rb' + +# Offense count: 2 +# Configuration parameters: Max, AllowedGroups. +RSpec/NestedGroups: + Exclude: + - 'spec/integration/feed_importing_spec.rb' + +# Offense count: 5 +# Configuration parameters: IgnoreNameless, IgnoreSymbolicNames. +RSpec/VerifiedDoubles: + Exclude: + - 'spec/commands/feed/fetch_one_spec.rb' + - 'spec/commands/feed/find_new_stories_spec.rb' + - 'spec/tasks/remove_old_stories_spec.rb' + +# Offense count: 1 +Rails/Env: + Exclude: + - 'spec/rails_helper.rb' + +# Offense count: 2 +# Configuration parameters: IgnoreScopes. +Rails/InverseOf: + Exclude: + - 'app/models/feed.rb' + +# Offense count: 3 +Rails/ReversibleMigrationMethodDefinition: + Exclude: + - 'db/migrate/20130423001740_drop_email_from_user.rb' + - 'db/migrate/20130423180446_remove_author_from_stories.rb' + - 'db/migrate/20130425222157_add_delayed_job.rb' + +# Offense count: 9 +# This cop supports unsafe autocorrection (--autocorrect-all). +# Configuration parameters: AllowImplicitReturn, AllowedReceivers. +Rails/SaveBang: + Exclude: + - 'app/commands/feed/create.rb' + - 'app/commands/feed/import_from_opml.rb' + - 'app/repositories/feed_repository.rb' + - 'app/repositories/story_repository.rb' + - 'app/repositories/user_repository.rb' + - 'db/migrate/20130821020313_update_nil_entry_ids.rb' + +# Offense count: 2 +# Configuration parameters: ForbiddenMethods, AllowedMethods. +# ForbiddenMethods: decrement!, decrement_counter, increment!, increment_counter, insert, insert!, insert_all, insert_all!, toggle!, touch, touch_all, update_all, update_attribute, update_column, update_columns, update_counters, upsert, upsert_all +Rails/SkipsModelValidations: + Exclude: + - 'db/migrate/20140421224454_fix_invalid_unicode.rb' + - 'db/migrate/20141102103617_fix_invalid_titles_with_unicode_line_endings.rb' + +# Offense count: 5 +Rails/ThreeStateBooleanColumn: + Exclude: + - 'db/migrate/20130412185253_add_new_fields_to_stories.rb' + - 'db/migrate/20130425211008_add_setup_complete_to_user.rb' + - 'db/migrate/20130513025939_add_keep_unread_to_stories.rb' + - 'db/migrate/20130513044029_add_is_starred_status_for_stories.rb' + - 'db/migrate/20230801025233_create_good_job_executions.rb' + +# Offense count: 6 +# This cop supports unsafe autocorrection (--autocorrect-all). +# Configuration parameters: EnforcedStyle. +# SupportedStyles: strict, flexible +Rails/TimeZone: + Exclude: + - 'app/commands/feed/find_new_stories.rb' + - 'app/repositories/story_repository.rb' + - 'app/tasks/remove_old_stories.rb' + - 'app/utils/sample_story.rb' + +# Offense count: 1 +# This cop supports safe autocorrection (--autocorrect). +Rails/Validation: + Exclude: + - 'app/models/story.rb' + +# Offense count: 23 +# This cop supports unsafe autocorrection (--autocorrect-all). +# Configuration parameters: EnforcedStyle. +# SupportedStyles: brackets, fetch +Style/HashLookupMethod: + Exclude: + - 'app/commands/story/mark_as_read.rb' + - 'app/commands/story/mark_as_starred.rb' + - 'app/commands/story/mark_as_unread.rb' + - 'app/commands/story/mark_as_unstarred.rb' + - 'app/controllers/application_controller.rb' + - 'app/controllers/feeds_controller.rb' + - 'app/controllers/stories_controller.rb' + - 'config/application.rb' + - 'exe/eslint_autogen' + - 'exe/stylelint_autogen' + - 'spec/repositories/feed_repository_spec.rb' + - 'spec/repositories/story_repository_spec.rb' + - 'spec/repositories/user_repository_spec.rb' + +# Offense count: 1 +# This cop supports safe autocorrection (--autocorrect). +Style/IfUnlessModifier: + Exclude: + - 'db/migrate/20240314031223_create_index_good_job_jobs_for_candidate_lookup.rb' + +# Offense count: 2 +# Configuration parameters: AllowedClasses. +Style/OneClassPerFile: + Exclude: + - 'config/application.rb' + - 'spec/support/factory_bot.rb' + +# Offense count: 1 +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: EnforcedStyle. +# SupportedStyles: return, return_nil +Style/ReturnNil: + Exclude: + - 'app/repositories/user_repository.rb' + +# Offense count: 4 +# This cop supports unsafe autocorrection (--autocorrect-all). +Style/StaticClass: + Exclude: + - 'app/repositories/feed_repository.rb' + - 'app/repositories/group_repository.rb' + - 'app/repositories/story_repository.rb' + - 'app/repositories/user_repository.rb' + +# Offense count: 1 +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: EnforcedStyle. +# SupportedStyles: single_quotes, double_quotes +Style/StringLiteralsInInterpolation: + Exclude: + - 'exe/stylelint_autogen' + +# Offense count: 1 +Style/TopLevelMethodDefinition: + Exclude: + - 'spec/integration/feed_importing_spec.rb' diff --git a/.ruby-version b/.ruby-version new file mode 100644 index 000000000..4d54daddb --- /dev/null +++ b/.ruby-version @@ -0,0 +1 @@ +4.0.2 diff --git a/.stringer.env b/.stringer.env new file mode 100644 index 000000000..12499378e --- /dev/null +++ b/.stringer.env @@ -0,0 +1,8 @@ +SECRET_KEY_BASE=5e1a0474b6c8b517c58a676bb9baae9da8fc82d4c5a13a42a1b69c3b310fe666ff824bfe9426db1c0814ea83fdacb1a8f80eed90ae3501006ea17440136620d9 +ENCRYPTION_PRIMARY_KEY=773dddc695536c2e7bcfe7e56f1bfc9ba29a793663304a6b6ef4764867fd7fdb93b9bbf52f3cf67b11b5bcb31d1da3583202e216e08d645363d453feef776a60 +ENCRYPTION_DETERMINISTIC_KEY=a827a6e936dec463b1635803bb19b96815b74e7aa871c656ac8bce45c070dbdf893300ff5e633ee5143efcf5915ee52c760d851e1bfb48f794ac12a40d433398 +ENCRYPTION_KEY_DERIVATION_SALT=63a7e1e618d35721a98d9d71628bda6d5e4b154f5357ad84d78b20b3be09416a204dce57ba1bf1946cfcad05cc990ffc6e8693d801ee4b184d6ba92d5831d68a + +DATABASE_URL=postgres://:@/ +FETCH_FEEDS_CRON='*/5 * * * *' +CLEANUP_CRON='0 0 * * *' diff --git a/.stylelint_todo.yml b/.stylelint_todo.yml new file mode 100644 index 000000000..e6959f1fd --- /dev/null +++ b/.stylelint_todo.yml @@ -0,0 +1,66 @@ +# This configuration was generated by `exe/stylelint_autogen` +# on 2026-02-19 04:45:04 UTC. +# The point is for the user to remove these configuration records +# one by one as the offenses are removed from the code base. + +overrides: + + # Offense count: 5 + - rules: { 'alpha-value-notation': null } + files: + - app/assets/stylesheets/application.css + + # Offense count: 5 + - rules: { 'color-function-alias-notation': null } + files: + - app/assets/stylesheets/application.css + + # Offense count: 5 + - rules: { 'color-function-notation': null } + files: + - app/assets/stylesheets/application.css + + # Offense count: 1 + - rules: { 'color-hex-length': null } + files: + - app/assets/stylesheets/application.css + + # Offense count: 2 + - rules: { 'comment-empty-line-before': null } + files: + - app/assets/stylesheets/application.css + + # Offense count: 3 + - rules: { 'font-family-name-quotes': null } + files: + - app/assets/stylesheets/application.css + + # Offense count: 15 + - rules: { 'length-zero-no-unit': null } + files: + - app/assets/stylesheets/application.css + + # Offense count: 3 + - rules: { 'media-feature-range-notation': null } + files: + - app/assets/stylesheets/application.css + + # Offense count: 21 + - rules: { 'no-descending-specificity': null } + files: + - app/assets/stylesheets/application.css + + # Offense count: 68 + - rules: { 'order/properties-order': null } + files: + - app/assets/stylesheets/application.css + + # Offense count: 19 + - rules: { 'property-no-vendor-prefix': null } + files: + - app/assets/stylesheets/application.css + + # Offense count: 2 + - rules: { 'selector-class-pattern': null } + files: + - app/assets/stylesheets/application.css diff --git a/.tool-versions b/.tool-versions new file mode 100644 index 000000000..4ed290d5b --- /dev/null +++ b/.tool-versions @@ -0,0 +1,4 @@ +ruby 4.0.2 +nodejs 25.8.1 +postgres 16.8 +pnpm 10.5.2 diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 61ce87bb1..000000000 --- a/.travis.yml +++ /dev/null @@ -1,17 +0,0 @@ -language: ruby -rvm: - - 2.0.0 - - 2.1.0 -before_install: - - gem update bundler - - sed -i '1d' Gemfile -before_script: - - npm install -g mocha-phantomjs@2.0.2 - - bundle exec rake test_js &> /dev/null & - - sleep 5 -script: - - bundle exec rspec - - mocha-phantomjs http://localhost:4567/test -matrix: - allow_failures: - - rvm: 2.1.0 diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 000000000..fa581a793 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,128 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, religion, or sexual identity +and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the + overall community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or + advances of any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email + address, without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +robert@boon.gl. +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series +of actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or +permanent ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within +the community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.0, available at +https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. + +Community Impact Guidelines were inspired by [Mozilla's code of conduct +enforcement ladder](https://github.com/mozilla/diversity). + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see the FAQ at +https://www.contributor-covenant.org/faq. Translations are available at +https://www.contributor-covenant.org/translations. diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 000000000..c6064d75c --- /dev/null +++ b/Dockerfile @@ -0,0 +1,52 @@ +FROM ruby:4.0.2 + +ENV RACK_ENV=production +ENV RAILS_ENV=production +ENV PORT=8080 +ENV BUNDLER_VERSION=2.6.2 + +EXPOSE 8080 + +SHELL ["/bin/bash", "-c"] + +WORKDIR /app +ADD Gemfile Gemfile.lock /app/ +RUN gem install bundler:$BUNDLER_VERSION && bundle install + +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + supervisor locales nodejs vim nano \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* + +RUN DEBIAN_FRONTEND=noninteractive dpkg-reconfigure locales \ + && locale-gen C.UTF-8 \ + && /usr/sbin/update-locale LANG=C.UTF-8 + +ENV LC_ALL=C.UTF-8 + +ARG TARGETARCH +ENV SUPERCRONIC_URL=https://github.com/aptible/supercronic/releases/download/v0.1.3/supercronic-linux-$TARGETARCH \ + SUPERCRONIC=supercronic-linux-$TARGETARCH \ + SUPERCRONIC_amd64_SHA1SUM=96960ba3207756bb01e6892c978264e5362e117e \ + SUPERCRONIC_arm_SHA1SUM=8c1e7af256ee35a9fcaf19c6a22aa59a8ccc03ef \ + SUPERCRONIC_arm64_SHA1SUM=f0e8049f3aa8e24ec43e76955a81b76e90c02270 \ + SUPERCRONIC_SHA1SUM="SUPERCRONIC_${TARGETARCH}_SHA1SUM" + +RUN curl -fsSLO "$SUPERCRONIC_URL" \ + && echo "${!SUPERCRONIC_SHA1SUM} ${SUPERCRONIC}" | sha1sum -c - \ + && chmod +x "$SUPERCRONIC" \ + && mv "$SUPERCRONIC" "/usr/local/bin/${SUPERCRONIC}" \ + && ln -s "/usr/local/bin/${SUPERCRONIC}" /usr/local/bin/supercronic + +ADD docker/supervisord.conf /etc/supervisord.conf +ADD docker/start.sh /app/ +ADD . /app + +RUN useradd -m stringer +RUN chown -R stringer:stringer /app +USER stringer + +ENV RAILS_SERVE_STATIC_FILES=true + +CMD /app/start.sh diff --git a/Gemfile b/Gemfile index 0b219450c..aa18bc9cd 100644 --- a/Gemfile +++ b/Gemfile @@ -1,51 +1,66 @@ -ruby "2.0.0" +# frozen_string_literal: true + +ruby_version_file = File.expand_path(".ruby-version", __dir__) +ruby File.read(ruby_version_file).chomp if File.readable?(ruby_version_file) source "https://rubygems.org" +git_source(:github) { |repo| "https://github.com/#{repo}.git" } -group :production do - gem "pg", "~> 0.17.1" - gem "unicorn", "~> 4.7" -end +gem "dotenv-rails" + +gem "rails", "~> 8.1.0" + +gem "bcrypt" +gem "bootsnap", require: false +gem "cssbundling-rails" +gem "feedbag" +gem "feedjira" +gem "goldiloader" +gem "good_job", "~> 4.13.0" +gem "httparty" +gem "jsbundling-rails" +gem "nokogiri", "~> 1.19.0" +gem "pg" +gem "propshaft" +gem "puma", "~> 7.0" +gem "rack-ssl" +gem "sass" +gem "stimulus-rails" +gem "stripe" +gem "strong_migrations" +gem "thread" +gem "turbo-rails" +gem "will_paginate" group :development do - gem "sqlite3", "~> 1.3", ">= 1.3.8" + gem "brakeman", require: false + gem "rubocop", require: false + gem "rubocop-capybara", require: false + gem "rubocop-factory_bot", require: false + gem "rubocop-rails", require: false + gem "rubocop-rake", require: false + gem "rubocop-rspec", require: false + gem "rubocop-rspec_rails", require: false + gem "web-console" end group :development, :test do - gem "coveralls", "~> 0.7", require: false - gem "faker", "~> 1.2" - gem "foreman", "~> 0.63.0" - gem "pry-byebug", "~> 1.2" - gem "rack-test", "~> 0.6.2" - gem "rspec", "~> 2.14", ">= 2.14.1" - gem "rspec-html-matchers", "~> 0.4.3" - gem "shotgun", "~> 0.9.0" + gem "bundler-audit", require: false + gem "capybara" + gem "coveralls_reborn", require: false + gem "debug" + gem "factory_bot" + gem "factory_bot_rails", require: false + gem "pry-byebug" + gem "rspec" + gem "rspec-rails" + gem "simplecov" + gem "webmock", require: false end -group :heroku do - gem "excon", "~> 0.31.0" - gem "formatador", "~> 0.2.4" - gem "netrc", "~> 0.7.7" - gem "rendezvous", "~> 0.0.2" +group :test do + gem "axe-core-rspec" + gem "rspec_junit_formatter" + gem "selenium-webdriver" + gem "webdrivers" + gem "with_model" end - -gem "activerecord", "~> 4.0" -# need to work around bug in 4.0.1 https://github.com/rails/arel/pull/216 -gem 'arel', git: 'git://github.com/rails/arel.git', branch: '4-0-stable' -gem "bcrypt-ruby", "~> 3.1.2" -gem "delayed_job", "~> 4.0" -gem "delayed_job_active_record", "~> 4.0" -gem "feedbag", "~> 0.9.2" -gem "feedzirra", "~> 0.6.0" -gem "highline", "~> 1.6", ">= 1.6.20", require: false -gem "i18n", "~> 0.6.9" -gem "loofah", github: "swanson/loofah" -gem "nokogiri", "~> 1.6" -gem "racksh", "~> 1.0" -gem "rake", "~> 10.1", ">= 10.1.1" -gem "sinatra", "~> 1.4", ">= 1.4.4" -gem "sinatra-assetpack", "~> 0.3.1", require: "sinatra/assetpack" -gem "sinatra-activerecord", "~> 1.2", ">= 1.2.3" -gem "sinatra-contrib", ">= 1.4.2" -gem "sinatra-flash", "~> 0.3.0" -gem "thread", "~> 0.1.3" -gem "will_paginate", "~> 3.0", ">= 3.0.5" diff --git a/Gemfile.lock b/Gemfile.lock index c8cc0411b..d58069d04 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,201 +1,516 @@ -GIT - remote: git://github.com/rails/arel.git - revision: 454a25f18c95cdfba5520a6fc5bdb6d476e20a85 - branch: 4-0-stable - specs: - arel (4.0.1.20131022201058) - -GIT - remote: git://github.com/swanson/loofah.git - revision: 825d715e6f1281501882d886cf34e82aebabb356 - specs: - loofah (1.2.1.20130718080038) - nokogiri (>= 1.5.9) - GEM remote: https://rubygems.org/ specs: - activemodel (4.0.2) - activesupport (= 4.0.2) - builder (~> 3.1.0) - activerecord (4.0.2) - activemodel (= 4.0.2) - activerecord-deprecated_finders (~> 1.0.2) - activesupport (= 4.0.2) - arel (~> 4.0.0) - activerecord-deprecated_finders (1.0.3) - activesupport (4.0.2) - i18n (~> 0.6, >= 0.6.4) - minitest (~> 4.2) - multi_json (~> 1.3) - thread_safe (~> 0.1) - tzinfo (~> 0.3.37) - atomic (1.1.14) - backports (3.3.5) - bcrypt-ruby (3.1.2) - builder (3.1.4) - byebug (2.5.0) - columnize (~> 0.3.6) - debugger-linecache (~> 1.2.0) - coderay (1.1.0) - columnize (0.3.6) - coveralls (0.7.0) - multi_json (~> 1.3) - rest-client - simplecov (>= 0.7) - term-ansicolor - thor - curb (0.8.5) - debugger-linecache (1.2.0) - delayed_job (4.0.0) - activesupport (>= 3.0, < 4.1) - delayed_job_active_record (4.0.0) - activerecord (>= 3.0, < 4.1) - delayed_job (>= 3.0, < 4.1) - diff-lcs (1.2.5) - docile (1.1.1) - dotenv (0.9.0) - excon (0.31.0) - faker (1.2.0) - i18n (~> 0.5) - feedbag (0.9.2) - hpricot (>= 0.6) - feedzirra (0.6.0) - curb (~> 0.8.1) - loofah (~> 1.2.1) - sax-machine (~> 0.2.1) - foreman (0.63.0) - dotenv (>= 0.7) - thor (>= 0.13.6) - formatador (0.2.4) - highline (1.6.20) - hpricot (0.8.6) - i18n (0.6.9) - jsmin (1.0.1) - kgio (2.8.1) - method_source (0.8.2) - mime-types (2.0) - mini_portile (0.5.2) - minitest (4.7.5) - multi_json (1.8.2) - netrc (0.7.7) - nokogiri (1.6.1) - mini_portile (~> 0.5.0) - pg (0.17.1) - pry (0.9.12.4) - coderay (~> 1.0) - method_source (~> 0.8) - slop (~> 3.4) - pry-byebug (1.2.0) - byebug (~> 2.2) - pry (~> 0.9.12) - rack (1.5.2) - rack-protection (1.5.1) + action_text-trix (2.1.17) + railties + actioncable (8.1.2.1) + actionpack (= 8.1.2.1) + activesupport (= 8.1.2.1) + nio4r (~> 2.0) + websocket-driver (>= 0.6.1) + zeitwerk (~> 2.6) + actionmailbox (8.1.2.1) + actionpack (= 8.1.2.1) + activejob (= 8.1.2.1) + activerecord (= 8.1.2.1) + activestorage (= 8.1.2.1) + activesupport (= 8.1.2.1) + mail (>= 2.8.0) + actionmailer (8.1.2.1) + actionpack (= 8.1.2.1) + actionview (= 8.1.2.1) + activejob (= 8.1.2.1) + activesupport (= 8.1.2.1) + mail (>= 2.8.0) + rails-dom-testing (~> 2.2) + actionpack (8.1.2.1) + actionview (= 8.1.2.1) + activesupport (= 8.1.2.1) + nokogiri (>= 1.8.5) + rack (>= 2.2.4) + rack-session (>= 1.0.1) + rack-test (>= 0.6.3) + rails-dom-testing (~> 2.2) + rails-html-sanitizer (~> 1.6) + useragent (~> 0.16) + actiontext (8.1.2.1) + action_text-trix (~> 2.1.15) + actionpack (= 8.1.2.1) + activerecord (= 8.1.2.1) + activestorage (= 8.1.2.1) + activesupport (= 8.1.2.1) + globalid (>= 0.6.0) + nokogiri (>= 1.8.5) + actionview (8.1.2.1) + activesupport (= 8.1.2.1) + builder (~> 3.1) + erubi (~> 1.11) + rails-dom-testing (~> 2.2) + rails-html-sanitizer (~> 1.6) + activejob (8.1.2.1) + activesupport (= 8.1.2.1) + globalid (>= 0.3.6) + activemodel (8.1.2.1) + activesupport (= 8.1.2.1) + activerecord (8.1.2.1) + activemodel (= 8.1.2.1) + activesupport (= 8.1.2.1) + timeout (>= 0.4.0) + activestorage (8.1.2.1) + actionpack (= 8.1.2.1) + activejob (= 8.1.2.1) + activerecord (= 8.1.2.1) + activesupport (= 8.1.2.1) + marcel (~> 1.0) + activesupport (8.1.2.1) + base64 + bigdecimal + concurrent-ruby (~> 1.0, >= 1.3.1) + connection_pool (>= 2.2.5) + drb + i18n (>= 1.6, < 2) + json + logger (>= 1.4.2) + minitest (>= 5.1) + securerandom (>= 0.3) + tzinfo (~> 2.0, >= 2.0.5) + uri (>= 0.13.1) + addressable (2.8.9) + public_suffix (>= 2.0.2, < 8.0) + ast (2.4.3) + axe-core-api (4.11.1) + dumb_delegator + ostruct + virtus + axe-core-rspec (4.11.1) + axe-core-api (= 4.11.1) + dumb_delegator + ostruct + virtus + axiom-types (0.1.1) + descendants_tracker (~> 0.0.4) + ice_nine (~> 0.11.0) + thread_safe (~> 0.3, >= 0.3.1) + base64 (0.3.0) + bcrypt (3.1.22) + bigdecimal (4.1.0) + bindex (0.8.1) + bootsnap (1.23.0) + msgpack (~> 1.2) + brakeman (8.0.4) + racc + builder (3.3.0) + bundler-audit (0.9.3) + bundler (>= 1.2.0) + thor (~> 1.0) + byebug (13.0.0) + reline (>= 0.6.0) + 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) + coderay (1.1.3) + coercible (1.0.0) + descendants_tracker (~> 0.0.1) + concurrent-ruby (1.3.6) + connection_pool (3.0.2) + coveralls_reborn (0.29.0) + simplecov (~> 0.22.0) + term-ansicolor (~> 1.7) + thor (~> 1.2) + tins (~> 1.32) + crack (1.0.1) + bigdecimal + rexml + crass (1.0.6) + cssbundling-rails (1.4.3) + railties (>= 6.0.0) + csv (3.3.5) + date (3.5.1) + debug (1.11.1) + irb (~> 1.10) + reline (>= 0.3.8) + descendants_tracker (0.0.4) + thread_safe (~> 0.3, >= 0.3.1) + diff-lcs (1.6.2) + docile (1.4.1) + dotenv (3.2.0) + dotenv-rails (3.2.0) + dotenv (= 3.2.0) + railties (>= 6.1) + drb (2.2.3) + dumb_delegator (1.1.0) + erb (6.0.2) + erubi (1.13.1) + et-orbi (1.4.0) + tzinfo + factory_bot (6.5.6) + activesupport (>= 6.1.0) + factory_bot_rails (6.5.1) + factory_bot (~> 6.5) + railties (>= 6.1.0) + feedbag (1.0.2) + addressable (~> 2.8) + nokogiri (~> 1.8, >= 1.8.2) + feedjira (4.0.1) + logger (>= 1.0, < 2) + loofah (>= 2.3.1, < 3) + sax-machine (>= 1.0, < 2) + ffi (1.17.3) + fugit (1.12.1) + et-orbi (~> 1.4) + raabro (~> 1.4) + globalid (1.3.0) + activesupport (>= 6.1) + goldiloader (6.0.0) + activerecord (>= 7.2, < 8.3) + activesupport (>= 7.2, < 8.3) + good_job (4.13.3) + activejob (>= 6.1.0) + activerecord (>= 6.1.0) + concurrent-ruby (>= 1.3.1) + fugit (>= 1.11.0) + railties (>= 6.1.0) + thor (>= 1.0.0) + hashdiff (1.2.1) + httparty (0.24.2) + csv + mini_mime (>= 1.0.0) + multi_xml (>= 0.5.2) + i18n (1.14.8) + concurrent-ruby (~> 1.0) + ice_nine (0.11.2) + io-console (0.8.2) + irb (1.17.0) + pp (>= 0.6.0) + prism (>= 1.3.0) + rdoc (>= 4.0.0) + reline (>= 0.4.2) + jsbundling-rails (1.3.1) + railties (>= 6.0.0) + json (2.19.2) + json-schema (6.2.0) + addressable (~> 2.8) + bigdecimal (>= 3.1, < 5) + language_server-protocol (3.17.0.5) + lint_roller (1.1.0) + logger (1.7.0) + loofah (2.25.1) + crass (~> 1.0.2) + nokogiri (>= 1.12.0) + mail (2.9.0) + logger + mini_mime (>= 0.1.1) + net-imap + net-pop + net-smtp + marcel (1.1.0) + matrix (0.4.3) + mcp (0.9.2) + json-schema (>= 4.1) + method_source (1.1.0) + mini_mime (1.1.5) + mini_portile2 (2.8.9) + minitest (6.0.2) + drb (~> 2.0) + prism (~> 1.5) + mize (0.6.1) + msgpack (1.8.0) + multi_xml (0.8.1) + bigdecimal (>= 3.1, < 5) + net-imap (0.6.3) + date + net-protocol + net-pop (0.1.2) + net-protocol + net-protocol (0.2.2) + timeout + net-smtp (0.5.1) + net-protocol + nio4r (2.7.5) + nokogiri (1.19.2) + mini_portile2 (~> 2.8.2) + racc (~> 1.4) + ostruct (0.6.3) + parallel (1.27.0) + parser (3.3.10.2) + ast (~> 2.4.1) + racc + pg (1.6.3) + pp (0.6.3) + prettyprint + prettyprint (0.2.0) + prism (1.9.0) + propshaft (1.3.1) + actionpack (>= 7.0.0) + activesupport (>= 7.0.0) rack - rack-test (0.6.2) - rack (>= 1.0) - racksh (1.0.0) - rack (>= 1.0) - rack-test (>= 0.5) - raindrops (0.12.0) - rake (10.1.1) - rendezvous (0.0.2) - rest-client (1.6.7) - mime-types (>= 1.16) - rspec (2.14.1) - rspec-core (~> 2.14.0) - rspec-expectations (~> 2.14.0) - rspec-mocks (~> 2.14.0) - rspec-core (2.14.7) - rspec-expectations (2.14.4) - diff-lcs (>= 1.1.3, < 2.0) - rspec-html-matchers (0.4.3) - nokogiri (>= 1.4.4) - rspec (>= 2.0.0) - rspec-mocks (2.14.4) - sax-machine (0.2.1) - nokogiri (~> 1.6.0) - shotgun (0.9) - rack (>= 1.0) - simplecov (0.8.2) - docile (~> 1.1.0) - multi_json - simplecov-html (~> 0.8.0) - simplecov-html (0.8.0) - sinatra (1.4.4) - rack (~> 1.4) - rack-protection (~> 1.4) - tilt (~> 1.3, >= 1.3.4) - sinatra-activerecord (1.2.3) - activerecord (>= 3.0) - sinatra (~> 1.0) - sinatra-assetpack (0.3.1) - jsmin - rack-test - sinatra - tilt (>= 1.3.0) - sinatra-contrib (1.4.2) - backports (>= 2.0) - multi_json - rack-protection - rack-test - sinatra (~> 1.4.0) - tilt (~> 1.3) - sinatra-flash (0.3.0) - sinatra (>= 1.0.0) - slop (3.4.7) - sqlite3 (1.3.8) - term-ansicolor (1.2.2) - tins (~> 0.8) - thor (0.18.1) - thread (0.1.3) - thread_safe (0.1.3) - atomic - tilt (1.4.1) - tins (0.13.1) - tzinfo (0.3.38) - unicorn (4.7.0) - kgio (~> 2.6) + pry (0.16.0) + coderay (~> 1.1) + method_source (~> 1.0) + reline (>= 0.6.0) + pry-byebug (3.12.0) + byebug (~> 13.0) + pry (>= 0.13, < 0.17) + psych (5.3.1) + date + stringio + public_suffix (7.0.5) + puma (7.2.0) + nio4r (~> 2.0) + raabro (1.4.0) + racc (1.8.1) + rack (3.2.5) + rack-session (2.1.1) + base64 (>= 0.1.0) + rack (>= 3.0.0) + rack-ssl (1.4.1) rack - raindrops (~> 0.7) - will_paginate (3.0.5) + rack-test (2.2.0) + rack (>= 1.3) + rackup (2.3.1) + rack (>= 3) + rails (8.1.2.1) + actioncable (= 8.1.2.1) + actionmailbox (= 8.1.2.1) + actionmailer (= 8.1.2.1) + actionpack (= 8.1.2.1) + actiontext (= 8.1.2.1) + actionview (= 8.1.2.1) + activejob (= 8.1.2.1) + activemodel (= 8.1.2.1) + activerecord (= 8.1.2.1) + activestorage (= 8.1.2.1) + activesupport (= 8.1.2.1) + bundler (>= 1.15.0) + railties (= 8.1.2.1) + rails-dom-testing (2.3.0) + activesupport (>= 5.0.0) + minitest + nokogiri (>= 1.6) + rails-html-sanitizer (1.7.0) + loofah (~> 2.25) + nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0) + railties (8.1.2.1) + actionpack (= 8.1.2.1) + activesupport (= 8.1.2.1) + irb (~> 1.13) + rackup (>= 1.0.0) + rake (>= 12.2) + thor (~> 1.0, >= 1.2.2) + tsort (>= 0.2) + zeitwerk (~> 2.6) + rainbow (3.1.1) + rake (13.3.1) + rb-fsevent (0.11.2) + rb-inotify (0.11.1) + ffi (~> 1.0) + rdoc (7.2.0) + erb + psych (>= 4.0.0) + tsort + readline (0.0.4) + reline + regexp_parser (2.11.3) + reline (0.6.3) + io-console (~> 0.5) + rexml (3.4.4) + rspec (3.13.2) + rspec-core (~> 3.13.0) + rspec-expectations (~> 3.13.0) + rspec-mocks (~> 3.13.0) + rspec-core (3.13.6) + rspec-support (~> 3.13.0) + rspec-expectations (3.13.5) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.13.0) + rspec-mocks (3.13.8) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.13.0) + rspec-rails (8.0.4) + actionpack (>= 7.2) + activesupport (>= 7.2) + railties (>= 7.2) + rspec-core (>= 3.13.0, < 5.0.0) + rspec-expectations (>= 3.13.0, < 5.0.0) + rspec-mocks (>= 3.13.0, < 5.0.0) + rspec-support (>= 3.13.0, < 5.0.0) + rspec-support (3.13.7) + rspec_junit_formatter (0.6.0) + rspec-core (>= 2, < 4, != 2.12.0) + rubocop (1.85.1) + json (~> 2.3) + language_server-protocol (~> 3.17.0.2) + lint_roller (~> 1.1.0) + mcp (~> 0.6) + parallel (~> 1.10) + parser (>= 3.3.0.2) + rainbow (>= 2.2.2, < 4.0) + regexp_parser (>= 2.9.3, < 3.0) + rubocop-ast (>= 1.49.0, < 2.0) + ruby-progressbar (~> 1.7) + unicode-display_width (>= 2.4.0, < 4.0) + rubocop-ast (1.49.1) + parser (>= 3.3.7.2) + prism (~> 1.7) + rubocop-capybara (2.22.1) + lint_roller (~> 1.1) + rubocop (~> 1.72, >= 1.72.1) + rubocop-factory_bot (2.28.0) + lint_roller (~> 1.1) + rubocop (~> 1.72, >= 1.72.1) + rubocop-rails (2.34.3) + activesupport (>= 4.2.0) + lint_roller (~> 1.1) + rack (>= 1.1) + rubocop (>= 1.75.0, < 2.0) + rubocop-ast (>= 1.44.0, < 2.0) + rubocop-rake (0.7.1) + lint_roller (~> 1.1) + rubocop (>= 1.72.1) + rubocop-rspec (3.9.0) + lint_roller (~> 1.1) + rubocop (~> 1.81) + rubocop-rspec_rails (2.32.0) + lint_roller (~> 1.1) + rubocop (~> 1.72, >= 1.72.1) + rubocop-rspec (~> 3.5) + ruby-progressbar (1.13.0) + rubyzip (2.4.1) + sass (3.7.4) + sass-listen (~> 4.0.0) + sass-listen (4.0.0) + rb-fsevent (~> 0.9, >= 0.9.4) + rb-inotify (~> 0.9, >= 0.9.7) + sax-machine (1.3.2) + securerandom (0.4.1) + selenium-webdriver (4.10.0) + rexml (~> 3.2, >= 3.2.5) + rubyzip (>= 1.2.2, < 3.0) + websocket (~> 1.0) + simplecov (0.22.0) + docile (~> 1.1) + simplecov-html (~> 0.11) + simplecov_json_formatter (~> 0.1) + simplecov-html (0.13.2) + simplecov_json_formatter (0.1.4) + stimulus-rails (1.3.4) + railties (>= 6.0.0) + stringio (3.2.0) + stripe (18.4.2) + strong_migrations (2.5.2) + activerecord (>= 7.1) + sync (0.5.0) + term-ansicolor (1.11.3) + tins (~> 1) + thor (1.5.0) + thread (0.2.2) + thread_safe (0.3.6) + timeout (0.6.1) + tins (1.52.0) + bigdecimal + mize (~> 0.6) + readline + sync + tsort (0.2.0) + turbo-rails (2.0.23) + actionpack (>= 7.1.0) + railties (>= 7.1.0) + tzinfo (2.0.6) + concurrent-ruby (~> 1.0) + unicode-display_width (3.2.0) + unicode-emoji (~> 4.1) + unicode-emoji (4.2.0) + uri (1.1.1) + useragent (0.16.11) + virtus (2.0.0) + axiom-types (~> 0.1) + coercible (~> 1.0) + descendants_tracker (~> 0.0, >= 0.0.3) + web-console (4.3.0) + actionview (>= 8.0.0) + bindex (>= 0.4.0) + railties (>= 8.0.0) + webdrivers (5.3.1) + nokogiri (~> 1.6) + rubyzip (>= 1.3.0) + selenium-webdriver (~> 4.0, < 4.11) + webmock (3.26.2) + addressable (>= 2.8.0) + crack (>= 0.3.2) + hashdiff (>= 0.4.0, < 2.0.0) + websocket (1.2.11) + websocket-driver (0.8.0) + base64 + websocket-extensions (>= 0.1.0) + websocket-extensions (0.1.5) + will_paginate (4.0.1) + with_model (2.2.0) + activerecord (>= 7.0) + xpath (3.2.0) + nokogiri (~> 1.8) + zeitwerk (2.7.5) PLATFORMS ruby DEPENDENCIES - activerecord (~> 4.0) - arel! - bcrypt-ruby (~> 3.1.2) - coveralls (~> 0.7) - delayed_job (~> 4.0) - delayed_job_active_record (~> 4.0) - excon (~> 0.31.0) - faker (~> 1.2) - feedbag (~> 0.9.2) - feedzirra (~> 0.6.0) - foreman (~> 0.63.0) - formatador (~> 0.2.4) - highline (~> 1.6, >= 1.6.20) - i18n (~> 0.6.9) - loofah! - netrc (~> 0.7.7) - nokogiri (~> 1.6) - pg (~> 0.17.1) - pry-byebug (~> 1.2) - rack-test (~> 0.6.2) - racksh (~> 1.0) - rake (~> 10.1, >= 10.1.1) - rendezvous (~> 0.0.2) - rspec (~> 2.14, >= 2.14.1) - rspec-html-matchers (~> 0.4.3) - shotgun (~> 0.9.0) - sinatra (~> 1.4, >= 1.4.4) - sinatra-activerecord (~> 1.2, >= 1.2.3) - sinatra-assetpack (~> 0.3.1) - sinatra-contrib (>= 1.4.2) - sinatra-flash (~> 0.3.0) - sqlite3 (~> 1.3, >= 1.3.8) - thread (~> 0.1.3) - unicorn (~> 4.7) - will_paginate (~> 3.0, >= 3.0.5) + axe-core-rspec + bcrypt + bootsnap + brakeman + bundler-audit + capybara + coveralls_reborn + cssbundling-rails + debug + dotenv-rails + factory_bot + factory_bot_rails + feedbag + feedjira + goldiloader + good_job (~> 4.13.0) + httparty + jsbundling-rails + nokogiri (~> 1.19.0) + pg + propshaft + pry-byebug + puma (~> 7.0) + rack-ssl + rails (~> 8.1.0) + rspec + rspec-rails + rspec_junit_formatter + rubocop + rubocop-capybara + rubocop-factory_bot + rubocop-rails + rubocop-rake + rubocop-rspec + rubocop-rspec_rails + sass + selenium-webdriver + simplecov + stimulus-rails + stripe + strong_migrations + thread + turbo-rails + web-console + webdrivers + webmock + will_paginate + with_model + +RUBY VERSION + ruby 4.0.2 + +BUNDLED WITH + 2.6.2 diff --git a/Procfile b/Procfile index dfa2ae6e9..819eb20a1 100644 --- a/Procfile +++ b/Procfile @@ -1,2 +1 @@ -web: bundle exec unicorn -p $PORT -c ./config/unicorn.rb -console: bundle exec racksh +web: bundle exec puma -C ./config/puma.rb diff --git a/Procfile.dev b/Procfile.dev new file mode 100644 index 000000000..bd4b65e62 --- /dev/null +++ b/Procfile.dev @@ -0,0 +1,3 @@ +web: PORT=3000 bundle exec puma -C config/puma.rb +js: pnpm build --watch +css: pnpm build:css --watch diff --git a/README.md b/README.md index 71d658858..37a04b641 100644 --- a/README.md +++ b/README.md @@ -1,76 +1,48 @@ -##Stringer -[![Build Status](https://travis-ci.org/swanson/stringer.png)](https://travis-ci.org/swanson/stringer) -[![Code Climate](https://codeclimate.com/github/swanson/stringer.png)](https://codeclimate.com/github/swanson/stringer) -[![Coverage Status](https://coveralls.io/repos/swanson/stringer/badge.png?branch=master)](https://coveralls.io/r/swanson/stringer) +# Stringer -### A [work-in-progress] self-hosted, anti-social RSS reader. +[![CircleCI](https://circleci.com/gh/stringer-rss/stringer/tree/main.svg?style=svg)](https://circleci.com/gh/stringer-rss/stringer/tree/main) +[![Code Climate](https://api.codeclimate.com/v1/badges/899c5407c870e541af4e/maintainability)](https://codeclimate.com/github/stringer-rss/stringer/maintainability) +[![Coverage Status](https://coveralls.io/repos/github/stringer-rss/stringer/badge.svg?branch=main)](https://coveralls.io/github/stringer-rss/stringer?branch=main) +[![GitHub Sponsors](https://img.shields.io/github/sponsors/mockdeep?logo=github)](https://github.com/sponsors/mockdeep) -Stringer has no external dependencies, no social recommendations/sharing, and no fancy machine learning algorithms. +### A self-hosted, anti-social RSS reader. -But it does have keyboard shortcuts and was made with love! +Stringer has no external dependencies, no social recommendations/sharing, and no fancy machine learning algorithms. -When `BIG_FREE_READER` shuts down, your instance of Stringer will still be kicking. +But it does have keyboard shortcuts and was made with love! ![](screenshots/instructions.png) ![](screenshots/stories.png) ![](screenshots/feed.png) -# Installation - -Stringer is a Ruby (2.0.0+) app based on Sinatra, ActiveRecord, PostgreSQL, Backbone.js and DelayedJob. - -Instructions are provided for deploying to Heroku (runs fine on the free plan) but Stringer can be deployed anywhere that supports Ruby (setup instructions for a Linux-based VPS are provided [here](/docs/VPS.md), and for OpenShift, provided [here](/docs/OpenShift.md)). +## Installation -```sh -git clone git://github.com/swanson/stringer.git -cd stringer -heroku create -git push heroku master - -heroku config:set APP_URL=`heroku apps:info | grep -o 'http[^"]*'` -heroku config:set SECRET_TOKEN=`openssl rand -hex 20` +Stringer is a Ruby app based on Rails, PostgreSQL, Backbone.js and GoodJob. -heroku run rake db:migrate -heroku restart - -heroku addons:add scheduler -heroku addons:open scheduler -``` +[![Deploy to Heroku](https://cdn.herokuapp.com/deploy/button.svg)](https://heroku.com/deploy?template=https://github.com/stringer-rss/stringer) -Add an hourly task that runs `rake lazy_fetch` (if you are not on Heroku you may want `rake fetch_feeds` instead). +Stringer will run just fine on the Eco/Basic Heroku plans. -Load the app and follow the instructions to import your feeds and start using the app. - ---- - -In the event that you need to change your password, run `heroku run rake change_password` from the app folder. - -## Updating the app - -From the app's directory: - -```sh -git pull -git push heroku master -heroku run rake db:migrate -heroku restart -``` +Instructions are provided for deploying to [Heroku manually](/docs/Heroku.md), to any Ruby +compatible [Linux-based VPS](/docs/VPS.md), to [Docker](docs/Docker.md) and to [OpenShift](/docs/OpenShift.md). -# Niceties +## Niceties -Keyboard Shortcuts +### Keyboard Shortcuts You can access the keyboard shortcuts when using the app by hitting `?`. ![](screenshots/keyboard_shortcuts.png) ---- +### Using your own domain with Heroku You can run Stringer at `http://reader.yourdomain.com` using a CNAME. If you are on Heroku: -`heroku domains:add reader.yourdomain.com` +``` +heroku domains:add reader.yourdomain.com +``` Go to your registrar and add a CNAME: ``` @@ -81,7 +53,7 @@ Target: your-heroku-instance.herokuapp.com Wait a few minutes for changes to propagate. ---- +### Fever API Stringer implements a clone of [Fever's API](http://www.feedafever.com/api) so it can be used with any mobile client that supports Fever. @@ -96,9 +68,7 @@ Email: stringer (case-sensitive) Password: {your-stringer-password} ``` -If you have previously setup Stringer, you will need to migrate your database and run `rake change_password` for the API key to be setup properly. - ---- +### Translations Stringer has been translated to [several other languages](config/locales). Your language can be set with the `LOCALE` environment variable. @@ -106,46 +76,57 @@ To set your locale on Heroku, run `heroku config:set LOCALE=en`. If you would like to translate Stringer to your preferred language, please use [LocaleApp](http://www.localeapp.com/projects/4637). ---- - -Clean up old read stories +### Clean up old read stories on Heroku -If you are on the Heroku free plan, there is a 10k row limit so you will -eventually run out of space. - -You can clean up old stories by running: - -`rake cleanup_old_stories` +You can clean up old stories by running: `rake cleanup_old_stories` By default, this removes read stories that are more than 30 days old (that are not starred). You can either run this manually or add it as a scheduled task. -# Development +## Development -Run the Ruby tests with `rspec`. +Run the Ruby tests with `rspec`. Run the Javascript tests with `rake test_js` and then open a browser to `http://localhost:4567/test`. -## Getting Started +### Getting Started + +To get started using Stringer for development you first need to install `foreman`. + + gem install foreman -To get started using Stringer for development simply run the following: +Then run the following commands. ```sh bundle install -rake db:migrate +rails db:setup foreman start ``` -The application will be running on port `5000` +The application will be running on port `5000`. -You can launch an interactive console (ala `rails c`) using `racksh` +You can launch an interactive console (a la `rails c`) using `rake console`. -# Acknowledgements -Most of the heavy-lifting is done by [`feedzirra`](https://github.com/pauldix/feedzirra) and [`feedbag`](https://github.com/dwillis/feedbag). +## Acknowledgments + +Most of the heavy-lifting is done by [`feedjira`](https://github.com/feedjira/feedjira) and [`feedbag`](https://github.com/dwillis/feedbag). General sexiness courtesy of [`Twitter Bootstrap`](http://twitter.github.io/bootstrap/) and [`Flat UI`](http://designmodo.github.io/Flat-UI/). -# Contact -Matt Swanson, [mdswanson.com](http://mdswanson.com) [@_swanson](http://twitter.com/_swanson) +ReenieBeanie Font Copyright © 2010 Typeco (james@typeco.com). Licensed under [SIL Open Font License, 1.1](http://scripts.sil.org/OFL). + +Lato Font Copyright © 2010-2011 by tyPoland Lukasz Dziedzic (team@latofonts.com). Licensed under [SIL Open Font License, 1.1](http://scripts.sil.org/OFL). + +## Contact + +If you have a question, feature idea, or are running into problems, our preferred method of contact is to open an issue on GitHub. This allows multiple people to weigh in, and we can keep everything in one place. Thanks! + +## Maintainers + +Robert Fletcher [boon.gl](https://boon.gl) + +## Alumni +Matt Swanson (creator), [mdswanson.com](http://mdswanson.com), [@_swanson](http://twitter.com/_swanson) +Victor Koronen, [victor.koronen.se](http://victor.koronen.se/), [@victorkoronen](https://twitter.com/victorkoronen) diff --git a/Rakefile b/Rakefile index d2983e70d..5fc8d463b 100644 --- a/Rakefile +++ b/Rakefile @@ -1,215 +1,35 @@ -require "bundler" -Bundler.setup +# frozen_string_literal: true -require "rubygems" -require "net/http" -require 'active_record' -require 'delayed_job' -require 'delayed_job_active_record' +require_relative "config/application" -require "sinatra/activerecord/rake" - -require "./app" -require_relative "./app/jobs/fetch_feed_job" -require_relative "./app/tasks/fetch_feeds" -require_relative "./app/tasks/change_password" -require_relative "./app/tasks/remove_old_stories.rb" +Rails.application.load_tasks desc "Fetch all feeds." -task :fetch_feeds do - FetchFeeds.new(Feed.all).fetch_all +task fetch_feeds: :environment do + Feed::FetchAll.call end desc "Lazily fetch all feeds." -task :lazy_fetch do - if ENV['APP_URL'] - uri = URI(ENV['APP_URL']) +task lazy_fetch: :environment do + if ENV["APP_URL"] + uri = URI(ENV["APP_URL"]) + + # warm up server by fetching the root path Net::HTTP.get_response(uri) end FeedRepository.list.each do |feed| - Delayed::Job.enqueue FetchFeedJob.new(feed.id) + CallableJob.perform_later(Feed::FetchOne, feed) end end desc "Fetch single feed" -task :fetch_feed, :id do |t, args| - FetchFeed.new(Feed.find(args[:id])).fetch -end - -desc "Clear the delayed_job queue." -task :clear_jobs do - Delayed::Job.delete_all -end - -desc "Work the delayed_job queue." -task :work_jobs do - Delayed::Job.delete_all - - 3.times do - Delayed::Worker.new(:min_priority => ENV['MIN_PRIORITY'], - :max_priority => ENV['MAX_PRIORITY']).start - end -end - -desc "Change your password" -task :change_password do - ChangePassword.new.change_password +task :fetch_feed, [:id] => :environment do |_t, args| + Feed::FetchOne.call(Feed.find(args[:id])) end desc "Clean up old stories that are read and unstarred" -task :cleanup_old_stories, :number_of_days do |t, args| - args.with_defaults(:number_of_days => 30) - RemoveOldStories.remove!(args[:number_of_days].to_i) -end - -desc "Start server and serve JavaScript test suite at /test" -task :test_js do - require_relative "./spec/javascript/test_controller" - Stringer.run! -end - -begin - require 'rspec/core/rake_task' - - RSpec::Core::RakeTask.new(:speedy_tests) do |t| - t.rspec_opts = "--tag ~speed:slow" - end - - RSpec::Core::RakeTask.new(:spec) - - task :default => [:speedy_tests] -rescue LoadError - # allow for bundle install --without development:test -end - -desc "deploy stringer on Heroku" -task :deploy do - - require 'excon' - require 'formatador' - require 'json' - require 'netrc' - require 'rendezvous' - require 'securerandom' - - Formatador.display_line("[negative]<> deploying stringer to Heroku[/]") - - # grab netrc credentials, set by toolbelt via `heroku login` - Formatador.display_line("[negative]<> reading your global Heroku credentials from ~/.netrc (set when you ran heroku login)...[/]") - _, password = Netrc.read['api.heroku.com'] - - # setup excon for API calls - heroku = Excon.new( - 'https://api.heroku.com', - :headers => { - "Accept" => "application/vnd.heroku+json; version=3", - "Authorization" => "Basic #{[':' << password].pack('m').delete("\r\n")}", - "Content-Type" => "application/json" - } - ) - - #heroku create - Formatador.display_line("[negative]<> creating app[/]") - app_data = JSON.parse(heroku.post(:path => "/apps").body) - - #git push heroku master - Formatador.display_line("[negative]<> pushing code to [underline]#{app_data['name']}[/]") - `git push git@heroku.com:#{app_data['name']}.git master` - - heroku.reset # reset socket as git push may take long enough for timeout - - #heroku config:set SECRET_TOKEN=`openssl rand -hex 20` - Formatador.display_line("[negative]<> setting SECRET_TOKEN on [underline]#{app_data['name']}[/]") - heroku.patch( - :body => { "SECRET_TOKEN" => SecureRandom.hex(20) }.to_json, - :path => "/apps/#{app_data['id']}/config-vars" - ) - - #heroku run rake db:migrate - Formatador.display_line("[negative]<> running `rake db:migrate` on [underline]#{app_data['name']}[/]") - run_data = JSON.parse(heroku.post( - :body => { - "attach" => true, - "command" => "rake db:migrate" - }.to_json, - :path => "/apps/#{app_data['id']}/dynos" - ).body) - Rendezvous.start( - :url => run_data['attach_url'] - ) - - heroku.reset # reset socket as db:migrate may take long enough for timeout - - #heroku restart - Formatador.display_line("[negative]<> restarting [underline]#{app_data['name']}[/]") - heroku.delete(:path => "/apps/#{app_data['id']}/dynos") - - #heroku addons:add scheduler - Formatador.display_line("[negative]<> adding scheduler:standard to [underline]#{app_data['name']}[/]") - heroku.post( - :body => { "plan" => { "name" => "scheduler:standard" } }.to_json, - :path => "/apps/#{app_data['id']}/addons" - ) - - #heroku addons:open scheduler - Formatador.display_lines([ - "[negative]<> Add `[bold]rake lazy_fetch[/][negative]` hourly task at [underline]https://api.heroku.com/apps/#{app_data['id']}/addons/scheduler:standard[/]", - "[negative]<> Impatient? After adding feeds, immediately fetch the latest with `heroku run rake fetch_feeds -a #{app_data['name']}`", - "[negative]<> stringer available at [underline]#{app_data['web_url']}[/]" - ]) -end - -desc "update stringer on heroku" -task :update, :app do |task, args| - - require 'excon' - require 'formatador' - require 'json' - require 'netrc' - require 'rendezvous' - - unless args.app - Formatador.display_line("[negative]! Error: App required, please run as `bundle exec rake update[app]`[/]") - exit - end - - Formatador.display_line("[negative]<> updating Heroku stringer on [underline]#{args.app}[/]") - - # grab netrc credentials, set by toolbelt via `heroku login` - Formatador.display_line("[negative]<> reading your global Heroku credentials from ~/.netrc (set when you ran heroku login)...[/]") - _, password = Netrc.read['api.heroku.com'] - - # setup excon for API calls - heroku = Excon.new( - 'https://api.heroku.com', - :headers => { - "Accept" => "application/vnd.heroku+json; version=3", - "Authorization" => "Basic #{[':' << password].pack('m').delete("\r\n")}", - "Content-Type" => "application/json" - } - ) - - #git push heroku master - Formatador.display_line("[negative]<> pushing code to [underline]#{args.app}[/]") - `git push git@heroku.com:#{args.app}.git master` - - #heroku run rake db:migrate - Formatador.display_line("[negative]<> running `rake db:migrate` on [underline]#{args.app}[/]") - run_data = JSON.parse(heroku.post( - :body => { - "attach" => true, - "command" => "rake db:migrate" - }.to_json, - :path => "/apps/#{args.app}/dynos" - ).body) - Rendezvous.start( - :url => run_data['attach_url'] - ) - - heroku.reset # reset socket as db:migrate may take long enough for timeout - - #heroku restart - Formatador.display_line("[negative]<> restarting [underline]#{args.app}[/]") - heroku.delete(:path => "/apps/#{args.app}/dynos") +task :cleanup_old_stories, [:number_of_days] => :environment do |_t, args| + args.with_defaults(number_of_days: 30) + RemoveOldStories.call(args[:number_of_days].to_i) end diff --git a/app.json b/app.json new file mode 100644 index 000000000..a82ae22ba --- /dev/null +++ b/app.json @@ -0,0 +1,52 @@ +{ + "name": "Stringer", + "description": "A self-hosted, anti-social RSS reader.", + "logo": "https://raw.githubusercontent.com/stringer-rss/stringer/main/screenshots/logo.png", + "keywords": [ + "RSS", + "Ruby" + ], + "website": "https://github.com/stringer-rss/stringer", + "success_url": "/heroku", + "scripts": { + "postdeploy": "bundle exec rake db:migrate" + }, + "env": { + "SECRET_KEY_BASE": { + "description": "Secret key used by rails for encryption", + "generator": "secret" + }, + "ENCRYPTION_PRIMARY_KEY": { + "description": "Secret key used by rails for encryption", + "generator": "secret" + }, + "ENCRYPTION_DETERMINISTIC_KEY": { + "description": "Secret key used by rails for encryption", + "generator": "secret" + }, + "ENCRYPTION_KEY_DERIVATION_SALT": { + "description": "Secret key used by rails for encryption", + "generator": "secret" + }, + "LOCALE": { + "description": "Specify the translation locale you wish to use", + "value": "en" + }, + "ENFORCE_SSL": { + "description": "Force all clients to connect over SSL", + "value": "true" + }, + "WORKER_EMBEDDED": { + "description": "Force worker threads to be spawned by main process", + "value": "true" + }, + "WORKER_RETRY": { + "description": "Number of times to respawn the worker thread if it fails", + "value": "3" + } + }, + "addons": [ + "heroku-postgresql:hobby-dev", + "scheduler:standard" + ] +} diff --git a/app.rb b/app.rb deleted file mode 100644 index f68db4565..000000000 --- a/app.rb +++ /dev/null @@ -1,106 +0,0 @@ -require "sinatra/base" -require "sinatra/activerecord" -require "sinatra/flash" -require "sinatra/contrib/all" -require "sinatra/assetpack" -require "json" -require "i18n" -require "will_paginate" -require "will_paginate/active_record" - -require_relative "app/helpers/authentication_helpers" -require_relative "app/repositories/user_repository" - -I18n.load_path += Dir[File.join(File.dirname(__FILE__), 'config/locales', '*.yml').to_s] -I18n.config.enforce_available_locales=false - -class Stringer < Sinatra::Base - register Sinatra::ActiveRecordExtension - register Sinatra::Flash - register Sinatra::Contrib - register Sinatra::AssetPack - - configure do - set :database_file, "config/database.yml" - set :views, "app/views" - set :public_dir, "app/public" - set :root, File.dirname(__FILE__) - - enable :sessions - set :session_secret, ENV["SECRET_TOKEN"] || "secret!" - enable :logging - - ActiveRecord::Base.include_root_in_json = false - end - - helpers do - include Sinatra::AuthenticationHelpers - - def render_partial(name, locals = {}) - erb "partials/_#{name}".to_sym, layout: false, locals: locals - end - - def render_js_template(name) - erb "js/templates/_#{name}.js".to_sym, layout: false - end - - def render_js(name, locals = {}) - erb "js/#{name}.js".to_sym, layout: false, locals: locals - end - - def t(*args) - I18n.t(*args) - end - end - - assets { - serve "/js", from: "app/public/js" - serve "/css", from: "app/public/css" - serve "/images", from: "app/public/img" - - js :application, "/js/application.js", [ - "/js/jquery-min.js", - "/js/bootstrap-min.js", - "/js/bootstrap.file-input.js", - "/js/mousetrap-min.js", - "/js/jquery-visible-min.js", - "/js/underscore-min.js", - "/js/backbone-min.js", - "/js/app.js" - ] - - css :application, "/css/application.css", [ - "/css/bootstrap-min.css", - "/css/flat-ui-no-icons.css", - "/css/font-awesome-min.css", - "/css/styles.css" - ] - - js_compression :jsmin - css_compression :simple - - prebuild true unless ENV['RACK_ENV'] == 'test' - } - - before do - I18n.locale = ENV["LOCALE"].blank? ? :en : ENV["LOCALE"].to_sym - - if !is_authenticated? && needs_authentication?(request.path) - redirect '/login' - end - end - - get "/" do - if UserRepository.setup_complete? - redirect to("/news") - else - redirect to("/setup/password") - end - end -end - -require_relative "app/controllers/stories_controller" -require_relative "app/controllers/first_run_controller" -require_relative "app/controllers/sessions_controller" -require_relative "app/controllers/feeds_controller" -require_relative "app/controllers/debug_controller" diff --git a/app/assets/builds/.keep b/app/assets/builds/.keep new file mode 100644 index 000000000..e69de29bb diff --git a/app/assets/images/.keep b/app/assets/images/.keep new file mode 100644 index 000000000..e69de29bb diff --git a/app/public/css/styles.css b/app/assets/stylesheets/application.css similarity index 77% rename from app/public/css/styles.css rename to app/assets/stylesheets/application.css index 4e69b86db..0939a389c 100644 --- a/app/public/css/styles.css +++ b/app/assets/stylesheets/application.css @@ -1,3 +1,5 @@ +@import "./custom.css"; + html, body { height: 100%; color: #484948; @@ -6,6 +8,43 @@ html, body { body { background-color: #FAF2E5; overflow-x: hidden; + font: 14px/1.231 "Lato", sans-serif; +} + +a { + color: #1abc9c; + text-decoration: underline; +} + +a:hover { + color: #2ecc71; + text-decoration: none; +} + +textarea, +input[type="text"], +input[type="password"], +input[type="email"], +input[type="url"], +input[type="search"], +input[type="tel"] { + border: 2px solid #dce4ec; + font-family: "Lato", sans-serif; + font-size: 14px; + padding: 8px 0 9px 10px; + border-radius: 6px; + box-shadow: none; +} + +textarea:focus, +input[type="text"]:focus, +input[type="password"]:focus, +input[type="email"]:focus, +input[type="url"]:focus, +input[type="search"]:focus, +input[type="tel"]:focus { + border-color: #484948; + box-shadow: none; } hr { @@ -15,12 +54,24 @@ hr { margin: 20px auto; } +code { + white-space: normal; +} + +.warning { + background-color: #F2DEDE; +} + .container { width: 100%; max-width: 720px; margin: 0 auto; } +.hidden { + display: none !important; +} + /* Wrapper for page content to push down footer */ #wrap { min-height: 100%; @@ -39,6 +90,23 @@ hr { border-top: 7px solid #484948; } +@media (max-width: 768px) { + #footer .row > * { + display: none; + } + + #footer .row > .col-md-8:first-child { + display: block; + width: 100%; + text-align: center; + } + + #frame { + padding-top: 7px; + border-top: none; + } +} + .alert { text-align: center; -webkit-border-radius: 0px; @@ -49,11 +117,19 @@ hr { #action-bar .btn { padding: 8px; - height: 18px; - width: 18px; + height: 34px; + width: 34px; + line-height: 18px; +} + +#action-bar button.btn { + height: 34px; + width: 34px; } .btn { + border: none; + font-size: 16.5px; position: relative; top: 0px; background-color: #484948; @@ -96,6 +172,13 @@ hr { margin-bottom: 14px; } +@media (max-width: 768px) { + #action-bar { + margin-left: 8px; + margin-right: 8px; + } +} + ul#story-list, ul#feed-list { list-style-type: none; margin-left: 0px; @@ -132,6 +215,10 @@ li.story.read { opacity: 0.5; } +li.story.keepUnread .story-preview { + font-weight: bold; +} + li.story.open { opacity: 1.0; } @@ -160,14 +247,18 @@ li.story.open .story-preview { } .story-lead { - color: #e5e5e5; + color: #c5c5c5; } .story-published { margin-left: 20px; } -.story-keep-unread, .story-starred { +.story-enclosure { + float: right; +} + +.story-keep-unread, .story-starred { display: inline-block; cursor: pointer; -webkit-touch-callout: none; @@ -186,11 +277,11 @@ li.story.open .story-preview { margin-right: 13px; } -li.story .icon-star { +li.story .fa-star { color: #F67100; } -li.story .icon-star-empty { +li.story .fa-star-o { color: #e5e5e5; } @@ -201,6 +292,11 @@ li.story .icon-star-empty { text-align: left; } +.story-body img { + max-width: 100%; + height: auto; +} + .story-body h1, .story-body h2, .story-body h3 { font-family: "Lato", sans-serif; font-weight: 700; @@ -226,7 +322,7 @@ li.story.cursor { border: 3px solid #484948; } -li.story .story-body-container { +li.story .story-body-container { display: none; } @@ -235,7 +331,7 @@ li.story.open .story-body-container { } p.story-details { - margin-right: 5px; + margin-right: 14px; overflow: hidden; } @@ -303,8 +399,16 @@ p.story-details { color: #7F8281; } +nav { + display: flex; + justify-content: space-between; + align-items: center; +} + li.feed .feed-line { cursor: default; + display: flex; + justify-content: space-between; } li.feed .feed-title { @@ -353,13 +457,33 @@ li.feed .feed-last-updated { text-align: right; } +@media (max-width: 768px) { + li.feed .feed-last-updated { + display: none; + } + + li.feed .row .col-md-2 { + float: right; + margin-right: 21px; + } +} + li.feed .last-updated { font-size: 10px; } -li.feed .last-updated-time { - width: 100px; - display: inline-block; +.feed-actions { + display: flex; + gap: 10px; + justify-content: space-between; +} + +li.feed .edit-feed { + cursor: pointer; +} + +li.feed .edit-feed a { + color: #000; } li.feed .remove-feed { @@ -368,9 +492,6 @@ li.feed .remove-feed { li.feed .remove-feed a { text-align: center; - padding-left: 3px; - padding-right: 3px; - margin-left: 10px; color: #C0392B; } @@ -408,8 +529,12 @@ li.feed .remove-feed a:hover { padding-top: 10px; } +.setup__label { + font-weight: bold; +} + .setup { - width: 350px; + width: 500px; margin: 0 auto; padding-top: 100px; } @@ -428,8 +553,8 @@ li.feed .remove-feed a:hover { text-align: center; } -.setup .control-group input { - width: 210px; +.setup .form-group input { + width: 350px; } .setup .field-icon { @@ -445,11 +570,11 @@ li.feed .remove-feed a:hover { transition: 0.25s; } -.setup .control-group input:focus + .field-icon { +.setup .form-group input:focus + .field-icon { color: #484948; } -.setup .control-group input:focus ~ .field-label { +.setup .form-group input:focus ~ .field-label { color: #484948; } @@ -465,12 +590,20 @@ li.feed .remove-feed a:hover { transition: 0.25s; } -.setup #password, .setup #password-confirmation, .setup #feed-url { +.setup .form-control { padding-left: 100px; padding-right: 36px; } -.setup .control-group { +.setup select { + display: block; + position: absolute; + left: 100px; + top: 5px; + width: 244px; +} + +.setup .form-group { position: relative; } @@ -550,6 +683,11 @@ kbd { white-space: nowrap; } + +#shortcuts .modal-body { + max-height: 500px; +} + ul.shortcut-legend li { margin-bottom: 10px; } diff --git a/app/assets/stylesheets/custom.css b/app/assets/stylesheets/custom.css new file mode 100644 index 000000000..03b222d39 --- /dev/null +++ b/app/assets/stylesheets/custom.css @@ -0,0 +1,6 @@ +/* custom CSS goes here */ +@import "bootstrap/dist/css/bootstrap.min.css"; +@import "font-awesome/css/font-awesome.min.css"; +@import "@fontsource/lato/latin.css"; +@import "@fontsource/lato/latin-400-italic.css"; +@import "@fontsource/reenie-beanie/latin.css"; diff --git a/app/assets/stylesheets/font-awesome-fonts.css b/app/assets/stylesheets/font-awesome-fonts.css new file mode 100644 index 000000000..60274ae61 --- /dev/null +++ b/app/assets/stylesheets/font-awesome-fonts.css @@ -0,0 +1,11 @@ +@font-face { + font-family: FontAwesome; + font-style: normal; + font-weight: normal; + src: url('fontawesome-webfont.eot'); + src: url('fontawesome-webfont.eot?#iefix') format('embedded-opentype'), + url('fontawesome-webfont.woff2') format('woff2'), + url('fontawesome-webfont.woff') format('woff'), + url('fontawesome-webfont.ttf') format('truetype'), + url('fontawesome-webfont.svg#fontawesomeregular') format('svg'); +} diff --git a/app/assets/stylesheets/lato-fonts.css b/app/assets/stylesheets/lato-fonts.css new file mode 100644 index 000000000..e6b0c27ce --- /dev/null +++ b/app/assets/stylesheets/lato-fonts.css @@ -0,0 +1,35 @@ +@font-face { + font-family: 'Lato'; + font-style: normal; + font-weight: 300; + src: url('lato-latin-300-normal.woff2') format('woff2'), + url('lato-latin-300-normal.woff') format('woff'); +} +@font-face { + font-family: 'Lato'; + font-style: normal; + font-weight: 400; + src: url('lato-latin-400-normal.woff2') format('woff2'), + url('lato-latin-400-normal.woff') format('woff'); +} +@font-face { + font-family: 'Lato'; + font-style: normal; + font-weight: 700; + src: url('lato-latin-700-normal.woff2') format('woff2'), + url('lato-latin-700-normal.woff') format('woff'); +} +@font-face { + font-family: 'Lato'; + font-style: normal; + font-weight: 900; + src: url('lato-latin-900-normal.woff2') format('woff2'), + url('lato-latin-900-normal.woff') format('woff'); +} +@font-face { + font-family: 'Lato'; + font-style: italic; + font-weight: 400; + src: url('lato-latin-400-italic.woff2') format('woff2'), + url('lato-latin-400-italic.woff') format('woff'); +} diff --git a/app/assets/stylesheets/reenie-beanie-font.css b/app/assets/stylesheets/reenie-beanie-font.css new file mode 100644 index 000000000..bd2ed6bac --- /dev/null +++ b/app/assets/stylesheets/reenie-beanie-font.css @@ -0,0 +1,7 @@ +@font-face { + font-family: 'Reenie Beanie'; + font-style: normal; + font-weight: 400; + src: url('reenie-beanie-latin-400-normal.woff2') format('woff2'), + url('reenie-beanie-latin-400-normal.woff') format('woff'); +} diff --git a/app/commands/cast_boolean.rb b/app/commands/cast_boolean.rb new file mode 100644 index 000000000..7c0c59ae5 --- /dev/null +++ b/app/commands/cast_boolean.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +module CastBoolean + TRUE_VALUES = Set.new(["true", true, "1"]).freeze + FALSE_VALUES = Set.new(["false", false, "0"]).freeze + + def self.call(boolean) + if (TRUE_VALUES + FALSE_VALUES).exclude?(boolean) + raise(ArgumentError, "cannot cast to boolean: #{boolean.inspect}") + end + + TRUE_VALUES.include?(boolean) + end +end diff --git a/app/commands/feed/create.rb b/app/commands/feed/create.rb new file mode 100644 index 000000000..ca4cc50dd --- /dev/null +++ b/app/commands/feed/create.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +module Feed::Create + def self.call(url, user:) + result = FeedDiscovery.call(url) + return false unless result + + name = ContentSanitizer.call(result.title.presence || result.feed_url) + + Feed.create(name:, user:, url: result.feed_url, last_fetched: 1.day.ago) + end +end diff --git a/app/commands/feed/export_to_opml.rb b/app/commands/feed/export_to_opml.rb new file mode 100644 index 000000000..bc913d0e1 --- /dev/null +++ b/app/commands/feed/export_to_opml.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +module Feed::ExportToOpml + class << self + def call(feeds) + builder = + Nokogiri::XML::Builder.new do |xml| + xml.opml(version: "1.0") do + xml.head { xml.title("Feeds from Stringer") } + xml.body { feeds.each { |feed| feed_outline(xml, feed) } } + end + end + + builder.to_xml + end + + private + + def feed_outline(xml, feed) + xml.outline( + text: feed.name, + title: feed.name, + type: "rss", + xmlUrl: feed.url + ) + end + end +end diff --git a/app/commands/feed/fetch_all.rb b/app/commands/feed/fetch_all.rb new file mode 100644 index 000000000..c1f140cef --- /dev/null +++ b/app/commands/feed/fetch_all.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +require "thread/pool" + +module Feed::FetchAll + def self.call + pool = Thread.pool(10) + + Feed.find_each { |feed| pool.process { Feed::FetchOne.call(feed) } } + + pool.shutdown + end +end diff --git a/app/commands/feed/fetch_all_for_user.rb b/app/commands/feed/fetch_all_for_user.rb new file mode 100644 index 000000000..65bd42427 --- /dev/null +++ b/app/commands/feed/fetch_all_for_user.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +require "thread/pool" + +module Feed::FetchAllForUser + def self.call(user) + pool = Thread.pool(10) + + user.feeds.find_each { |feed| pool.process { Feed::FetchOne.call(feed) } } + + pool.shutdown + end +end diff --git a/app/commands/feed/fetch_one.rb b/app/commands/feed/fetch_one.rb new file mode 100644 index 000000000..6d71b5f49 --- /dev/null +++ b/app/commands/feed/fetch_one.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +module Feed::FetchOne + class << self + def call(feed) + raw_feed = fetch_raw_feed(feed) + + new_entries_from(feed, raw_feed).each do |entry| + StoryRepository.add(entry, feed) + end + + FeedRepository.update_last_fetched(feed, raw_feed.last_modified) + + FeedRepository.set_status(:green, feed) + rescue StandardError => e + FeedRepository.set_status(:red, feed) + + Rails.logger.error("Something went wrong when parsing #{feed.url}: #{e}") + end + + private + + def fetch_raw_feed(feed) + response = HTTParty.get(feed.url).to_s + Feedjira.parse(response) + end + + def new_entries_from(feed, raw_feed) + Feed::FindNewStories.call(raw_feed, feed.id, latest_entry_id(feed)) + end + + def latest_entry_id(feed) + feed.stories.first.entry_id unless feed.stories.empty? + end + end +end diff --git a/app/commands/feed/find_new_stories.rb b/app/commands/feed/find_new_stories.rb new file mode 100644 index 000000000..e46ea9ca1 --- /dev/null +++ b/app/commands/feed/find_new_stories.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +module Feed::FindNewStories + STORY_AGE_THRESHOLD_DAYS = 3 + + def self.call(raw_feed, feed_id, latest_entry_id = nil) + stories = [] + + raw_feed.entries.each do |story| + break if latest_entry_id && story.id == latest_entry_id + next if story_age_exceeds_threshold?(story) || StoryRepository.exists?( + story.id, + feed_id + ) + + stories << story + end + + stories + end + + def self.story_age_exceeds_threshold?(story) + max_age = Time.now - STORY_AGE_THRESHOLD_DAYS.days + story.published && story.published < max_age + end +end diff --git a/app/commands/feed/import_from_opml.rb b/app/commands/feed/import_from_opml.rb new file mode 100644 index 000000000..16d23faf3 --- /dev/null +++ b/app/commands/feed/import_from_opml.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true + +module Feed::ImportFromOpml + class << self + def call(opml_contents, user:) + feeds_with_groups = OpmlParser.call(opml_contents) + + # It considers a situation when feeds are already imported without + # groups, so it's possible to re-import the same subscriptions.xml just + # to set group_id for existing feeds. Feeds without groups are in + # 'Ungrouped' group, we don't create such group and create such feeds + # with group_id = nil. + feeds_with_groups.each do |group_name, parsed_feeds| + group = find_or_create_group(group_name, user) + + parsed_feeds.each do |parsed_feed| + create_feed(parsed_feed, group, user) + end + end + end + + private + + def find_or_create_group(group_name, user) + return if group_name == "Ungrouped" + + user.groups.create_or_find_by(name: group_name) + end + + def create_feed(parsed_feed, group, user) + feed = user.feeds.where(**parsed_feed.slice(:name, :url)) + .first_or_initialize + find_feed_name(feed, parsed_feed) + feed.last_fetched = 1.day.ago if feed.new_record? + feed.group_id = group.id if group + feed.save + end + + def find_feed_name(feed, parsed_feed) + return if feed.name? + + result = FeedDiscovery.call(parsed_feed[:url]) + title = result.title if result + feed.name = ContentSanitizer.call(title.presence || parsed_feed[:url]) + end + end +end diff --git a/app/commands/feeds/add_new_feed.rb b/app/commands/feeds/add_new_feed.rb deleted file mode 100644 index f6e9a2ba3..000000000 --- a/app/commands/feeds/add_new_feed.rb +++ /dev/null @@ -1,15 +0,0 @@ -require_relative "../../models/feed" -require_relative "../../utils/feed_discovery" - -class AddNewFeed - ONE_DAY = 24 * 60 * 60 - - def self.add(url, discoverer = FeedDiscovery.new, repo = Feed) - result = discoverer.discover(url) - return false unless result - - repo.create(name: result.title, - url: result.feed_url, - last_fetched: Time.now - ONE_DAY) - end -end \ No newline at end of file diff --git a/app/commands/feeds/export_to_opml.rb b/app/commands/feeds/export_to_opml.rb deleted file mode 100644 index 8822b057d..000000000 --- a/app/commands/feeds/export_to_opml.rb +++ /dev/null @@ -1,29 +0,0 @@ -require "nokogiri" - -class ExportToOpml - def initialize(feeds) - @feeds = feeds - end - - def to_xml - builder = Nokogiri::XML::Builder.new do |xml| - xml.opml(version: "1.0") do - xml.head { - xml.title "Feeds from Stringer" - } - xml.body { - @feeds.each do |feed| - xml.outline( - text: feed.name, - title: feed.name, - type: "rss", - xmlUrl: feed.url - ) - end - } - end - end - - builder.to_xml - end -end \ No newline at end of file diff --git a/app/commands/feeds/find_new_stories.rb b/app/commands/feeds/find_new_stories.rb deleted file mode 100644 index 8db937080..000000000 --- a/app/commands/feeds/find_new_stories.rb +++ /dev/null @@ -1,22 +0,0 @@ -class FindNewStories - def initialize(raw_feed, last_fetched, latest_entry_id = nil) - @raw_feed = raw_feed - @last_fetched = last_fetched - @latest_entry_id = latest_entry_id - end - - def new_stories - return [] if @raw_feed.last_modified && - @raw_feed.last_modified < @last_fetched - - stories = [] - @raw_feed.entries.each do |story| - break if @latest_entry_id && story.id == @latest_entry_id - - stories << story unless story.published && - story.published < @last_fetched - end - - stories - end -end diff --git a/app/commands/feeds/import_from_opml.rb b/app/commands/feeds/import_from_opml.rb deleted file mode 100644 index 120a1d5d5..000000000 --- a/app/commands/feeds/import_from_opml.rb +++ /dev/null @@ -1,16 +0,0 @@ -require_relative "../../models/feed" -require_relative "../../utils/opml_parser" - -class ImportFromOpml - ONE_DAY = 24 * 60 * 60 - - def self.import(opml_contents) - feeds = OpmlParser.new.parse_feeds(opml_contents) - - feeds.each do |feed| - Feed.create(name: feed[:name], - url: feed[:url], - last_fetched: Time.now - ONE_DAY) - end - end -end \ No newline at end of file diff --git a/app/commands/fever_api.rb b/app/commands/fever_api.rb new file mode 100644 index 000000000..bcb975d3f --- /dev/null +++ b/app/commands/fever_api.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +module FeverAPI + API_VERSION = 3 + + PARAMS = [ + :as, + :before, + :favicons, + :feeds, + :groups, + :id, + :items, + :links, + :mark, + :saved_item_ids, + :since_id, + :unread_item_ids, + :with_ids + ].freeze +end diff --git a/app/commands/fever_api/authentication.rb b/app/commands/fever_api/authentication.rb new file mode 100644 index 000000000..1ff89b28f --- /dev/null +++ b/app/commands/fever_api/authentication.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +module FeverAPI::Authentication + def self.call(authorization:, **_params) + feeds = authorization.scope(Feed) + last_refreshed_on_time = (feeds.maximum(:last_fetched) || 0).to_i + + { auth: 1, last_refreshed_on_time: } + end +end diff --git a/app/commands/fever_api/read_favicons.rb b/app/commands/fever_api/read_favicons.rb new file mode 100644 index 000000000..2227d9376 --- /dev/null +++ b/app/commands/fever_api/read_favicons.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +class FeverAPI::ReadFavicons + ICON = "R0lGODlhAQABAIAAAObm5gAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==" + + class << self + def call(params) + if params.key?(:favicons) + { favicons: } + else + {} + end + end + + private + + def favicons + [ + { + id: 0, + data: "image/gif;base64,#{ICON}" + } + ] + end + end +end diff --git a/app/commands/fever_api/read_feeds.rb b/app/commands/fever_api/read_feeds.rb new file mode 100644 index 000000000..2cf674999 --- /dev/null +++ b/app/commands/fever_api/read_feeds.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +module FeverAPI::ReadFeeds + class << self + def call(authorization:, **params) + if params.key?(:feeds) + { feeds: feeds(authorization) } + else + {} + end + end + + private + + def feeds(authorization) + authorization.scope(FeedRepository.list).map(&:as_fever_json) + end + end +end diff --git a/app/commands/fever_api/read_feeds_groups.rb b/app/commands/fever_api/read_feeds_groups.rb new file mode 100644 index 000000000..c751ac9aa --- /dev/null +++ b/app/commands/fever_api/read_feeds_groups.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +class FeverAPI::ReadFeedsGroups + class << self + def call(authorization:, **params) + if params.key?(:feeds) || params.key?(:groups) + { feeds_groups: feeds_groups(authorization) } + else + {} + end + end + + private + + def feeds_groups(authorization) + scoped_feeds = authorization.scope(FeedRepository.list) + grouped_feeds = scoped_feeds.order("LOWER(name)").group_by(&:group_id) + grouped_feeds.map do |group_id, feeds| + group_id ||= Group::UNGROUPED.id + + { + group_id:, + feed_ids: feeds.map(&:id).join(",") + } + end + end + end +end diff --git a/app/commands/fever_api/read_groups.rb b/app/commands/fever_api/read_groups.rb new file mode 100644 index 000000000..8fe316f10 --- /dev/null +++ b/app/commands/fever_api/read_groups.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +module FeverAPI::ReadGroups + class << self + def call(authorization:, **params) + if params.key?(:groups) + { groups: groups(authorization) } + else + {} + end + end + + private + + def groups(authorization) + [Group::UNGROUPED, *authorization.scope(GroupRepository.list)] + .map(&:as_fever_json) + end + end +end diff --git a/app/commands/fever_api/read_items.rb b/app/commands/fever_api/read_items.rb new file mode 100644 index 000000000..eca1d15c7 --- /dev/null +++ b/app/commands/fever_api/read_items.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +module FeverAPI::ReadItems + class << self + def call(authorization:, **params) + return {} unless params.key?(:items) + + item_ids = params[:with_ids].split(",") if params.key?(:with_ids) + + { + items: items(item_ids, params[:since_id], authorization), + total_items: total_items(item_ids, authorization) + } + end + + private + + def items(item_ids, since_id, authorization) + items = + if item_ids + stories_by_ids(item_ids, authorization) + else + unread_stories(since_id, authorization) + end + items.order(:published, :id).map(&:as_fever_json) + end + + def total_items(item_ids, authorization) + items = + if item_ids + stories_by_ids(item_ids, authorization) + else + unread_stories(nil, authorization) + end + items.count + end + + def stories_by_ids(ids, authorization) + authorization.scope(StoryRepository.fetch_by_ids(ids)) + end + + def unread_stories(since_id, authorization) + if since_id.present? + authorization.scope(StoryRepository.unread_since_id(since_id)) + else + authorization.scope(StoryRepository.unread) + end + end + end +end diff --git a/app/commands/fever_api/read_links.rb b/app/commands/fever_api/read_links.rb new file mode 100644 index 000000000..db5c0fbaf --- /dev/null +++ b/app/commands/fever_api/read_links.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module FeverAPI::ReadLinks + def self.call(params) + if params.key?(:links) + { links: [] } + else + {} + end + end +end diff --git a/app/commands/fever_api/response.rb b/app/commands/fever_api/response.rb new file mode 100644 index 000000000..55c7e699e --- /dev/null +++ b/app/commands/fever_api/response.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +module FeverAPI::Response + ACTIONS = [ + FeverAPI::Authentication, + FeverAPI::ReadFeeds, + FeverAPI::ReadGroups, + FeverAPI::ReadFeedsGroups, + FeverAPI::ReadFavicons, + FeverAPI::ReadItems, + FeverAPI::ReadLinks, + FeverAPI::SyncUnreadItemIds, + FeverAPI::SyncSavedItemIds, + FeverAPI::WriteMarkItem, + FeverAPI::WriteMarkFeed, + FeverAPI::WriteMarkGroup + ].freeze + + def self.call(params) + result = { api_version: FeverAPI::API_VERSION } + ACTIONS.each { |action| result.merge!(action.call(**params)) } + result.to_json + end +end diff --git a/app/commands/fever_api/sync_saved_item_ids.rb b/app/commands/fever_api/sync_saved_item_ids.rb new file mode 100644 index 000000000..0a0a160a7 --- /dev/null +++ b/app/commands/fever_api/sync_saved_item_ids.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module FeverAPI::SyncSavedItemIds + class << self + def call(authorization:, **params) + if params.key?(:saved_item_ids) + { saved_item_ids: saved_item_ids(authorization) } + else + {} + end + end + + private + + def saved_item_ids(authorization) + all_starred_stories(authorization).map(&:id).join(",") + end + + def all_starred_stories(authorization) + authorization.scope(StoryRepository.all_starred) + end + end +end diff --git a/app/commands/fever_api/sync_unread_item_ids.rb b/app/commands/fever_api/sync_unread_item_ids.rb new file mode 100644 index 000000000..9f6351d2e --- /dev/null +++ b/app/commands/fever_api/sync_unread_item_ids.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module FeverAPI::SyncUnreadItemIds + class << self + def call(authorization:, **params) + if params.key?(:unread_item_ids) + { unread_item_ids: unread_item_ids(authorization) } + else + {} + end + end + + private + + def unread_item_ids(authorization) + authorization.scope(unread_stories).map(&:id).join(",") + end + + def unread_stories + StoryRepository.unread + end + end +end diff --git a/app/commands/fever_api/write_mark_feed.rb b/app/commands/fever_api/write_mark_feed.rb new file mode 100644 index 000000000..d01147078 --- /dev/null +++ b/app/commands/fever_api/write_mark_feed.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +module FeverAPI::WriteMarkFeed + def self.call(authorization:, **params) + if params[:mark] == "feed" + authorization.check(Feed.find(params[:id])) + MarkFeedAsRead.call(params[:id], params[:before]) + end + + {} + end +end diff --git a/app/commands/fever_api/write_mark_group.rb b/app/commands/fever_api/write_mark_group.rb new file mode 100644 index 000000000..bf285a820 --- /dev/null +++ b/app/commands/fever_api/write_mark_group.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +module FeverAPI::WriteMarkGroup + def self.call(authorization:, **params) + if params[:mark] == "group" + authorization.check(Group.find(params[:id])) + MarkGroupAsRead.call(params[:id], params[:before]) + end + + {} + end +end diff --git a/app/commands/fever_api/write_mark_item.rb b/app/commands/fever_api/write_mark_item.rb new file mode 100644 index 000000000..9550ace4b --- /dev/null +++ b/app/commands/fever_api/write_mark_item.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +module FeverAPI::WriteMarkItem + class << self + def call(authorization:, **params) + if params[:mark] == "item" + authorization.check(Story.find(params[:id])) if params.key?(:id) + mark_item_as(params[:id], params[:as]) + end + + {} + end + + private + + def mark_item_as(id, mark_as) + case mark_as + when "read" + MarkAsRead.call(id) + when "unread" + MarkAsUnread.call(id) + when "saved" + MarkAsStarred.call(id) + when "unsaved" + MarkAsUnstarred.call(id) + end + end + end +end diff --git a/app/commands/stories/mark_all_as_read.rb b/app/commands/stories/mark_all_as_read.rb deleted file mode 100644 index 4e0500472..000000000 --- a/app/commands/stories/mark_all_as_read.rb +++ /dev/null @@ -1,13 +0,0 @@ -require_relative "../../repositories/story_repository" - -class MarkAllAsRead - def initialize(story_ids, repository = StoryRepository) - @story_ids = story_ids - @repo = repository - end - - def mark_as_read - @repo.fetch_by_ids(@story_ids).update_all(is_read: true) - end -end - diff --git a/app/commands/stories/mark_as_read.rb b/app/commands/stories/mark_as_read.rb deleted file mode 100644 index ea42df246..000000000 --- a/app/commands/stories/mark_as_read.rb +++ /dev/null @@ -1,13 +0,0 @@ -require_relative "../../repositories/story_repository" - -class MarkAsRead - def initialize(story_id, repository = StoryRepository) - @story_id = story_id - @repo = repository - end - - def mark_as_read - @repo.fetch(@story_id).update_attributes(is_read: true) - end -end - diff --git a/app/commands/stories/mark_as_starred.rb b/app/commands/stories/mark_as_starred.rb deleted file mode 100644 index 1449372e1..000000000 --- a/app/commands/stories/mark_as_starred.rb +++ /dev/null @@ -1,13 +0,0 @@ -require_relative "../../repositories/story_repository" - -class MarkAsStarred - def initialize(story_id, repository = StoryRepository) - @story_id = story_id - @repo = repository - end - - def mark_as_starred - @repo.fetch(@story_id).update_attributes(is_starred: true) - end -end - diff --git a/app/commands/stories/mark_as_unread.rb b/app/commands/stories/mark_as_unread.rb deleted file mode 100644 index 3eb26cc24..000000000 --- a/app/commands/stories/mark_as_unread.rb +++ /dev/null @@ -1,14 +0,0 @@ -require_relative "../../repositories/story_repository" - -class MarkAsUnread - def initialize(story_id, repository = StoryRepository) - @story_id = story_id - @repo = repository - end - - def mark_as_unread - @repo.fetch(@story_id).update_attributes(is_read: false) - end -end - - diff --git a/app/commands/stories/mark_as_unstarred.rb b/app/commands/stories/mark_as_unstarred.rb deleted file mode 100644 index 797f864f3..000000000 --- a/app/commands/stories/mark_as_unstarred.rb +++ /dev/null @@ -1,14 +0,0 @@ -require_relative "../../repositories/story_repository" - -class MarkAsUnstarred - def initialize(story_id, repository = StoryRepository) - @story_id = story_id - @repo = repository - end - - def mark_as_unstarred - @repo.fetch(@story_id).update_attributes(is_starred: false) - end -end - - diff --git a/app/commands/stories/mark_feed_as_read.rb b/app/commands/stories/mark_feed_as_read.rb deleted file mode 100644 index bf664fb7a..000000000 --- a/app/commands/stories/mark_feed_as_read.rb +++ /dev/null @@ -1,14 +0,0 @@ -require_relative "../../repositories/story_repository" - -class MarkFeedAsRead - def initialize(feed_id, timestamp, repository = StoryRepository) - @feed_id = feed_id.to_i - @repo = repository - @timestamp = timestamp - end - - def mark_feed_as_read - @repo.fetch_unread_for_feed_by_timestamp(@feed_id, @timestamp).update_all(is_read: true) - end -end - diff --git a/app/commands/stories/mark_group_as_read.rb b/app/commands/stories/mark_group_as_read.rb deleted file mode 100644 index 3c379edd5..000000000 --- a/app/commands/stories/mark_group_as_read.rb +++ /dev/null @@ -1,19 +0,0 @@ -require_relative "../../repositories/story_repository" - -class MarkGroupAsRead - KINDLING_GROUP_ID = 1 - SPARKS_AND_KINDLING_GROUP_ID = 0 - - def initialize(group_id, timestamp, repository = StoryRepository) - @group_id = group_id.to_i - @repo = repository - @timestamp = timestamp - end - - def mark_group_as_read - if [SPARKS_AND_KINDLING_GROUP_ID, KINDLING_GROUP_ID].include? @group_id - @repo.fetch_unread_by_timestamp(@timestamp).update_all(is_read: true) - end - end -end - diff --git a/app/commands/story/mark_all_as_read.rb b/app/commands/story/mark_all_as_read.rb new file mode 100644 index 000000000..c20e49518 --- /dev/null +++ b/app/commands/story/mark_all_as_read.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +module MarkAllAsRead + def self.call(story_ids) + StoryRepository.fetch_by_ids(story_ids).update_all(is_read: true) + end +end diff --git a/app/commands/story/mark_as_read.rb b/app/commands/story/mark_as_read.rb new file mode 100644 index 000000000..27f7ab751 --- /dev/null +++ b/app/commands/story/mark_as_read.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +module MarkAsRead + def self.call(story_id) + StoryRepository.fetch(story_id).update!(is_read: true) + end +end diff --git a/app/commands/story/mark_as_starred.rb b/app/commands/story/mark_as_starred.rb new file mode 100644 index 000000000..aae2198a5 --- /dev/null +++ b/app/commands/story/mark_as_starred.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +module MarkAsStarred + def self.call(story_id) + StoryRepository.fetch(story_id).update!(is_starred: true) + end +end diff --git a/app/commands/story/mark_as_unread.rb b/app/commands/story/mark_as_unread.rb new file mode 100644 index 000000000..cf72798a9 --- /dev/null +++ b/app/commands/story/mark_as_unread.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +module MarkAsUnread + def self.call(story_id) + StoryRepository.fetch(story_id).update!(is_read: false) + end +end diff --git a/app/commands/story/mark_as_unstarred.rb b/app/commands/story/mark_as_unstarred.rb new file mode 100644 index 000000000..144abc19c --- /dev/null +++ b/app/commands/story/mark_as_unstarred.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +module MarkAsUnstarred + def self.call(story_id) + StoryRepository.fetch(story_id).update!(is_starred: false) + end +end diff --git a/app/commands/story/mark_feed_as_read.rb b/app/commands/story/mark_feed_as_read.rb new file mode 100644 index 000000000..de3afa936 --- /dev/null +++ b/app/commands/story/mark_feed_as_read.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +module MarkFeedAsRead + def self.call(feed_id, timestamp) + StoryRepository + .fetch_unread_for_feed_by_timestamp(feed_id, timestamp) + .update_all(is_read: true) + end +end diff --git a/app/commands/story/mark_group_as_read.rb b/app/commands/story/mark_group_as_read.rb new file mode 100644 index 000000000..9104c6238 --- /dev/null +++ b/app/commands/story/mark_group_as_read.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +module MarkGroupAsRead + KINDLING_GROUP_ID = 0 + SPARKS_GROUP_ID = -1 + + def self.call(group_id, timestamp) + return unless group_id + + if [KINDLING_GROUP_ID, SPARKS_GROUP_ID].include?(group_id.to_i) + StoryRepository + .fetch_unread_by_timestamp(timestamp).update_all(is_read: true) + else + StoryRepository + .fetch_unread_by_timestamp_and_group(timestamp, group_id) + .update_all(is_read: true) + end + end +end diff --git a/app/commands/user/sign_in_user.rb b/app/commands/user/sign_in_user.rb new file mode 100644 index 000000000..021ec10b6 --- /dev/null +++ b/app/commands/user/sign_in_user.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +module SignInUser + def self.call(username, submitted_password) + user = User.find_by(username:) + return unless user + + user_password = BCrypt::Password.new(user.password_digest) + + user if user_password == submitted_password + end +end diff --git a/app/commands/users/change_user_password.rb b/app/commands/users/change_user_password.rb deleted file mode 100644 index 72ce8cce2..000000000 --- a/app/commands/users/change_user_password.rb +++ /dev/null @@ -1,17 +0,0 @@ -require_relative "../../repositories/user_repository" -require_relative "../../utils/api_key" - -class ChangeUserPassword - def initialize(repository = UserRepository) - @repo = repository - end - - def change_user_password(new_password) - user = @repo.first - user.password = new_password - user.api_key = ApiKey.compute(new_password) - - @repo.save(user) - user - end -end diff --git a/app/commands/users/complete_setup.rb b/app/commands/users/complete_setup.rb deleted file mode 100644 index f0518a11e..000000000 --- a/app/commands/users/complete_setup.rb +++ /dev/null @@ -1,7 +0,0 @@ -class CompleteSetup - def self.complete(user) - user.setup_complete = true - user.save - user - end -end \ No newline at end of file diff --git a/app/commands/users/create_user.rb b/app/commands/users/create_user.rb deleted file mode 100644 index 456eddd91..000000000 --- a/app/commands/users/create_user.rb +++ /dev/null @@ -1,15 +0,0 @@ -require_relative "../../utils/api_key" - -class CreateUser - def initialize(repository = User) - @repo = repository - end - - def create(password) - @repo.delete_all - @repo.create(password: password, - password_confirmation: password, - setup_complete: false, - api_key: ApiKey.compute(password)) - end -end \ No newline at end of file diff --git a/app/commands/users/sign_in_user.rb b/app/commands/users/sign_in_user.rb deleted file mode 100644 index 71536bb2a..000000000 --- a/app/commands/users/sign_in_user.rb +++ /dev/null @@ -1,14 +0,0 @@ -require_relative "../../models/user" - -class SignInUser - def self.sign_in(submitted_password, repository = User) - user = repository.first - user_password = BCrypt::Password.new(user.password_digest) - - if user_password == submitted_password - user - else - nil - end - end -end \ No newline at end of file diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb new file mode 100644 index 000000000..2520b577b --- /dev/null +++ b/app/controllers/application_controller.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +class ApplicationController < ActionController::Base + before_action :complete_setup + before_action :authenticate_user + after_action -> { authorization.verify } + + private + + def authorization + @authorization ||= Authorization.new(current_user) + end + + def complete_setup + redirect_to("/setup/password") unless UserRepository.setup_complete? + end + + def authenticate_user + return if current_user + + session[:redirect_to] = request.fullpath + redirect_to("/login") + end + + def current_user + @current_user ||= UserRepository.fetch(session[:user_id]) + end + helper_method :current_user +end diff --git a/app/controllers/concerns/.keep b/app/controllers/concerns/.keep new file mode 100644 index 000000000..e69de29bb diff --git a/app/controllers/debug_controller.rb b/app/controllers/debug_controller.rb index fec72216e..bc87c11bb 100644 --- a/app/controllers/debug_controller.rb +++ b/app/controllers/debug_controller.rb @@ -1,10 +1,20 @@ -require_relative '../models/migration_status' +# frozen_string_literal: true -class Stringer < Sinatra::Base - get "/debug" do - erb :"debug", locals: { - queued_jobs_count: Delayed::Job.count, - pending_migrations: MigrationStatus.new.pending_migrations - } +class DebugController < ApplicationController + skip_before_action :complete_setup, only: [:heroku] + skip_before_action :authenticate_user, only: [:heroku] + + def index + authorization.skip + render( + locals: { + queued_jobs_count: GoodJob::Job.queued.count, + pending_migrations: MigrationStatus.call + } + ) + end + + def heroku + authorization.skip end end diff --git a/app/controllers/exports_controller.rb b/app/controllers/exports_controller.rb new file mode 100644 index 000000000..6faa78f0c --- /dev/null +++ b/app/controllers/exports_controller.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +class ExportsController < ApplicationController + def index + xml = Feed::ExportToOpml.call(authorization.scope(Feed.all)) + + send_data( + xml, + type: "application/xml", + disposition: "attachment", + filename: "stringer.opml" + ) + end +end diff --git a/app/controllers/feeds_controller.rb b/app/controllers/feeds_controller.rb index 3b6ae5662..b54de2d4e 100644 --- a/app/controllers/feeds_controller.rb +++ b/app/controllers/feeds_controller.rb @@ -1,56 +1,64 @@ -require_relative "../repositories/feed_repository" -require_relative "../commands/feeds/add_new_feed" -require_relative "../commands/feeds/export_to_opml" +# frozen_string_literal: true -class Stringer < Sinatra::Base - get "/feeds" do - @feeds = FeedRepository.list - - erb :'feeds/index' +class FeedsController < ApplicationController + def index + @feeds = authorization.scope(FeedRepository.list.with_unread_stories_counts) end - delete "/feeds/:feed_id" do - FeedRepository.delete(params[:feed_id]) + def show + @feed = FeedRepository.fetch(params[:feed_id]) + authorization.check(@feed) + + @stories = StoryRepository.feed(params[:feed_id]) + @unread_stories = @stories.reject(&:is_read) + end - status 200 + def new + authorization.skip + @feed_url = params[:feed_url] end - get "/feeds/new" do - erb :'feeds/add' + def edit + @feed = FeedRepository.fetch(params[:id]) + authorization.check(@feed) end - post "/feeds" do + def create + authorization.skip @feed_url = params[:feed_url] - feed = AddNewFeed.add(@feed_url) + feed = Feed::Create.call(@feed_url, user: current_user) - if feed and feed.valid? - FetchFeeds.enqueue([feed]) + if feed && feed.valid? + CallableJob.perform_later(Feed::FetchOne, feed) - flash[:success] = t('feeds.add.flash.added_successfully') - redirect to("/") - elsif feed - flash.now[:error] = t('feeds.add.flash.already_subscribed_error') - erb :'feeds/add' + redirect_to("/", flash: { success: t(".success") }) else - flash.now[:error] = t('feeds.add.flash.feed_not_found_error') - erb :'feeds/add' + flash.now[:error] = feed ? t(".already_subscribed") : t(".feed_not_found") + + render(:new) end end - get "/feeds/import" do - erb :'feeds/import' - end + def update + feed = FeedRepository.fetch(params[:id]) + authorization.check(feed) - post "/feeds/import" do - ImportFromOpml.import(params["opml_file"][:tempfile].read) + FeedRepository.update_feed( + feed, + params[:feed_name], + params[:feed_url], + params[:group_id] + ) - redirect to("/setup/tutorial") + flash[:success] = t("feeds.edit.flash.updated_successfully") + redirect_to("/feeds") end - get "/feeds/export" do - content_type 'application/xml' - attachment 'stringer.opml' + def destroy + authorization.check(Feed.find(params[:id])) + FeedRepository.delete(params[:id]) - ExportToOpml.new(Feed.all).to_xml + flash[:success] = t(".success") + redirect_to("/feeds") end end diff --git a/app/controllers/fever_controller.rb b/app/controllers/fever_controller.rb new file mode 100644 index 000000000..bc3b59adc --- /dev/null +++ b/app/controllers/fever_controller.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +class FeverController < ApplicationController + skip_before_action :complete_setup, only: [:index, :update] + protect_from_forgery with: :null_session, only: [:update] + + def index + authorization.skip + render(json: FeverAPI::Response.call(fever_params)) + end + + def update + authorization.skip + render(json: FeverAPI::Response.call(fever_params)) + end + + private + + def fever_params + params.permit(FeverAPI::PARAMS).to_hash.symbolize_keys.merge(authorization:) + end + + def authenticate_user + return if current_user + + render(json: { api_version: FeverAPI::API_VERSION, auth: 0 }) + end + + def current_user + if instance_variable_defined?(:@current_user) + @current_user + else + @current_user = User.find_by(api_key: params[:api_key]) + end + end +end diff --git a/app/controllers/first_run_controller.rb b/app/controllers/first_run_controller.rb deleted file mode 100644 index c4bfbed67..000000000 --- a/app/controllers/first_run_controller.rb +++ /dev/null @@ -1,49 +0,0 @@ -require_relative "../commands/feeds/import_from_opml" -require_relative "../commands/users/create_user" -require_relative "../commands/users/complete_setup" -require_relative "../repositories/user_repository" -require_relative "../repositories/story_repository" -require_relative "../tasks/fetch_feeds" - -class Stringer < Sinatra::Base - namespace "/setup" do - before do - if UserRepository.setup_complete? - redirect to("/news") - end - end - - get "/password" do - erb :"first_run/password" - end - - post "/password" do - if no_password(params) or password_mismatch?(params) - flash.now[:error] = t('first_run.password.flash.passwords_dont_match') - erb :"first_run/password" - else - user = CreateUser.new.create(params[:password]) - session[:user_id] = user.id - - redirect to("/feeds/import") - end - end - - get "/tutorial" do - FetchFeeds.enqueue(Feed.all) - CompleteSetup.complete(current_user) - - @sample_stories = StoryRepository.samples - erb :tutorial - end - end - - private - def no_password(params) - params[:password].nil? || params[:password] == "" - end - - def password_mismatch?(params) - params[:password] != params[:password_confirmation] - end -end \ No newline at end of file diff --git a/app/controllers/imports_controller.rb b/app/controllers/imports_controller.rb new file mode 100644 index 000000000..c8887548c --- /dev/null +++ b/app/controllers/imports_controller.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +class ImportsController < ApplicationController + def new + authorization.skip + end + + def create + authorization.skip + Feed::ImportFromOpml.call(params["opml_file"].read, user: current_user) + + redirect_to("/setup/tutorial") + end +end diff --git a/app/controllers/passwords_controller.rb b/app/controllers/passwords_controller.rb new file mode 100644 index 000000000..0b1644439 --- /dev/null +++ b/app/controllers/passwords_controller.rb @@ -0,0 +1,58 @@ +# frozen_string_literal: true + +class PasswordsController < ApplicationController + skip_before_action :complete_setup, only: [:new, :create] + skip_before_action :authenticate_user, only: [:new, :create] + before_action :check_signups_enabled, only: [:new, :create] + + def new + authorization.skip + end + + def create + authorization.skip + + user = User.new(user_params) + if user.save + session[:user_id] = user.id + + redirect_to("/feeds/import") + else + flash.now[:error] = user.error_messages + render(:new) + end + end + + def update + authorization.skip + + if current_user.update(password_params) + redirect_to("/news", flash: { success: t(".success") }) + else + flash.now[:error] = t(".failure", errors: current_user.error_messages) + render("profiles/edit", locals: { user: current_user }) + end + end + + private + + def check_signups_enabled + redirect_to(login_path) unless Setting::UserSignup.enabled? + end + + def password_params + params + .expect(user: [ + :password_challenge, + :password, + :password_confirmation + ]) + end + + def user_params + params + .expect(user: [:username, :password, :password_confirmation]) + .merge(admin: User.none?) + .to_h.symbolize_keys + end +end diff --git a/app/controllers/profiles_controller.rb b/app/controllers/profiles_controller.rb new file mode 100644 index 000000000..7d80c917a --- /dev/null +++ b/app/controllers/profiles_controller.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +class ProfilesController < ApplicationController + def edit + authorization.skip + + render(locals: { user: current_user }) + end + + def update + authorization.skip + + if current_user.update(user_params) + redirect_to(news_path, flash: { success: t(".success") }) + else + errors = current_user.error_messages + flash.now[:error] = t(".failure", errors:) + render(:edit, locals: { user: current_user }) + end + end + + private + + def user_params + params.expect(user: [:username, :password_challenge, :stories_order]) + end +end diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb index cd3a5b0f7..a9fa82b36 100644 --- a/app/controllers/sessions_controller.rb +++ b/app/controllers/sessions_controller.rb @@ -1,26 +1,31 @@ -require_relative "../commands/users/sign_in_user" +# frozen_string_literal: true -class Stringer < Sinatra::Base - get "/login" do - erb :"sessions/new" +class SessionsController < ApplicationController + skip_before_action :authenticate_user, only: [:new, :create] + + def new + authorization.skip end - post "/login" do - user = SignInUser.sign_in(params[:password]) + def create + authorization.skip + user = SignInUser.call(params[:username], params[:password]) if user session[:user_id] = user.id - redirect to("/") + redirect_uri = session.delete(:redirect_to) || "/" + redirect_to(redirect_uri) else - flash.now[:error] = t('sessions.new.flash.wrong_password') - erb :"sessions/new" + flash.now[:error] = t("sessions.new.flash.wrong_password") + render(:new) end end - get "/logout" do - flash[:success] = t('sessions.destroy.flash.logged_out_successfully') + def destroy + authorization.skip + flash[:success] = t("sessions.destroy.flash.logged_out_successfully") session[:user_id] = nil - redirect to("/") + redirect_to("/") end -end \ No newline at end of file +end diff --git a/app/controllers/settings_controller.rb b/app/controllers/settings_controller.rb new file mode 100644 index 000000000..8452f4ca6 --- /dev/null +++ b/app/controllers/settings_controller.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +class SettingsController < ApplicationController + def index + authorization.skip + end + + def update + authorization.skip + + setting = Setting.find(params[:id]) + setting.update!(setting_params) + + redirect_to(settings_path) + end + + private + + def setting_params + params.expect(setting: [:enabled]) + end +end diff --git a/app/controllers/stories_controller.rb b/app/controllers/stories_controller.rb index 1b18964ab..d5b03747b 100644 --- a/app/controllers/stories_controller.rb +++ b/app/controllers/stories_controller.rb @@ -1,48 +1,33 @@ -require_relative "../repositories/story_repository" -require_relative "../commands/stories/mark_all_as_read" +# frozen_string_literal: true -class Stringer < Sinatra::Base - get "/news" do - @unread_stories = StoryRepository.unread - - erb :index +class StoriesController < ApplicationController + def index + order = current_user.stories_order + @unread_stories = authorization.scope(StoryRepository.unread(order:)) end - get "/feed/:feed_id" do - @feed = FeedRepository.fetch(params[:feed_id]) - - @stories = StoryRepository.feed(params[:feed_id]) - @unread_stories = @stories.find_all {|story| !story.is_read } - - erb :feed - end + def update + json_params = JSON.parse(request.body.read, symbolize_names: true) - get "/archive" do - @read_stories = StoryRepository.read(params[:page]) + story = authorization.check(StoryRepository.fetch(params[:id])) + story.update!(json_params.slice(:is_read, :is_starred, :keep_unread)) - erb :archive + head(:no_content) end - get "/starred" do - @starred_stories = StoryRepository.starred(params[:page]) + def mark_all_as_read + stories = authorization.scope(Story.where(id: params[:story_ids])) + MarkAllAsRead.call(stories.ids) - erb :starred + redirect_to("/news") end - put "/stories/:id" do - json_params = JSON.parse(request.body.read, symbolize_names: true) - - story = StoryRepository.fetch(params[:id]) - story.is_read = !!json_params[:is_read] - story.keep_unread = !!json_params[:keep_unread] - story.is_starred = !!json_params[:is_starred] - - StoryRepository.save(story) + def archived + @read_stories = authorization.scope(StoryRepository.read(params[:page])) end - post "/stories/mark_all_as_read" do - MarkAllAsRead.new(params[:story_ids]).mark_as_read - - redirect to("/news") + def starred + @starred_stories = + authorization.scope(StoryRepository.starred(params[:page])) end end diff --git a/app/controllers/tutorials_controller.rb b/app/controllers/tutorials_controller.rb new file mode 100644 index 000000000..42c9328d3 --- /dev/null +++ b/app/controllers/tutorials_controller.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +class TutorialsController < ApplicationController + def index + authorization.skip + CallableJob.perform_later(Feed::FetchAllForUser, current_user) + + @sample_stories = StoryRepository.samples + end +end diff --git a/app/fever_api/authentication.rb b/app/fever_api/authentication.rb deleted file mode 100644 index 00088cb6c..000000000 --- a/app/fever_api/authentication.rb +++ /dev/null @@ -1,11 +0,0 @@ -module FeverAPI - class Authentication - def initialize(options = {}) - @clock = options.fetch(:clock){ Time } - end - - def call(params) - { auth: 1, last_refreshed_on_time: @clock.now.to_i } - end - end -end diff --git a/app/fever_api/read_favicons.rb b/app/fever_api/read_favicons.rb deleted file mode 100644 index a1ad37858..000000000 --- a/app/fever_api/read_favicons.rb +++ /dev/null @@ -1,22 +0,0 @@ -module FeverAPI - class ReadFavicons - def call(params = {}) - if params.keys.include?('favicons') - { favicons: favicons } - else - {} - end - end - - private - - def favicons - [ - { - id: 0, - data: "image/gif;base64,R0lGODlhAQABAIAAAObm5gAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==" - } - ] - end - end -end diff --git a/app/fever_api/read_feeds.rb b/app/fever_api/read_feeds.rb deleted file mode 100644 index b9ff55852..000000000 --- a/app/fever_api/read_feeds.rb +++ /dev/null @@ -1,23 +0,0 @@ -require_relative "../repositories/feed_repository" - -module FeverAPI - class ReadFeeds - def initialize(options = {}) - @feed_repository = options.fetch(:feed_repository){ FeedRepository } - end - - def call(params = {}) - if params.keys.include?('feeds') - { feeds: feeds } - else - {} - end - end - - private - - def feeds - @feed_repository.list.map{|f| f.as_fever_json } - end - end -end diff --git a/app/fever_api/read_feeds_groups.rb b/app/fever_api/read_feeds_groups.rb deleted file mode 100644 index 8ca468b3e..000000000 --- a/app/fever_api/read_feeds_groups.rb +++ /dev/null @@ -1,32 +0,0 @@ -require_relative "../repositories/feed_repository" - -module FeverAPI - class ReadFeedsGroups - def initialize(options = {}) - @feed_repository = options.fetch(:feed_repository){ FeedRepository } - end - - def call(params = {}) - if params.keys.include?('feeds') || params.keys.include?('groups') - { feeds_groups: feeds_groups } - else - {} - end - end - - private - - def feeds_groups - [ - { - group_id: 1, - feed_ids: feeds.map{|f| f.id}.join(",") - } - ] - end - - def feeds - @feed_repository.list - end - end -end diff --git a/app/fever_api/read_groups.rb b/app/fever_api/read_groups.rb deleted file mode 100644 index 4291baf78..000000000 --- a/app/fever_api/read_groups.rb +++ /dev/null @@ -1,22 +0,0 @@ -module FeverAPI - class ReadGroups - def call(params = {}) - if params.keys.include?('groups') - { groups: groups } - else - {} - end - end - - private - - def groups - [ - { - id: 1, - title: "All items" - } - ] - end - end -end diff --git a/app/fever_api/read_items.rb b/app/fever_api/read_items.rb deleted file mode 100644 index 3481c476a..000000000 --- a/app/fever_api/read_items.rb +++ /dev/null @@ -1,46 +0,0 @@ -require_relative "../repositories/story_repository" - -module FeverAPI - class ReadItems - def initialize(options = {}) - @story_repository = options.fetch(:story_repository){ StoryRepository } - end - - def call(params = {}) - if params.keys.include?('items') - item_ids = params[:with_ids].split(',') rescue nil - - { - items: items(item_ids, params[:since_id]), - total_items: total_items(item_ids) - } - else - {} - end - end - - private - - def items(item_ids, since_id) - items = item_ids ? stories_by_ids(item_ids) : unread_stories(since_id) - items.map{|s| s.as_fever_json } - end - - def total_items(item_ids) - items = item_ids ? stories_by_ids(item_ids) : unread_stories - items.count - end - - def stories_by_ids(ids) - @story_repository.fetch_by_ids(ids) - end - - def unread_stories(since_id = nil) - if since_id - @story_repository.unread_since_id(since_id) - else - @story_repository.unread - end - end - end -end diff --git a/app/fever_api/read_links.rb b/app/fever_api/read_links.rb deleted file mode 100644 index df2005c88..000000000 --- a/app/fever_api/read_links.rb +++ /dev/null @@ -1,17 +0,0 @@ -module FeverAPI - class ReadLinks - def call(params = {}) - if params.keys.include?('links') - { links: links } - else - {} - end - end - - private - - def links - [] - end - end -end diff --git a/app/fever_api/response.rb b/app/fever_api/response.rb deleted file mode 100644 index 4656eabad..000000000 --- a/app/fever_api/response.rb +++ /dev/null @@ -1,43 +0,0 @@ -require_relative "authentication" - -require_relative "read_groups" -require_relative "read_feeds" -require_relative "read_feeds_groups" -require_relative "read_favicons" -require_relative "read_items" -require_relative "read_links" - -require_relative "sync_unread_item_ids" -require_relative "sync_saved_item_ids" - -require_relative "write_mark_item" -require_relative "write_mark_feed" -require_relative "write_mark_group" - -module FeverAPI - class Response - def initialize(params) - @response = { api_version: 3 } - - @response.merge! Authentication.new.call(params) - - @response.merge! ReadFeeds.new.call(params) - @response.merge! ReadGroups.new.call(params) - @response.merge! ReadFeedsGroups.new.call(params) - @response.merge! ReadFavicons.new.call(params) - @response.merge! ReadItems.new.call(params) - @response.merge! ReadLinks.new.call(params) - - @response.merge! SyncUnreadItemIds.new.call(params) - @response.merge! SyncSavedItemIds.new.call(params) - - @response.merge! WriteMarkItem.new.call(params) - @response.merge! WriteMarkFeed.new.call(params) - @response.merge! WriteMarkGroup.new.call(params) - end - - def to_json - @response.to_json - end - end -end diff --git a/app/fever_api/sync_saved_item_ids.rb b/app/fever_api/sync_saved_item_ids.rb deleted file mode 100644 index cbee949d4..000000000 --- a/app/fever_api/sync_saved_item_ids.rb +++ /dev/null @@ -1,27 +0,0 @@ -require_relative "../repositories/story_repository" - -module FeverAPI - class SyncSavedItemIds - def initialize(options = {}) - @story_repository = options.fetch(:story_repository){ StoryRepository } - end - - def call(params = {}) - if params.keys.include?('saved_item_ids') - { saved_item_ids: saved_item_ids } - else - {} - end - end - - private - - def saved_item_ids - all_starred_stories.map{|s| s.id}.join(',') - end - - def all_starred_stories - @story_repository.all_starred - end - end -end diff --git a/app/fever_api/sync_unread_item_ids.rb b/app/fever_api/sync_unread_item_ids.rb deleted file mode 100644 index e17d2bbe4..000000000 --- a/app/fever_api/sync_unread_item_ids.rb +++ /dev/null @@ -1,27 +0,0 @@ -require_relative "../repositories/story_repository" - -module FeverAPI - class SyncUnreadItemIds - def initialize(options = {}) - @story_repository = options.fetch(:story_repository){ StoryRepository } - end - - def call(params = {}) - if params.keys.include?('unread_item_ids') - { unread_item_ids: unread_item_ids } - else - {} - end - end - - private - - def unread_item_ids - unread_stories.map{|s| s.id}.join(',') - end - - def unread_stories - @story_repository.unread - end - end -end diff --git a/app/fever_api/write_mark_feed.rb b/app/fever_api/write_mark_feed.rb deleted file mode 100644 index b4b582e6b..000000000 --- a/app/fever_api/write_mark_feed.rb +++ /dev/null @@ -1,17 +0,0 @@ -require_relative "../commands/stories/mark_feed_as_read" - -module FeverAPI - class WriteMarkFeed - def initialize(options = {}) - @marker_class = options.fetch(:marker_class) { MarkFeedAsRead } - end - - def call(params = {}) - if params[:mark] == "feed" - @marker_class.new(params[:id], params[:before]).mark_feed_as_read - end - - {} - end - end -end diff --git a/app/fever_api/write_mark_group.rb b/app/fever_api/write_mark_group.rb deleted file mode 100644 index 7ccbe127b..000000000 --- a/app/fever_api/write_mark_group.rb +++ /dev/null @@ -1,17 +0,0 @@ -require_relative "../commands/stories/mark_group_as_read" - -module FeverAPI - class WriteMarkGroup - def initialize(options = {}) - @marker_class = options.fetch(:marker_class) { MarkGroupAsRead } - end - - def call(params = {}) - if params[:mark] == "group" - @marker_class.new(params[:id], params[:before]).mark_group_as_read - end - - {} - end - end -end diff --git a/app/fever_api/write_mark_item.rb b/app/fever_api/write_mark_item.rb deleted file mode 100644 index b3f1fd15f..000000000 --- a/app/fever_api/write_mark_item.rb +++ /dev/null @@ -1,32 +0,0 @@ -require_relative "../commands/stories/mark_as_read" -require_relative "../commands/stories/mark_as_unread" -require_relative "../commands/stories/mark_as_starred" -require_relative "../commands/stories/mark_as_unstarred" - -module FeverAPI - class WriteMarkItem - def initialize(options = {}) - @read_marker_class = options.fetch(:read_marker_class) { MarkAsRead } - @unread_marker_class = options.fetch(:unread_marker_class) { MarkAsUnread } - @starred_marker_class = options.fetch(:starred_marker_class) { MarkAsStarred } - @unstarred_marker_class = options.fetch(:unstarred_marker_class) { MarkAsUnstarred } - end - - def call(params = {}) - if params[:mark] == "item" - case params[:as] - when "read" - @read_marker_class.new(params[:id]).mark_as_read - when "unread" - @unread_marker_class.new(params[:id]).mark_as_unread - when "saved" - @starred_marker_class.new(params[:id]).mark_as_starred - when "unsaved" - @unstarred_marker_class.new(params[:id]).mark_as_unstarred - end - end - - {} - end - end -end diff --git a/app/helpers/authentication_helpers.rb b/app/helpers/authentication_helpers.rb deleted file mode 100644 index ae4b20b72..000000000 --- a/app/helpers/authentication_helpers.rb +++ /dev/null @@ -1,23 +0,0 @@ -require "sinatra/base" - -require_relative "../repositories/user_repository" - -module Sinatra - module AuthenticationHelpers - def is_authenticated? - session[:user_id] - end - - def needs_authentication?(path) - return false if ENV['RACK_ENV'] == 'test' - return false if !UserRepository.setup_complete? - return false if path == "/login" || path == "/logout" - return false if path =~ /css/ || path =~ /js/ || path =~ /img/ - true - end - - def current_user - UserRepository.fetch(session[:user_id]) - end - end -end \ No newline at end of file diff --git a/app/helpers/url_helpers.rb b/app/helpers/url_helpers.rb new file mode 100644 index 000000000..a17ee89cd --- /dev/null +++ b/app/helpers/url_helpers.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +module UrlHelpers + def expand_absolute_urls(content, base_url) + doc = Nokogiri::HTML.fragment(content) + + [["a", "href"], ["img", "src"], ["video", "src"]].each do |tag, attr| + doc.css("#{tag}[#{attr}]").each do |node| + url = node.get_attribute(attr) + next if url =~ URI::RFC2396_PARSER.regexp[:ABS_URI] + + node.set_attribute(attr, URI.join(base_url, url).to_s) + rescue URI::InvalidURIError + # Just ignore. If we cannot parse the url, we don't want the entire + # import to blow up. + end + end + + doc.to_html + end + + def normalize_url(url, base_url) + uri = URI.parse(url.strip) + + # resolve (protocol) relative URIs + if uri.relative? + base_uri = URI.parse(base_url.strip) + scheme = base_uri.scheme || "http" + uri = URI.join("#{scheme}://#{base_uri.host}", uri) + end + + uri.to_s + end +end diff --git a/app/javascript/@types/eslint-plugin-sort-keys-fix.d.ts b/app/javascript/@types/eslint-plugin-sort-keys-fix.d.ts new file mode 100644 index 000000000..cfc6f705f --- /dev/null +++ b/app/javascript/@types/eslint-plugin-sort-keys-fix.d.ts @@ -0,0 +1 @@ +declare module "eslint-plugin-sort-keys-fix"; diff --git a/app/public/js/app.js b/app/javascript/application.ts similarity index 59% rename from app/public/js/app.js rename to app/javascript/application.ts index 75cc47d85..3f2c20d83 100644 --- a/app/public/js/app.js +++ b/app/javascript/application.ts @@ -1,9 +1,58 @@ +// @ts-nocheck +import "@hotwired/turbo-rails"; +import "@rails/activestorage"; +import * as bootstrap from "bootstrap"; + +window.bootstrap = bootstrap; +import "mousetrap"; +import _ from "underscore"; +import Backbone from "backbone"; +import "backbone.nativeview"; + +import "./controllers/index"; + +Turbo.session.drive = false; + +/* global Mousetrap */ + +Backbone.ajax = async function(options) { + try { + const response = await fetch(options.url, { + body: options.data, + headers: {"Content-Type": "application/json", ...options.headers}, + method: options.type || "GET", + }); + const data = await response.json(); + if (response.ok && options.success) { + options.success(data, response.statusText, response); + } else if (!response.ok && options.error) { + options.error(response, response.statusText, null); + } + } catch (err: unknown) { + if (options.error) options.error(null, "error", err); + } +}; + _.templateSettings = { - interpolate: /\{\{\=(.+?)\}\}/g, - evaluate: /\{\{(.+?)\}\}/g + evaluate: /\{\{(.+?)\}\}/g, + interpolate: /\{\{=(.+?)\}\}/g }; +function CSRFToken() { + const tokenTag = document.getElementsByName('csrf-token')[0]; + + return (tokenTag && tokenTag.content) || ''; +} + +function requestHeaders() { + return { 'X-CSRF-Token': CSRFToken() }; +} + var Story = Backbone.Model.extend({ + close: function() { + this.set("open", false); + }, + defaults: function() { return { "open" : false, @@ -11,21 +60,9 @@ var Story = Backbone.Model.extend({ } }, - toggle: function() { - if (this.get("open")) { - this.close(); - } else { - this.open(); - } - }, - - shouldSave: function() { - return this.changedAttributes() && this.get("id") > 0; - }, - open: function() { if (!this.get("keep_unread")) this.set("is_read", true); - if (this.shouldSave()) this.save(); + if (this.shouldSave()) this.save(null, { headers: requestHeaders() }); if(this.collection){ this.collection.closeOthers(this); @@ -37,6 +74,27 @@ var Story = Backbone.Model.extend({ this.set("selected", true); }, + openInTab: function() { + window.open(this.get("permalink"), '_blank'); + }, + + select: function() { + if(this.collection) this.collection.unselectAll(); + this.set("selected", true); + }, + + shouldSave: function() { + return this.changedAttributes() && this.get("id") > 0; + }, + + toggle: function() { + if (this.get("open")) { + this.close(); + } else { + this.open(); + } + }, + toggleKeepUnread: function() { if (this.get("keep_unread")) { this.set("keep_unread", false); @@ -46,137 +104,94 @@ var Story = Backbone.Model.extend({ this.set("is_read", false); } - if (this.shouldSave()) this.save(); - }, - - toggleStarred: function() { - if (this.get("is_starred")) { - this.set("is_starred", false); - } else { - this.set("is_starred", true); - } - - if (this.shouldSave()) this.save(); - }, - - close: function() { - this.set("open", false); - }, - - select: function() { - if(this.collection) this.collection.unselectAll(); - this.set("selected", true); + if (this.shouldSave()) this.save(null, { headers: requestHeaders() }); }, unselect: function() { this.set("selected", false); - }, - - openInTab: function() { - window.open(this.get("permalink"), '_blank'); } }); -var StoryView = Backbone.View.extend({ - tagName: "li", +var StoryView = Backbone.NativeView.extend({ className: "story", - - template: "#story-template", - events: { - "click .story-preview" : "storyClicked", - "click .story-keep-unread" : "toggleKeepUnread", - "click .story-starred" : "toggleStarred" + "click .story-preview" : "storyClicked" }, initialize: function() { - this.template = _.template($(this.template).html()); + this.template = _.template(document.querySelector(this.template).innerHTML); this.listenTo(this.model, 'add', this.render); this.listenTo(this.model, 'change:selected', this.itemSelected); this.listenTo(this.model, 'change:open', this.itemOpened); this.listenTo(this.model, 'change:is_read', this.itemRead); - this.listenTo(this.model, 'change:keep_unread', this.itemKeepUnread); - this.listenTo(this.model, 'change:is_starred', this.itemStarred); - }, - - render: function() { - var jsonModel = this.model.toJSON(); - this.$el.html(this.template(jsonModel)); - if (jsonModel.is_read) { - this.$el.addClass('read'); - } - return this; - }, - - itemRead: function() { - this.$el.toggleClass("read", this.model.get("is_read")); + this.el.addEventListener('keep-unread-toggle:toggled', (e) => { + var detail = e.detail; + this.model.set({is_read: detail.isRead, keep_unread: detail.keepUnread}, {silent: true}); + this.model.trigger('change:is_read'); + }); }, itemOpened: function() { + var storyLead = this.el.querySelector(".story-lead"); if (this.model.get("open")) { - this.$el.addClass("open"); - $(".story-lead", this.$el).fadeOut(1000); - window.scrollTo(0, this.$el.offset().top); + this.el.classList.add("open"); + if (storyLead) storyLead.style.display = "none"; + this.el.scrollIntoView({ block: "start" }); } else { - this.$el.removeClass("open"); - $(".story-lead", this.$el).show(); + this.el.classList.remove("open"); + if (storyLead) storyLead.style.display = ""; } }, - itemSelected: function() { - this.$el.toggleClass("cursor", this.model.get("selected")); - if (!this.$el.visible()) window.scrollTo(0, this.$el.offset().top); + itemRead: function() { + this.el.classList.toggle("read", this.model.get("is_read")); }, - itemKeepUnread: function() { - var icon = this.model.get("keep_unread") ? "icon-check" : "icon-check-empty"; - this.$(".story-keep-unread > i").attr("class", icon); + itemSelected: function() { + this.el.classList.toggle("cursor", this.model.get("selected")); + requestAnimationFrame(() => { this.el.scrollIntoView({ block: "nearest" }); }); }, - itemStarred: function() { - var icon = this.model.get("is_starred") ? "icon-star" : "icon-star-empty"; - this.$(".story-starred > i").attr("class", icon); + render: function() { + var jsonModel = this.model.toJSON(); + this.el.innerHTML = this.template(jsonModel); + if (jsonModel.is_read) { + this.el.classList.add('read'); + } + if (jsonModel.keep_unread) { + this.el.classList.add('keepUnread'); + } + Object.assign(this.el.dataset, { + controller: "star-toggle keep-unread-toggle", + keepUnreadToggleIdValue: String(jsonModel.id), + keepUnreadToggleIsReadValue: String(jsonModel.is_read), + keepUnreadToggleKeepUnreadValue: String(jsonModel.keep_unread), + starToggleIdValue: String(jsonModel.id), + starToggleStarredValue: String(jsonModel.is_starred), + }); + return this; }, storyClicked: function(e) { if (e.metaKey || e.ctrlKey || e.which == 2) { - var background_tab = window.open(this.model.get("permalink")); - background_tab.blur(); + var backgroundTab = window.open(this.model.get("permalink")); + if (backgroundTab) backgroundTab.blur(); window.focus(); if (!this.model.get("keep_unread")) this.model.set("is_read", true); - if (this.model.shouldSave()) this.model.save(); + if (this.model.shouldSave()) this.model.save(null, { headers: requestHeaders() }); } else { this.model.toggle(); - window.scrollTo(0, this.$el.offset().top); + this.el.scrollIntoView({ block: "start" }); } }, - toggleKeepUnread: function() { - this.model.toggleKeepUnread(); - }, + tagName: "li", + + template: "#story-template", - toggleStarred: function(e) { - e.stopPropagation(); - this.model.toggleStarred(); - } }); var StoryList = Backbone.Collection.extend({ - model: Story, - url: "/stories", - - initialize: function() { - this.cursorPosition = -1; - }, - - max_position: function() { - return this.length - 1; - }, - - unreadCount: function() { - return this.where({is_read: false}).length; - }, - closeOthers: function(modelToSkip) { this.each(function(model) { if (model.id != modelToSkip.id) { @@ -184,23 +199,15 @@ var StoryList = Backbone.Collection.extend({ } }); }, - - selected: function() { - return this.where({selected: true}); - }, - - unselectAll: function() { - _.invoke(this.selected(), "unselect"); + initialize: function() { + this.cursorPosition = -1; }, - selectedStoryId: function() { - var selectedStory = this.at(this.cursorPosition); - return selectedStory ? selectedStory.id : -1; + max_position: function() { + return this.length - 1; }, - setSelection: function(model) { - this.cursorPosition = this.indexOf(model); - }, + model: Story, moveCursorDown: function() { if (this.cursorPosition < this.max_position()) { @@ -226,14 +233,22 @@ var StoryList = Backbone.Collection.extend({ this.at(this.cursorPosition).open(); }, - toggleCurrent: function() { - if (this.cursorPosition < 0) this.cursorPosition = 0; - this.at(this.cursorPosition).toggle(); + selected: function() { + return this.where({selected: true}); }, - viewCurrentInTab: function() { + selectedStoryId: function() { + var selectedStory = this.at(this.cursorPosition); + return selectedStory ? selectedStory.id : -1; + }, + + setSelection: function(model) { + this.cursorPosition = this.indexOf(model); + }, + + toggleCurrent: function() { if (this.cursorPosition < 0) this.cursorPosition = 0; - this.at(this.cursorPosition).openInTab(); + this.at(this.cursorPosition).toggle(); }, toggleCurrentKeepUnread: function() { @@ -241,18 +256,36 @@ var StoryList = Backbone.Collection.extend({ this.at(this.cursorPosition).toggleKeepUnread(); }, - toggleCurrentStarred: function() { + unreadCount: function() { + return this.where({is_read: false}).length; + }, + + unselectAll: function() { + _.invoke(this.selected(), "unselect"); + }, + + url: "/stories", + + viewCurrentInTab: function() { if (this.cursorPosition < 0) this.cursorPosition = 0; - this.at(this.cursorPosition).toggleStarred(); + this.at(this.cursorPosition).openInTab(); } }); -var AppView = Backbone.View.extend({ +var AppView = Backbone.NativeView.extend({ + addAll: function() { + this.stories.each(this.addOne, this); + }, + + addOne: function(story) { + var view = new StoryView({model: story}); + this.el.querySelector("#story-list").appendChild(view.render().el); + }, + el: "#stories", initialize: function(collection) { this.stories = collection; - this.el = $(this.el); this.listenTo(this.stories, 'add', this.addOne); this.listenTo(this.stories, 'reset', this.addAll); @@ -263,25 +296,6 @@ var AppView = Backbone.View.extend({ this.stories.reset(data); }, - render: function() { - var unreadCount = this.stories.unreadCount(); - - if (unreadCount === 0) { - document.title = window.i18n.titleName; - } else { - document.title = "(" + unreadCount + ") " + window.i18n.titleName; - } - }, - - addOne: function(story) { - var view = new StoryView({model: story}); - this.$("#story-list").append(view.render().el); - }, - - addAll: function() { - this.stories.each(this.addOne, this); - }, - moveCursorDown: function() { this.stories.moveCursorDown(); }, @@ -294,42 +308,36 @@ var AppView = Backbone.View.extend({ this.stories.openCurrentSelection(); }, - toggleCurrent: function() { - this.stories.toggleCurrent(); + render: function() { + var unreadCount = this.stories.unreadCount(); + + if (unreadCount === 0) { + document.title = window.i18n.titleName; + } else { + document.title = "(" + unreadCount + ") " + window.i18n.titleName; + } }, - viewCurrentInTab: function() { - this.stories.viewCurrentInTab(); + toggleCurrent: function() { + this.stories.toggleCurrent(); }, toggleCurrentKeepUnread: function() { this.stories.toggleCurrentKeepUnread(); }, - toggleCurrentStarred: function() { - this.stories.toggleCurrentStarred(); + viewCurrentInTab: function() { + this.stories.viewCurrentInTab(); } }); -$(document).ready(function() { - $(".remove-feed").click(function(e) { - e.preventDefault(); - var $this = $(this); - - var feedId = $this.parents("li").data("id"); - - if (feedId > 0) { - $.ajax({url: "/feeds/" + feedId, type: "DELETE"}) - .success(function() { - $this.parents("li").fadeOut(500, function () { - $(this).remove(); - }); - }) - .fail(function() { alert("something broke!"); }); - } - }); - +document.addEventListener("DOMContentLoaded", function() { Mousetrap.bind("?", function() { - $("#shortcuts").modal('toggle'); + bootstrap.Modal.getOrCreateInstance(document.getElementById('shortcuts')).toggle(); }); }); + +window.StoryList = StoryList; +window.AppView = AppView; + +export { Story, StoryView, StoryList, AppView }; diff --git a/app/javascript/controllers/application.ts b/app/javascript/controllers/application.ts new file mode 100644 index 000000000..4001c5817 --- /dev/null +++ b/app/javascript/controllers/application.ts @@ -0,0 +1,5 @@ +import {Application} from "@hotwired/stimulus"; + +const application = Application.start(); + +export {application}; diff --git a/app/javascript/controllers/dialog_controller.ts b/app/javascript/controllers/dialog_controller.ts new file mode 100644 index 000000000..85af41dc0 --- /dev/null +++ b/app/javascript/controllers/dialog_controller.ts @@ -0,0 +1,7 @@ +import {Controller} from "@hotwired/stimulus"; + +export default class extends Controller { + override connect(): void { + this.element.textContent = "Hello World!"; + } +} diff --git a/app/javascript/controllers/hotkeys_controller.ts b/app/javascript/controllers/hotkeys_controller.ts new file mode 100644 index 000000000..851ff3e62 --- /dev/null +++ b/app/javascript/controllers/hotkeys_controller.ts @@ -0,0 +1,27 @@ +import {Controller} from "@hotwired/stimulus"; + +import {assert} from "helpers/assert"; + +export default class extends Controller { + static override targets = ["click"]; + + clickTargets!: HTMLElement[]; + + indexedClickTargets = new Map(); + + clickTargetConnected(element: HTMLElement): void { + const {hotkey} = element.dataset; + this.indexedClickTargets.set(assert(hotkey), element); + } + + clickTargetDisconnected(element: HTMLElement): void { + const {hotkey} = element.dataset; + this.indexedClickTargets.delete(assert(hotkey)); + } + + handleKeydown(event: KeyboardEvent): void { + const clickable = this.indexedClickTargets.get(event.key); + + if (clickable) { clickable.click(); } + } +} diff --git a/app/javascript/controllers/index.ts b/app/javascript/controllers/index.ts new file mode 100644 index 000000000..806d7496a --- /dev/null +++ b/app/javascript/controllers/index.ts @@ -0,0 +1,22 @@ +/* + * This file is auto-generated by ./bin/rails stimulus:manifest:update + * Run that command whenever you add a new controller or create them with + * ./bin/rails generate stimulus controllerName + */ + +import {application} from "./application"; + +import DialogController from "./dialog_controller"; +application.register("dialog", DialogController); + +import HotkeysController from "./hotkeys_controller"; +application.register("hotkeys", HotkeysController); + +import KeepUnreadToggleController from "./keep_unread_toggle_controller"; +application.register("keep-unread-toggle", KeepUnreadToggleController); + +import MarkAllAsReadController from "./mark_all_as_read_controller"; +application.register("mark-all-as-read", MarkAllAsReadController); + +import StarToggleController from "./star_toggle_controller"; +application.register("star-toggle", StarToggleController); diff --git a/app/javascript/controllers/keep_unread_toggle_controller.ts b/app/javascript/controllers/keep_unread_toggle_controller.ts new file mode 100644 index 000000000..816e678fb --- /dev/null +++ b/app/javascript/controllers/keep_unread_toggle_controller.ts @@ -0,0 +1,54 @@ +import {Controller} from "@hotwired/stimulus"; + +import {updateStory} from "helpers/api"; + +export default class extends Controller { + static override values = {id: String, isRead: Boolean, keepUnread: Boolean}; + + static override targets = ["icon"]; + + declare idValue: string; + + declare isReadValue: boolean; + + declare keepUnreadValue: boolean; + + iconTargets!: HTMLElement[]; + + toggle(): void { + this.keepUnreadValue = !this.keepUnreadValue; + this.isReadValue = !this.keepUnreadValue; + + let icon = "fa fa-square-o"; + if (this.keepUnreadValue) { icon = "fa fa-check"; } + + for (const target of this.iconTargets) { + target.className = icon; + } + + this.updateStoryElement(); + this.dispatch("toggled", { + bubbles: true, + detail: {isRead: this.isReadValue, keepUnread: this.keepUnreadValue}, + }); + this.persistState(); + } + + private updateStoryElement(): void { + const storyEl = this.element.closest("li.story"); + storyEl?.classList.toggle("keepUnread", this.keepUnreadValue); + storyEl?.classList.toggle("read", this.isReadValue); + } + + private persistState(): void { + /* eslint-disable camelcase */ + const attrs = { + is_read: this.isReadValue, + keep_unread: this.keepUnreadValue, + }; + /* eslint-enable camelcase */ + updateStory(this.idValue, attrs).catch(() => { + // Optimistic UI — ignore server errors + }); + } +} diff --git a/app/javascript/controllers/mark_all_as_read_controller.ts b/app/javascript/controllers/mark_all_as_read_controller.ts new file mode 100644 index 000000000..d237103fd --- /dev/null +++ b/app/javascript/controllers/mark_all_as_read_controller.ts @@ -0,0 +1,15 @@ +import {Controller} from "@hotwired/stimulus"; + +import {assert} from "helpers/assert"; + +export default class extends Controller { + static override targets = ["form"]; + + formTarget!: HTMLFormElement; + + submit(event: Event): void { + event.preventDefault(); + + assert(this.formTarget).requestSubmit(); + } +} diff --git a/app/javascript/controllers/star_toggle_controller.ts b/app/javascript/controllers/star_toggle_controller.ts new file mode 100644 index 000000000..20da1012d --- /dev/null +++ b/app/javascript/controllers/star_toggle_controller.ts @@ -0,0 +1,31 @@ +import {Controller} from "@hotwired/stimulus"; + +import {updateStory} from "helpers/api"; + +export default class extends Controller { + static override values = {id: String, starred: Boolean}; + + static override targets = ["icon"]; + + declare idValue: string; + + declare starredValue: boolean; + + iconTargets!: HTMLElement[]; + + toggle(): void { + this.starredValue = !this.starredValue; + + let icon = "fa fa-star-o"; + if (this.starredValue) { icon = "fa fa-star"; } + + for (const target of this.iconTargets) { + target.className = icon; + } + + // eslint-disable-next-line camelcase + updateStory(this.idValue, {is_starred: this.starredValue}).catch(() => { + // Optimistic UI — ignore server errors + }); + } +} diff --git a/app/javascript/helpers/api.ts b/app/javascript/helpers/api.ts new file mode 100644 index 000000000..24751b743 --- /dev/null +++ b/app/javascript/helpers/api.ts @@ -0,0 +1,22 @@ +function csrfToken(): string { + const tag = + document.querySelector("meta[name='csrf-token']"); + + return tag?.content ?? ""; +} + +async function updateStory( + id: string, + attributes: {[key: string]: unknown}, +): Promise { + return fetch(`/stories/${id}`, { + body: JSON.stringify(attributes), + headers: { + "Content-Type": "application/json", + "X-CSRF-Token": csrfToken(), + }, + method: "PUT", + }); +} + +export {updateStory}; diff --git a/app/javascript/helpers/assert.ts b/app/javascript/helpers/assert.ts new file mode 100644 index 000000000..2d2737bd3 --- /dev/null +++ b/app/javascript/helpers/assert.ts @@ -0,0 +1,9 @@ +function assert(value: T | null | undefined): T { + if (value === null || value === undefined) { + throw new Error("value is null or undefined"); + } + + return value; +} + +export {assert}; diff --git a/app/jobs/application_job.rb b/app/jobs/application_job.rb new file mode 100644 index 000000000..d92ffddcb --- /dev/null +++ b/app/jobs/application_job.rb @@ -0,0 +1,4 @@ +# frozen_string_literal: true + +class ApplicationJob < ActiveJob::Base +end diff --git a/app/jobs/callable_job.rb b/app/jobs/callable_job.rb new file mode 100644 index 000000000..3225d6dd1 --- /dev/null +++ b/app/jobs/callable_job.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class CallableJob < ApplicationJob + def perform(callable, *, **) + callable.call(*, **) + end +end diff --git a/app/jobs/fetch_feed_job.rb b/app/jobs/fetch_feed_job.rb deleted file mode 100644 index 36ce602c2..000000000 --- a/app/jobs/fetch_feed_job.rb +++ /dev/null @@ -1,6 +0,0 @@ -class FetchFeedJob < Struct.new(:feed_id) - def perform - feed = FeedRepository.fetch(feed_id) - FetchFeed.new(feed).fetch - end -end \ No newline at end of file diff --git a/app/models/application_record.rb b/app/models/application_record.rb new file mode 100644 index 000000000..866db6833 --- /dev/null +++ b/app/models/application_record.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +class ApplicationRecord < ActiveRecord::Base + primary_abstract_class + + def self.boolean_accessor(attribute, key, default: false) + store_accessor(attribute, key) + + define_method(key) do + value = super() + value.nil? ? default : CastBoolean.call(value) + end + alias_method(:"#{key}?", :"#{key}") + + define_method(:"#{key}=") do |value| + super(value.nil? ? default : CastBoolean.call(value)) + end + end + + def error_messages + errors.full_messages.join(", ") + end +end diff --git a/app/models/concerns/.keep b/app/models/concerns/.keep new file mode 100644 index 000000000..e69de29bb diff --git a/app/models/feed.rb b/app/models/feed.rb index e9bc2d4c9..17ffca406 100644 --- a/app/models/feed.rb +++ b/app/models/feed.rb @@ -1,40 +1,40 @@ -class Feed < ActiveRecord::Base - has_many :stories, -> {order "published desc"} , dependent: :delete_all +# frozen_string_literal: true - validates_uniqueness_of :url +class Feed < ApplicationRecord + has_many :stories, -> { order(published: :desc) }, dependent: :delete_all + has_many :unread_stories, -> { unread }, class_name: "Story" + belongs_to :group + belongs_to :user - STATUS = { green: 0, yellow: 1, red: 2 } + delegate :name, to: :group, prefix: true, allow_nil: true - def status - STATUS.key(read_attribute(:status)) - end + validates :url, presence: true, uniqueness: { scope: :user_id } + validates :user_id, presence: true - def status=(s) - write_attribute(:status, STATUS[s]) - end + enum :status, { green: 0, yellow: 1, red: 2 } - def status_bubble - return :yellow if status == :red && stories.any? - status - end + scope :with_unread_stories_counts, + lambda { + left_joins(:unread_stories) + .select("feeds.*, count(stories.id) as unread_stories_count") + .group("feeds.id") + } - def unread_stories - stories.where('is_read = ?', false) - end + def status_bubble + return "yellow" if status == "red" && stories.any? - def has_unread_stories - unread_stories.any? + status end def as_fever_json { - id: self.id, + id:, favicon_id: 0, - title: self.name, - url: self.url, - site_url: self.url, + title: name || "", + url:, + site_url: url, is_spark: 0, - last_updated_on_time: self.last_fetched.to_i + last_updated_on_time: last_fetched.to_i } end end diff --git a/app/models/group.rb b/app/models/group.rb new file mode 100644 index 000000000..26ebe1ddd --- /dev/null +++ b/app/models/group.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +class Group < ApplicationRecord + UNGROUPED = Group.new(id: 0, name: "Ungrouped") + + belongs_to :user + has_many :feeds + + validates :name, presence: true, uniqueness: { scope: :user_id } + validates :user_id, presence: true + + def as_fever_json + { id:, title: name } + end +end diff --git a/app/models/migration_status.rb b/app/models/migration_status.rb index a96d98d09..c9b0f618f 100644 --- a/app/models/migration_status.rb +++ b/app/models/migration_status.rb @@ -1,19 +1,11 @@ -class MigrationStatus - attr_reader :migrator +# frozen_string_literal: true - def initialize(migrator=ActiveRecord::Migrator) - @migrator = migrator - end - - def pending_migrations - migrations_path = migrator.migrations_path - migrations = migrator.migrations(migrations_path) - current_version = migrator.current_version +module MigrationStatus + def self.call + migrator = ActiveRecord::Base.connection.pool.migration_context.open - migrations.select do |m| - current_version < m.version - end.map do |m| - "#{m.name} - #{m.version}" + migrator.pending_migrations.map do |migration| + "#{migration.name} - #{migration.version}" end end end diff --git a/app/models/setting.rb b/app/models/setting.rb new file mode 100644 index 000000000..cb38322ec --- /dev/null +++ b/app/models/setting.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +class Setting < ApplicationRecord + validates :type, presence: true, uniqueness: true +end diff --git a/app/models/setting/user_signup.rb b/app/models/setting/user_signup.rb new file mode 100644 index 000000000..d1ec03f81 --- /dev/null +++ b/app/models/setting/user_signup.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +class Setting::UserSignup < Setting + boolean_accessor :data, :enabled, default: false + + validates :enabled, inclusion: { in: [true, false] } + + def self.first + first_or_create! + end + + def self.enabled? + first_or_create!.enabled? || User.none? + end +end diff --git a/app/models/story.rb b/app/models/story.rb index d82f7b2e1..3806fafe1 100644 --- a/app/models/story.rb +++ b/app/models/story.rb @@ -1,47 +1,53 @@ -require_relative "./feed" +# frozen_string_literal: true -class Story < ActiveRecord::Base +class Story < ApplicationRecord belongs_to :feed + has_one :user, through: :feed validates_uniqueness_of :entry_id, scope: :feed_id + delegate :group_id, :user_id, to: :feed + + scope :unread, -> { where(is_read: false) } + UNTITLED = "[untitled]" def headline - self.title.nil? ? UNTITLED : strip_html(self.title)[0, 50] + title.nil? ? UNTITLED : strip_html(title)[0, 50] end def lead - strip_html(self.body)[0,100] + strip_html(body)[0, 100] end def source - self.feed.name + feed.name end def pretty_date - I18n.l(self.published) + I18n.l(published) end - def as_json(options = {}) + def as_json(_options = {}) super(methods: [:headline, :lead, :source, :pretty_date]) end def as_fever_json { - id: self.id, - feed_id: self.feed_id, - title: self.title, + id:, + feed_id:, + title:, author: source, html: body, - url: self.permalink, - is_saved: self.is_starred ? 1 : 0, - is_read: self.is_read ? 1 : 0, - created_on_time: self.published.to_i + url: permalink, + is_saved: is_starred ? 1 : 0, + is_read: is_read ? 1 : 0, + created_on_time: published.to_i } end private + def strip_html(contents) Loofah.fragment(contents).text end diff --git a/app/models/subscription.rb b/app/models/subscription.rb new file mode 100644 index 000000000..6c001351e --- /dev/null +++ b/app/models/subscription.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +class Subscription < ApplicationRecord + belongs_to :user + + STATUSES = ["active", "past_due", "unpaid", "canceled"].freeze + + validates :user_id, presence: true, uniqueness: true + validates :stripe_customer_id, presence: true, uniqueness: true + validates :stripe_subscription_id, presence: true, uniqueness: true + validates :status, presence: true, inclusion: { in: STATUSES } + validates :current_period_start, presence: true + validates :current_period_end, presence: true +end diff --git a/app/models/user.rb b/app/models/user.rb index e799d2be1..587a6cc93 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -1,9 +1,45 @@ -class User < ActiveRecord::Base -# attr_accessible :setup_complete, :api_key -# attr_accessible :password, :password_confirmation +# frozen_string_literal: true + +class User < ApplicationRecord has_secure_password - def setup_complete? - setup_complete + encrypts :api_key, deterministic: true + + has_many :feeds, dependent: :delete_all + has_many :groups, dependent: :delete_all + + validates :username, presence: true, uniqueness: { case_sensitive: false } + validate :password_challenge_matches + + before_save :update_api_key + + enum :stories_order, { desc: "desc", asc: "asc" }, prefix: true + + attr_accessor :password_challenge + + # `password_challenge` logic should be able to be removed in Rails 7.1 + # https://blog.appsignal.com/2023/02/15/whats-new-in-rails-7-1.html#password-challenge-via-has_secure_password + def password_challenge_matches + return unless password_challenge + + digested_password = BCrypt::Password.new(password_digest_was) + return if digested_password.is_password?(password_challenge) + + errors.add(:original_password, "does not match") + end + + def update_api_key + return unless password_digest_changed? || username_changed? + + if password_challenge.blank? && password.blank? + message = "Cannot change username without providing a password" + + raise(ActiveRecord::ActiveRecordError, message) + end + + password = password_challenge.presence || self.password.presence + + # API key based on Fever spec: https://feedafever.com/api + self.api_key = Digest::MD5.hexdigest("#{username}:#{password}") end end diff --git a/app/public/css/bootstrap-min.css b/app/public/css/bootstrap-min.css deleted file mode 100644 index c10c7f417..000000000 --- a/app/public/css/bootstrap-min.css +++ /dev/null @@ -1,9 +0,0 @@ -/*! - * Bootstrap v2.3.1 - * - * Copyright 2012 Twitter, Inc - * Licensed under the Apache License v2.0 - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Designed and built with all the love in the world @twitter by @mdo and @fat. - */.clearfix{*zoom:1}.clearfix:before,.clearfix:after{display:table;line-height:0;content:""}.clearfix:after{clear:both}.hide-text{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.input-block-level{display:block;width:100%;min-height:30px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}article,aside,details,figcaption,figure,footer,header,hgroup,nav,section{display:block}audio,canvas,video{display:inline-block;*display:inline;*zoom:1}audio:not([controls]){display:none}html{font-size:100%;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%}a:focus{outline:thin dotted #333;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}a:hover,a:active{outline:0}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sup{top:-0.5em}sub{bottom:-0.25em}img{width:auto\9;height:auto;max-width:100%;vertical-align:middle;border:0;-ms-interpolation-mode:bicubic}#map_canvas img,.google-maps img{max-width:none}button,input,select,textarea{margin:0;font-size:100%;vertical-align:middle}button,input{*overflow:visible;line-height:normal}button::-moz-focus-inner,input::-moz-focus-inner{padding:0;border:0}button,html input[type="button"],input[type="reset"],input[type="submit"]{cursor:pointer;-webkit-appearance:button}label,select,button,input[type="button"],input[type="reset"],input[type="submit"],input[type="radio"],input[type="checkbox"]{cursor:pointer}input[type="search"]{-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box;-webkit-appearance:textfield}input[type="search"]::-webkit-search-decoration,input[type="search"]::-webkit-search-cancel-button{-webkit-appearance:none}textarea{overflow:auto;vertical-align:top}@media print{*{color:#000!important;text-shadow:none!important;background:transparent!important;box-shadow:none!important}a,a:visited{text-decoration:underline}a[href]:after{content:" (" attr(href) ")"}abbr[title]:after{content:" (" attr(title) ")"}.ir a:after,a[href^="javascript:"]:after,a[href^="#"]:after{content:""}pre,blockquote{border:1px solid #999;page-break-inside:avoid}thead{display:table-header-group}tr,img{page-break-inside:avoid}img{max-width:100%!important}@page{margin:.5cm}p,h2,h3{orphans:3;widows:3}h2,h3{page-break-after:avoid}}body{margin:0;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:14px;line-height:20px;color:#333;background-color:#fff}a{color:#08c;text-decoration:none}a:hover,a:focus{color:#005580;text-decoration:underline}.img-rounded{-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px}.img-polaroid{padding:4px;background-color:#fff;border:1px solid #ccc;border:1px solid rgba(0,0,0,0.2);-webkit-box-shadow:0 1px 3px rgba(0,0,0,0.1);-moz-box-shadow:0 1px 3px rgba(0,0,0,0.1);box-shadow:0 1px 3px rgba(0,0,0,0.1)}.img-circle{-webkit-border-radius:500px;-moz-border-radius:500px;border-radius:500px}.row{margin-left:-20px;*zoom:1}.row:before,.row:after{display:table;line-height:0;content:""}.row:after{clear:both}[class*="span"]{float:left;min-height:1px;margin-left:20px}.container,.navbar-static-top .container,.navbar-fixed-top .container,.navbar-fixed-bottom .container{width:940px}.span12{width:940px}.span11{width:860px}.span10{width:780px}.span9{width:700px}.span8{width:620px}.span7{width:540px}.span6{width:460px}.span5{width:380px}.span4{width:300px}.span3{width:220px}.span2{width:140px}.span1{width:60px}.offset12{margin-left:980px}.offset11{margin-left:900px}.offset10{margin-left:820px}.offset9{margin-left:740px}.offset8{margin-left:660px}.offset7{margin-left:580px}.offset6{margin-left:500px}.offset5{margin-left:420px}.offset4{margin-left:340px}.offset3{margin-left:260px}.offset2{margin-left:180px}.offset1{margin-left:100px}.row-fluid{width:100%;*zoom:1}.row-fluid:before,.row-fluid:after{display:table;line-height:0;content:""}.row-fluid:after{clear:both}.row-fluid [class*="span"]{display:block;float:left;width:100%;min-height:30px;margin-left:2.127659574468085%;*margin-left:2.074468085106383%;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.row-fluid [class*="span"]:first-child{margin-left:0}.row-fluid .controls-row [class*="span"]+[class*="span"]{margin-left:2.127659574468085%}.row-fluid .span12{width:100%;*width:99.94680851063829%}.row-fluid .span11{width:91.48936170212765%;*width:91.43617021276594%}.row-fluid .span10{width:82.97872340425532%;*width:82.92553191489361%}.row-fluid .span9{width:74.46808510638297%;*width:74.41489361702126%}.row-fluid .span8{width:65.95744680851064%;*width:65.90425531914893%}.row-fluid .span7{width:57.44680851063829%;*width:57.39361702127659%}.row-fluid .span6{width:48.93617021276595%;*width:48.88297872340425%}.row-fluid .span5{width:40.42553191489362%;*width:40.37234042553192%}.row-fluid .span4{width:31.914893617021278%;*width:31.861702127659576%}.row-fluid .span3{width:23.404255319148934%;*width:23.351063829787233%}.row-fluid .span2{width:14.893617021276595%;*width:14.840425531914894%}.row-fluid .span1{width:6.382978723404255%;*width:6.329787234042553%}.row-fluid .offset12{margin-left:104.25531914893617%;*margin-left:104.14893617021275%}.row-fluid .offset12:first-child{margin-left:102.12765957446808%;*margin-left:102.02127659574467%}.row-fluid .offset11{margin-left:95.74468085106382%;*margin-left:95.6382978723404%}.row-fluid .offset11:first-child{margin-left:93.61702127659574%;*margin-left:93.51063829787232%}.row-fluid .offset10{margin-left:87.23404255319149%;*margin-left:87.12765957446807%}.row-fluid .offset10:first-child{margin-left:85.1063829787234%;*margin-left:84.99999999999999%}.row-fluid .offset9{margin-left:78.72340425531914%;*margin-left:78.61702127659572%}.row-fluid .offset9:first-child{margin-left:76.59574468085106%;*margin-left:76.48936170212764%}.row-fluid .offset8{margin-left:70.2127659574468%;*margin-left:70.10638297872339%}.row-fluid .offset8:first-child{margin-left:68.08510638297872%;*margin-left:67.9787234042553%}.row-fluid .offset7{margin-left:61.70212765957446%;*margin-left:61.59574468085106%}.row-fluid .offset7:first-child{margin-left:59.574468085106375%;*margin-left:59.46808510638297%}.row-fluid .offset6{margin-left:53.191489361702125%;*margin-left:53.085106382978715%}.row-fluid .offset6:first-child{margin-left:51.063829787234035%;*margin-left:50.95744680851063%}.row-fluid .offset5{margin-left:44.68085106382979%;*margin-left:44.57446808510638%}.row-fluid .offset5:first-child{margin-left:42.5531914893617%;*margin-left:42.4468085106383%}.row-fluid .offset4{margin-left:36.170212765957444%;*margin-left:36.06382978723405%}.row-fluid .offset4:first-child{margin-left:34.04255319148936%;*margin-left:33.93617021276596%}.row-fluid .offset3{margin-left:27.659574468085104%;*margin-left:27.5531914893617%}.row-fluid .offset3:first-child{margin-left:25.53191489361702%;*margin-left:25.425531914893618%}.row-fluid .offset2{margin-left:19.148936170212764%;*margin-left:19.04255319148936%}.row-fluid .offset2:first-child{margin-left:17.02127659574468%;*margin-left:16.914893617021278%}.row-fluid .offset1{margin-left:10.638297872340425%;*margin-left:10.53191489361702%}.row-fluid .offset1:first-child{margin-left:8.51063829787234%;*margin-left:8.404255319148938%}[class*="span"].hide,.row-fluid [class*="span"].hide{display:none}[class*="span"].pull-right,.row-fluid [class*="span"].pull-right{float:right}.container{margin-right:auto;margin-left:auto;*zoom:1}.container:before,.container:after{display:table;line-height:0;content:""}.container:after{clear:both}.container-fluid{padding-right:20px;padding-left:20px;*zoom:1}.container-fluid:before,.container-fluid:after{display:table;line-height:0;content:""}.container-fluid:after{clear:both}p{margin:0 0 10px}.lead{margin-bottom:20px;font-size:21px;font-weight:200;line-height:30px}small{font-size:85%}strong{font-weight:bold}em{font-style:italic}cite{font-style:normal}.muted{color:#999}a.muted:hover,a.muted:focus{color:#808080}.text-warning{color:#c09853}a.text-warning:hover,a.text-warning:focus{color:#a47e3c}.text-error{color:#b94a48}a.text-error:hover,a.text-error:focus{color:#953b39}.text-info{color:#3a87ad}a.text-info:hover,a.text-info:focus{color:#2d6987}.text-success{color:#468847}a.text-success:hover,a.text-success:focus{color:#356635}.text-left{text-align:left}.text-right{text-align:right}.text-center{text-align:center}h1,h2,h3,h4,h5,h6{margin:10px 0;font-family:inherit;font-weight:bold;line-height:20px;color:inherit;text-rendering:optimizelegibility}h1 small,h2 small,h3 small,h4 small,h5 small,h6 small{font-weight:normal;line-height:1;color:#999}h1,h2,h3{line-height:40px}h1{font-size:38.5px}h2{font-size:31.5px}h3{font-size:24.5px}h4{font-size:17.5px}h5{font-size:14px}h6{font-size:11.9px}h1 small{font-size:24.5px}h2 small{font-size:17.5px}h3 small{font-size:14px}h4 small{font-size:14px}.page-header{padding-bottom:9px;margin:20px 0 30px;border-bottom:1px solid #eee}ul,ol{padding:0;margin:0 0 10px 25px}ul ul,ul ol,ol ol,ol ul{margin-bottom:0}li{line-height:20px}ul.unstyled,ol.unstyled{margin-left:0;list-style:none}ul.inline,ol.inline{margin-left:0;list-style:none}ul.inline>li,ol.inline>li{display:inline-block;*display:inline;padding-right:5px;padding-left:5px;*zoom:1}dl{margin-bottom:20px}dt,dd{line-height:20px}dt{font-weight:bold}dd{margin-left:10px}.dl-horizontal{*zoom:1}.dl-horizontal:before,.dl-horizontal:after{display:table;line-height:0;content:""}.dl-horizontal:after{clear:both}.dl-horizontal dt{float:left;width:160px;overflow:hidden;clear:left;text-align:right;text-overflow:ellipsis;white-space:nowrap}.dl-horizontal dd{margin-left:180px}hr{margin:20px 0;border:0;border-top:1px solid #eee;border-bottom:1px solid #fff}abbr[title],abbr[data-original-title]{cursor:help;border-bottom:1px dotted #999}abbr.initialism{font-size:90%;text-transform:uppercase}blockquote{padding:0 0 0 15px;margin:0 0 20px;border-left:5px solid #eee}blockquote p{margin-bottom:0;font-size:17.5px;font-weight:300;line-height:1.25}blockquote small{display:block;line-height:20px;color:#999}blockquote small:before{content:'\2014 \00A0'}blockquote.pull-right{float:right;padding-right:15px;padding-left:0;border-right:5px solid #eee;border-left:0}blockquote.pull-right p,blockquote.pull-right small{text-align:right}blockquote.pull-right small:before{content:''}blockquote.pull-right small:after{content:'\00A0 \2014'}q:before,q:after,blockquote:before,blockquote:after{content:""}address{display:block;margin-bottom:20px;font-style:normal;line-height:20px}code,pre{padding:0 3px 2px;font-family:Monaco,Menlo,Consolas,"Courier New",monospace;font-size:12px;color:#333;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px}code{padding:2px 4px;color:#d14;white-space:nowrap;background-color:#f7f7f9;border:1px solid #e1e1e8}pre{display:block;padding:9.5px;margin:0 0 10px;font-size:13px;line-height:20px;word-break:break-all;word-wrap:break-word;white-space:pre;white-space:pre-wrap;background-color:#f5f5f5;border:1px solid #ccc;border:1px solid rgba(0,0,0,0.15);-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px}pre.prettyprint{margin-bottom:20px}pre code{padding:0;color:inherit;white-space:pre;white-space:pre-wrap;background-color:transparent;border:0}.pre-scrollable{max-height:340px;overflow-y:scroll}form{margin:0 0 20px}fieldset{padding:0;margin:0;border:0}legend{display:block;width:100%;padding:0;margin-bottom:20px;font-size:21px;line-height:40px;color:#333;border:0;border-bottom:1px solid #e5e5e5}legend small{font-size:15px;color:#999}label,input,button,select,textarea{font-size:14px;font-weight:normal;line-height:20px}input,button,select,textarea{font-family:"Helvetica Neue",Helvetica,Arial,sans-serif}label{display:block;margin-bottom:5px}select,textarea,input[type="text"],input[type="password"],input[type="datetime"],input[type="datetime-local"],input[type="date"],input[type="month"],input[type="time"],input[type="week"],input[type="number"],input[type="email"],input[type="url"],input[type="search"],input[type="tel"],input[type="color"],.uneditable-input{display:inline-block;height:20px;padding:4px 6px;margin-bottom:10px;font-size:14px;line-height:20px;color:#555;vertical-align:middle;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px}input,textarea,.uneditable-input{width:206px}textarea{height:auto}textarea,input[type="text"],input[type="password"],input[type="datetime"],input[type="datetime-local"],input[type="date"],input[type="month"],input[type="time"],input[type="week"],input[type="number"],input[type="email"],input[type="url"],input[type="search"],input[type="tel"],input[type="color"],.uneditable-input{background-color:#fff;border:1px solid #ccc;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);-webkit-transition:border linear .2s,box-shadow linear .2s;-moz-transition:border linear .2s,box-shadow linear .2s;-o-transition:border linear .2s,box-shadow linear .2s;transition:border linear .2s,box-shadow linear .2s}textarea:focus,input[type="text"]:focus,input[type="password"]:focus,input[type="datetime"]:focus,input[type="datetime-local"]:focus,input[type="date"]:focus,input[type="month"]:focus,input[type="time"]:focus,input[type="week"]:focus,input[type="number"]:focus,input[type="email"]:focus,input[type="url"]:focus,input[type="search"]:focus,input[type="tel"]:focus,input[type="color"]:focus,.uneditable-input:focus{border-color:rgba(82,168,236,0.8);outline:0;outline:thin dotted \9;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 8px rgba(82,168,236,0.6);-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 8px rgba(82,168,236,0.6);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 8px rgba(82,168,236,0.6)}input[type="radio"],input[type="checkbox"]{margin:4px 0 0;margin-top:1px \9;*margin-top:0;line-height:normal}input[type="file"],input[type="image"],input[type="submit"],input[type="reset"],input[type="button"],input[type="radio"],input[type="checkbox"]{width:auto}select,input[type="file"]{height:30px;*margin-top:4px;line-height:30px}select{width:220px;background-color:#fff;border:1px solid #ccc}select[multiple],select[size]{height:auto}select:focus,input[type="file"]:focus,input[type="radio"]:focus,input[type="checkbox"]:focus{outline:thin dotted #333;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}.uneditable-input,.uneditable-textarea{color:#999;cursor:not-allowed;background-color:#fcfcfc;border-color:#ccc;-webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,0.025);-moz-box-shadow:inset 0 1px 2px rgba(0,0,0,0.025);box-shadow:inset 0 1px 2px rgba(0,0,0,0.025)}.uneditable-input{overflow:hidden;white-space:nowrap}.uneditable-textarea{width:auto;height:auto}input:-moz-placeholder,textarea:-moz-placeholder{color:#999}input:-ms-input-placeholder,textarea:-ms-input-placeholder{color:#999}input::-webkit-input-placeholder,textarea::-webkit-input-placeholder{color:#999}.radio,.checkbox{min-height:20px;padding-left:20px}.radio input[type="radio"],.checkbox input[type="checkbox"]{float:left;margin-left:-20px}.controls>.radio:first-child,.controls>.checkbox:first-child{padding-top:5px}.radio.inline,.checkbox.inline{display:inline-block;padding-top:5px;margin-bottom:0;vertical-align:middle}.radio.inline+.radio.inline,.checkbox.inline+.checkbox.inline{margin-left:10px}.input-mini{width:60px}.input-small{width:90px}.input-medium{width:150px}.input-large{width:210px}.input-xlarge{width:270px}.input-xxlarge{width:530px}input[class*="span"],select[class*="span"],textarea[class*="span"],.uneditable-input[class*="span"],.row-fluid input[class*="span"],.row-fluid select[class*="span"],.row-fluid textarea[class*="span"],.row-fluid .uneditable-input[class*="span"]{float:none;margin-left:0}.input-append input[class*="span"],.input-append .uneditable-input[class*="span"],.input-prepend input[class*="span"],.input-prepend .uneditable-input[class*="span"],.row-fluid input[class*="span"],.row-fluid select[class*="span"],.row-fluid textarea[class*="span"],.row-fluid .uneditable-input[class*="span"],.row-fluid .input-prepend [class*="span"],.row-fluid .input-append [class*="span"]{display:inline-block}input,textarea,.uneditable-input{margin-left:0}.controls-row [class*="span"]+[class*="span"]{margin-left:20px}input.span12,textarea.span12,.uneditable-input.span12{width:926px}input.span11,textarea.span11,.uneditable-input.span11{width:846px}input.span10,textarea.span10,.uneditable-input.span10{width:766px}input.span9,textarea.span9,.uneditable-input.span9{width:686px}input.span8,textarea.span8,.uneditable-input.span8{width:606px}input.span7,textarea.span7,.uneditable-input.span7{width:526px}input.span6,textarea.span6,.uneditable-input.span6{width:446px}input.span5,textarea.span5,.uneditable-input.span5{width:366px}input.span4,textarea.span4,.uneditable-input.span4{width:286px}input.span3,textarea.span3,.uneditable-input.span3{width:206px}input.span2,textarea.span2,.uneditable-input.span2{width:126px}input.span1,textarea.span1,.uneditable-input.span1{width:46px}.controls-row{*zoom:1}.controls-row:before,.controls-row:after{display:table;line-height:0;content:""}.controls-row:after{clear:both}.controls-row [class*="span"],.row-fluid .controls-row [class*="span"]{float:left}.controls-row .checkbox[class*="span"],.controls-row .radio[class*="span"]{padding-top:5px}input[disabled],select[disabled],textarea[disabled],input[readonly],select[readonly],textarea[readonly]{cursor:not-allowed;background-color:#eee}input[type="radio"][disabled],input[type="checkbox"][disabled],input[type="radio"][readonly],input[type="checkbox"][readonly]{background-color:transparent}.control-group.warning .control-label,.control-group.warning .help-block,.control-group.warning .help-inline{color:#c09853}.control-group.warning .checkbox,.control-group.warning .radio,.control-group.warning input,.control-group.warning select,.control-group.warning textarea{color:#c09853}.control-group.warning input,.control-group.warning select,.control-group.warning textarea{border-color:#c09853;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075)}.control-group.warning input:focus,.control-group.warning select:focus,.control-group.warning textarea:focus{border-color:#a47e3c;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #dbc59e;-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #dbc59e;box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #dbc59e}.control-group.warning .input-prepend .add-on,.control-group.warning .input-append .add-on{color:#c09853;background-color:#fcf8e3;border-color:#c09853}.control-group.error .control-label,.control-group.error .help-block,.control-group.error .help-inline{color:#b94a48}.control-group.error .checkbox,.control-group.error .radio,.control-group.error input,.control-group.error select,.control-group.error textarea{color:#b94a48}.control-group.error input,.control-group.error select,.control-group.error textarea{border-color:#b94a48;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075)}.control-group.error input:focus,.control-group.error select:focus,.control-group.error textarea:focus{border-color:#953b39;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #d59392;-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #d59392;box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #d59392}.control-group.error .input-prepend .add-on,.control-group.error .input-append .add-on{color:#b94a48;background-color:#f2dede;border-color:#b94a48}.control-group.success .control-label,.control-group.success .help-block,.control-group.success .help-inline{color:#468847}.control-group.success .checkbox,.control-group.success .radio,.control-group.success input,.control-group.success select,.control-group.success textarea{color:#468847}.control-group.success input,.control-group.success select,.control-group.success textarea{border-color:#468847;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075)}.control-group.success input:focus,.control-group.success select:focus,.control-group.success textarea:focus{border-color:#356635;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #7aba7b;-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #7aba7b;box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #7aba7b}.control-group.success .input-prepend .add-on,.control-group.success .input-append .add-on{color:#468847;background-color:#dff0d8;border-color:#468847}.control-group.info .control-label,.control-group.info .help-block,.control-group.info .help-inline{color:#3a87ad}.control-group.info .checkbox,.control-group.info .radio,.control-group.info input,.control-group.info select,.control-group.info textarea{color:#3a87ad}.control-group.info input,.control-group.info select,.control-group.info textarea{border-color:#3a87ad;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075)}.control-group.info input:focus,.control-group.info select:focus,.control-group.info textarea:focus{border-color:#2d6987;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #7ab5d3;-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #7ab5d3;box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #7ab5d3}.control-group.info .input-prepend .add-on,.control-group.info .input-append .add-on{color:#3a87ad;background-color:#d9edf7;border-color:#3a87ad}input:focus:invalid,textarea:focus:invalid,select:focus:invalid{color:#b94a48;border-color:#ee5f5b}input:focus:invalid:focus,textarea:focus:invalid:focus,select:focus:invalid:focus{border-color:#e9322d;-webkit-box-shadow:0 0 6px #f8b9b7;-moz-box-shadow:0 0 6px #f8b9b7;box-shadow:0 0 6px #f8b9b7}.form-actions{padding:19px 20px 20px;margin-top:20px;margin-bottom:20px;background-color:#f5f5f5;border-top:1px solid #e5e5e5;*zoom:1}.form-actions:before,.form-actions:after{display:table;line-height:0;content:""}.form-actions:after{clear:both}.help-block,.help-inline{color:#595959}.help-block{display:block;margin-bottom:10px}.help-inline{display:inline-block;*display:inline;padding-left:5px;vertical-align:middle;*zoom:1}.input-append,.input-prepend{display:inline-block;margin-bottom:10px;font-size:0;white-space:nowrap;vertical-align:middle}.input-append input,.input-prepend input,.input-append select,.input-prepend select,.input-append .uneditable-input,.input-prepend .uneditable-input,.input-append .dropdown-menu,.input-prepend .dropdown-menu,.input-append .popover,.input-prepend .popover{font-size:14px}.input-append input,.input-prepend input,.input-append select,.input-prepend select,.input-append .uneditable-input,.input-prepend .uneditable-input{position:relative;margin-bottom:0;*margin-left:0;vertical-align:top;-webkit-border-radius:0 4px 4px 0;-moz-border-radius:0 4px 4px 0;border-radius:0 4px 4px 0}.input-append input:focus,.input-prepend input:focus,.input-append select:focus,.input-prepend select:focus,.input-append .uneditable-input:focus,.input-prepend .uneditable-input:focus{z-index:2}.input-append .add-on,.input-prepend .add-on{display:inline-block;width:auto;height:20px;min-width:16px;padding:4px 5px;font-size:14px;font-weight:normal;line-height:20px;text-align:center;text-shadow:0 1px 0 #fff;background-color:#eee;border:1px solid #ccc}.input-append .add-on,.input-prepend .add-on,.input-append .btn,.input-prepend .btn,.input-append .btn-group>.dropdown-toggle,.input-prepend .btn-group>.dropdown-toggle{vertical-align:top;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.input-append .active,.input-prepend .active{background-color:#a9dba9;border-color:#46a546}.input-prepend .add-on,.input-prepend .btn{margin-right:-1px}.input-prepend .add-on:first-child,.input-prepend .btn:first-child{-webkit-border-radius:4px 0 0 4px;-moz-border-radius:4px 0 0 4px;border-radius:4px 0 0 4px}.input-append input,.input-append select,.input-append .uneditable-input{-webkit-border-radius:4px 0 0 4px;-moz-border-radius:4px 0 0 4px;border-radius:4px 0 0 4px}.input-append input+.btn-group .btn:last-child,.input-append select+.btn-group .btn:last-child,.input-append .uneditable-input+.btn-group .btn:last-child{-webkit-border-radius:0 4px 4px 0;-moz-border-radius:0 4px 4px 0;border-radius:0 4px 4px 0}.input-append .add-on,.input-append .btn,.input-append .btn-group{margin-left:-1px}.input-append .add-on:last-child,.input-append .btn:last-child,.input-append .btn-group:last-child>.dropdown-toggle{-webkit-border-radius:0 4px 4px 0;-moz-border-radius:0 4px 4px 0;border-radius:0 4px 4px 0}.input-prepend.input-append input,.input-prepend.input-append select,.input-prepend.input-append .uneditable-input{-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.input-prepend.input-append input+.btn-group .btn,.input-prepend.input-append select+.btn-group .btn,.input-prepend.input-append .uneditable-input+.btn-group .btn{-webkit-border-radius:0 4px 4px 0;-moz-border-radius:0 4px 4px 0;border-radius:0 4px 4px 0}.input-prepend.input-append .add-on:first-child,.input-prepend.input-append .btn:first-child{margin-right:-1px;-webkit-border-radius:4px 0 0 4px;-moz-border-radius:4px 0 0 4px;border-radius:4px 0 0 4px}.input-prepend.input-append .add-on:last-child,.input-prepend.input-append .btn:last-child{margin-left:-1px;-webkit-border-radius:0 4px 4px 0;-moz-border-radius:0 4px 4px 0;border-radius:0 4px 4px 0}.input-prepend.input-append .btn-group:first-child{margin-left:0}input.search-query{padding-right:14px;padding-right:4px \9;padding-left:14px;padding-left:4px \9;margin-bottom:0;-webkit-border-radius:15px;-moz-border-radius:15px;border-radius:15px}.form-search .input-append .search-query,.form-search .input-prepend .search-query{-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.form-search .input-append .search-query{-webkit-border-radius:14px 0 0 14px;-moz-border-radius:14px 0 0 14px;border-radius:14px 0 0 14px}.form-search .input-append .btn{-webkit-border-radius:0 14px 14px 0;-moz-border-radius:0 14px 14px 0;border-radius:0 14px 14px 0}.form-search .input-prepend .search-query{-webkit-border-radius:0 14px 14px 0;-moz-border-radius:0 14px 14px 0;border-radius:0 14px 14px 0}.form-search .input-prepend .btn{-webkit-border-radius:14px 0 0 14px;-moz-border-radius:14px 0 0 14px;border-radius:14px 0 0 14px}.form-search input,.form-inline input,.form-horizontal input,.form-search textarea,.form-inline textarea,.form-horizontal textarea,.form-search select,.form-inline select,.form-horizontal select,.form-search .help-inline,.form-inline .help-inline,.form-horizontal .help-inline,.form-search .uneditable-input,.form-inline .uneditable-input,.form-horizontal .uneditable-input,.form-search .input-prepend,.form-inline .input-prepend,.form-horizontal .input-prepend,.form-search .input-append,.form-inline .input-append,.form-horizontal .input-append{display:inline-block;*display:inline;margin-bottom:0;vertical-align:middle;*zoom:1}.form-search .hide,.form-inline .hide,.form-horizontal .hide{display:none}.form-search label,.form-inline label,.form-search .btn-group,.form-inline .btn-group{display:inline-block}.form-search .input-append,.form-inline .input-append,.form-search .input-prepend,.form-inline .input-prepend{margin-bottom:0}.form-search .radio,.form-search .checkbox,.form-inline .radio,.form-inline .checkbox{padding-left:0;margin-bottom:0;vertical-align:middle}.form-search .radio input[type="radio"],.form-search .checkbox input[type="checkbox"],.form-inline .radio input[type="radio"],.form-inline .checkbox input[type="checkbox"]{float:left;margin-right:3px;margin-left:0}.control-group{margin-bottom:10px}legend+.control-group{margin-top:20px;-webkit-margin-top-collapse:separate}.form-horizontal .control-group{margin-bottom:20px;*zoom:1}.form-horizontal .control-group:before,.form-horizontal .control-group:after{display:table;line-height:0;content:""}.form-horizontal .control-group:after{clear:both}.form-horizontal .control-label{float:left;width:160px;padding-top:5px;text-align:right}.form-horizontal .controls{*display:inline-block;*padding-left:20px;margin-left:180px;*margin-left:0}.form-horizontal .controls:first-child{*padding-left:180px}.form-horizontal .help-block{margin-bottom:0}.form-horizontal input+.help-block,.form-horizontal select+.help-block,.form-horizontal textarea+.help-block,.form-horizontal .uneditable-input+.help-block,.form-horizontal .input-prepend+.help-block,.form-horizontal .input-append+.help-block{margin-top:10px}.form-horizontal .form-actions{padding-left:180px}table{max-width:100%;background-color:transparent;border-collapse:collapse;border-spacing:0}.table{width:100%;margin-bottom:20px}.table th,.table td{padding:8px;line-height:20px;text-align:left;vertical-align:top;border-top:1px solid #ddd}.table th{font-weight:bold}.table thead th{vertical-align:bottom}.table caption+thead tr:first-child th,.table caption+thead tr:first-child td,.table colgroup+thead tr:first-child th,.table colgroup+thead tr:first-child td,.table thead:first-child tr:first-child th,.table thead:first-child tr:first-child td{border-top:0}.table tbody+tbody{border-top:2px solid #ddd}.table .table{background-color:#fff}.table-condensed th,.table-condensed td{padding:4px 5px}.table-bordered{border:1px solid #ddd;border-collapse:separate;*border-collapse:collapse;border-left:0;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px}.table-bordered th,.table-bordered td{border-left:1px solid #ddd}.table-bordered caption+thead tr:first-child th,.table-bordered caption+tbody tr:first-child th,.table-bordered caption+tbody tr:first-child td,.table-bordered colgroup+thead tr:first-child th,.table-bordered colgroup+tbody tr:first-child th,.table-bordered colgroup+tbody tr:first-child td,.table-bordered thead:first-child tr:first-child th,.table-bordered tbody:first-child tr:first-child th,.table-bordered tbody:first-child tr:first-child td{border-top:0}.table-bordered thead:first-child tr:first-child>th:first-child,.table-bordered tbody:first-child tr:first-child>td:first-child,.table-bordered tbody:first-child tr:first-child>th:first-child{-webkit-border-top-left-radius:4px;border-top-left-radius:4px;-moz-border-radius-topleft:4px}.table-bordered thead:first-child tr:first-child>th:last-child,.table-bordered tbody:first-child tr:first-child>td:last-child,.table-bordered tbody:first-child tr:first-child>th:last-child{-webkit-border-top-right-radius:4px;border-top-right-radius:4px;-moz-border-radius-topright:4px}.table-bordered thead:last-child tr:last-child>th:first-child,.table-bordered tbody:last-child tr:last-child>td:first-child,.table-bordered tbody:last-child tr:last-child>th:first-child,.table-bordered tfoot:last-child tr:last-child>td:first-child,.table-bordered tfoot:last-child tr:last-child>th:first-child{-webkit-border-bottom-left-radius:4px;border-bottom-left-radius:4px;-moz-border-radius-bottomleft:4px}.table-bordered thead:last-child tr:last-child>th:last-child,.table-bordered tbody:last-child tr:last-child>td:last-child,.table-bordered tbody:last-child tr:last-child>th:last-child,.table-bordered tfoot:last-child tr:last-child>td:last-child,.table-bordered tfoot:last-child tr:last-child>th:last-child{-webkit-border-bottom-right-radius:4px;border-bottom-right-radius:4px;-moz-border-radius-bottomright:4px}.table-bordered tfoot+tbody:last-child tr:last-child td:first-child{-webkit-border-bottom-left-radius:0;border-bottom-left-radius:0;-moz-border-radius-bottomleft:0}.table-bordered tfoot+tbody:last-child tr:last-child td:last-child{-webkit-border-bottom-right-radius:0;border-bottom-right-radius:0;-moz-border-radius-bottomright:0}.table-bordered caption+thead tr:first-child th:first-child,.table-bordered caption+tbody tr:first-child td:first-child,.table-bordered colgroup+thead tr:first-child th:first-child,.table-bordered colgroup+tbody tr:first-child td:first-child{-webkit-border-top-left-radius:4px;border-top-left-radius:4px;-moz-border-radius-topleft:4px}.table-bordered caption+thead tr:first-child th:last-child,.table-bordered caption+tbody tr:first-child td:last-child,.table-bordered colgroup+thead tr:first-child th:last-child,.table-bordered colgroup+tbody tr:first-child td:last-child{-webkit-border-top-right-radius:4px;border-top-right-radius:4px;-moz-border-radius-topright:4px}.table-striped tbody>tr:nth-child(odd)>td,.table-striped tbody>tr:nth-child(odd)>th{background-color:#f9f9f9}.table-hover tbody tr:hover>td,.table-hover tbody tr:hover>th{background-color:#f5f5f5}table td[class*="span"],table th[class*="span"],.row-fluid table td[class*="span"],.row-fluid table th[class*="span"]{display:table-cell;float:none;margin-left:0}.table td.span1,.table th.span1{float:none;width:44px;margin-left:0}.table td.span2,.table th.span2{float:none;width:124px;margin-left:0}.table td.span3,.table th.span3{float:none;width:204px;margin-left:0}.table td.span4,.table th.span4{float:none;width:284px;margin-left:0}.table td.span5,.table th.span5{float:none;width:364px;margin-left:0}.table td.span6,.table th.span6{float:none;width:444px;margin-left:0}.table td.span7,.table th.span7{float:none;width:524px;margin-left:0}.table td.span8,.table th.span8{float:none;width:604px;margin-left:0}.table td.span9,.table th.span9{float:none;width:684px;margin-left:0}.table td.span10,.table th.span10{float:none;width:764px;margin-left:0}.table td.span11,.table th.span11{float:none;width:844px;margin-left:0}.table td.span12,.table th.span12{float:none;width:924px;margin-left:0}.table tbody tr.success>td{background-color:#dff0d8}.table tbody tr.error>td{background-color:#f2dede}.table tbody tr.warning>td{background-color:#fcf8e3}.table tbody tr.info>td{background-color:#d9edf7}.table-hover tbody tr.success:hover>td{background-color:#d0e9c6}.table-hover tbody tr.error:hover>td{background-color:#ebcccc}.table-hover tbody tr.warning:hover>td{background-color:#faf2cc}.table-hover tbody tr.info:hover>td{background-color:#c4e3f3}[class^="icon-"],[class*=" icon-"]{display:inline-block;width:14px;height:14px;margin-top:1px;*margin-right:.3em;line-height:14px;vertical-align:text-top;background-image:url("../img/glyphicons-halflings.png");background-position:14px 14px;background-repeat:no-repeat}.icon-white,.nav-pills>.active>a>[class^="icon-"],.nav-pills>.active>a>[class*=" icon-"],.nav-list>.active>a>[class^="icon-"],.nav-list>.active>a>[class*=" icon-"],.navbar-inverse .nav>.active>a>[class^="icon-"],.navbar-inverse .nav>.active>a>[class*=" icon-"],.dropdown-menu>li>a:hover>[class^="icon-"],.dropdown-menu>li>a:focus>[class^="icon-"],.dropdown-menu>li>a:hover>[class*=" icon-"],.dropdown-menu>li>a:focus>[class*=" icon-"],.dropdown-menu>.active>a>[class^="icon-"],.dropdown-menu>.active>a>[class*=" icon-"],.dropdown-submenu:hover>a>[class^="icon-"],.dropdown-submenu:focus>a>[class^="icon-"],.dropdown-submenu:hover>a>[class*=" icon-"],.dropdown-submenu:focus>a>[class*=" icon-"]{background-image:url("../img/glyphicons-halflings-white.png")}.icon-glass{background-position:0 0}.icon-music{background-position:-24px 0}.icon-search{background-position:-48px 0}.icon-envelope{background-position:-72px 0}.icon-heart{background-position:-96px 0}.icon-star{background-position:-120px 0}.icon-star-empty{background-position:-144px 0}.icon-user{background-position:-168px 0}.icon-film{background-position:-192px 0}.icon-th-large{background-position:-216px 0}.icon-th{background-position:-240px 0}.icon-th-list{background-position:-264px 0}.icon-ok{background-position:-288px 0}.icon-remove{background-position:-312px 0}.icon-zoom-in{background-position:-336px 0}.icon-zoom-out{background-position:-360px 0}.icon-off{background-position:-384px 0}.icon-signal{background-position:-408px 0}.icon-cog{background-position:-432px 0}.icon-trash{background-position:-456px 0}.icon-home{background-position:0 -24px}.icon-file{background-position:-24px -24px}.icon-time{background-position:-48px -24px}.icon-road{background-position:-72px -24px}.icon-download-alt{background-position:-96px -24px}.icon-download{background-position:-120px -24px}.icon-upload{background-position:-144px -24px}.icon-inbox{background-position:-168px -24px}.icon-play-circle{background-position:-192px -24px}.icon-repeat{background-position:-216px -24px}.icon-refresh{background-position:-240px -24px}.icon-list-alt{background-position:-264px -24px}.icon-lock{background-position:-287px -24px}.icon-flag{background-position:-312px -24px}.icon-headphones{background-position:-336px -24px}.icon-volume-off{background-position:-360px -24px}.icon-volume-down{background-position:-384px -24px}.icon-volume-up{background-position:-408px -24px}.icon-qrcode{background-position:-432px -24px}.icon-barcode{background-position:-456px -24px}.icon-tag{background-position:0 -48px}.icon-tags{background-position:-25px -48px}.icon-book{background-position:-48px -48px}.icon-bookmark{background-position:-72px -48px}.icon-print{background-position:-96px -48px}.icon-camera{background-position:-120px -48px}.icon-font{background-position:-144px -48px}.icon-bold{background-position:-167px -48px}.icon-italic{background-position:-192px -48px}.icon-text-height{background-position:-216px -48px}.icon-text-width{background-position:-240px -48px}.icon-align-left{background-position:-264px -48px}.icon-align-center{background-position:-288px -48px}.icon-align-right{background-position:-312px -48px}.icon-align-justify{background-position:-336px -48px}.icon-list{background-position:-360px -48px}.icon-indent-left{background-position:-384px -48px}.icon-indent-right{background-position:-408px -48px}.icon-facetime-video{background-position:-432px -48px}.icon-picture{background-position:-456px -48px}.icon-pencil{background-position:0 -72px}.icon-map-marker{background-position:-24px -72px}.icon-adjust{background-position:-48px -72px}.icon-tint{background-position:-72px -72px}.icon-edit{background-position:-96px -72px}.icon-share{background-position:-120px -72px}.icon-check{background-position:-144px -72px}.icon-move{background-position:-168px -72px}.icon-step-backward{background-position:-192px -72px}.icon-fast-backward{background-position:-216px -72px}.icon-backward{background-position:-240px -72px}.icon-play{background-position:-264px -72px}.icon-pause{background-position:-288px -72px}.icon-stop{background-position:-312px -72px}.icon-forward{background-position:-336px -72px}.icon-fast-forward{background-position:-360px -72px}.icon-step-forward{background-position:-384px -72px}.icon-eject{background-position:-408px -72px}.icon-chevron-left{background-position:-432px -72px}.icon-chevron-right{background-position:-456px -72px}.icon-plus-sign{background-position:0 -96px}.icon-minus-sign{background-position:-24px -96px}.icon-remove-sign{background-position:-48px -96px}.icon-ok-sign{background-position:-72px -96px}.icon-question-sign{background-position:-96px -96px}.icon-info-sign{background-position:-120px -96px}.icon-screenshot{background-position:-144px -96px}.icon-remove-circle{background-position:-168px -96px}.icon-ok-circle{background-position:-192px -96px}.icon-ban-circle{background-position:-216px -96px}.icon-arrow-left{background-position:-240px -96px}.icon-arrow-right{background-position:-264px -96px}.icon-arrow-up{background-position:-289px -96px}.icon-arrow-down{background-position:-312px -96px}.icon-share-alt{background-position:-336px -96px}.icon-resize-full{background-position:-360px -96px}.icon-resize-small{background-position:-384px -96px}.icon-plus{background-position:-408px -96px}.icon-minus{background-position:-433px -96px}.icon-asterisk{background-position:-456px -96px}.icon-exclamation-sign{background-position:0 -120px}.icon-gift{background-position:-24px -120px}.icon-leaf{background-position:-48px -120px}.icon-fire{background-position:-72px -120px}.icon-eye-open{background-position:-96px -120px}.icon-eye-close{background-position:-120px -120px}.icon-warning-sign{background-position:-144px -120px}.icon-plane{background-position:-168px -120px}.icon-calendar{background-position:-192px -120px}.icon-random{width:16px;background-position:-216px -120px}.icon-comment{background-position:-240px -120px}.icon-magnet{background-position:-264px -120px}.icon-chevron-up{background-position:-288px -120px}.icon-chevron-down{background-position:-313px -119px}.icon-retweet{background-position:-336px -120px}.icon-shopping-cart{background-position:-360px -120px}.icon-folder-close{width:16px;background-position:-384px -120px}.icon-folder-open{width:16px;background-position:-408px -120px}.icon-resize-vertical{background-position:-432px -119px}.icon-resize-horizontal{background-position:-456px -118px}.icon-hdd{background-position:0 -144px}.icon-bullhorn{background-position:-24px -144px}.icon-bell{background-position:-48px -144px}.icon-certificate{background-position:-72px -144px}.icon-thumbs-up{background-position:-96px -144px}.icon-thumbs-down{background-position:-120px -144px}.icon-hand-right{background-position:-144px -144px}.icon-hand-left{background-position:-168px -144px}.icon-hand-up{background-position:-192px -144px}.icon-hand-down{background-position:-216px -144px}.icon-circle-arrow-right{background-position:-240px -144px}.icon-circle-arrow-left{background-position:-264px -144px}.icon-circle-arrow-up{background-position:-288px -144px}.icon-circle-arrow-down{background-position:-312px -144px}.icon-globe{background-position:-336px -144px}.icon-wrench{background-position:-360px -144px}.icon-tasks{background-position:-384px -144px}.icon-filter{background-position:-408px -144px}.icon-briefcase{background-position:-432px -144px}.icon-fullscreen{background-position:-456px -144px}.dropup,.dropdown{position:relative}.dropdown-toggle{*margin-bottom:-3px}.dropdown-toggle:active,.open .dropdown-toggle{outline:0}.caret{display:inline-block;width:0;height:0;vertical-align:top;border-top:4px solid #000;border-right:4px solid transparent;border-left:4px solid transparent;content:""}.dropdown .caret{margin-top:8px;margin-left:2px}.dropdown-menu{position:absolute;top:100%;left:0;z-index:1000;display:none;float:left;min-width:160px;padding:5px 0;margin:2px 0 0;list-style:none;background-color:#fff;border:1px solid #ccc;border:1px solid rgba(0,0,0,0.2);*border-right-width:2px;*border-bottom-width:2px;-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px;-webkit-box-shadow:0 5px 10px rgba(0,0,0,0.2);-moz-box-shadow:0 5px 10px rgba(0,0,0,0.2);box-shadow:0 5px 10px rgba(0,0,0,0.2);-webkit-background-clip:padding-box;-moz-background-clip:padding;background-clip:padding-box}.dropdown-menu.pull-right{right:0;left:auto}.dropdown-menu .divider{*width:100%;height:1px;margin:9px 1px;*margin:-5px 0 5px;overflow:hidden;background-color:#e5e5e5;border-bottom:1px solid #fff}.dropdown-menu>li>a{display:block;padding:3px 20px;clear:both;font-weight:normal;line-height:20px;color:#333;white-space:nowrap}.dropdown-menu>li>a:hover,.dropdown-menu>li>a:focus,.dropdown-submenu:hover>a,.dropdown-submenu:focus>a{color:#fff;text-decoration:none;background-color:#0081c2;background-image:-moz-linear-gradient(top,#08c,#0077b3);background-image:-webkit-gradient(linear,0 0,0 100%,from(#08c),to(#0077b3));background-image:-webkit-linear-gradient(top,#08c,#0077b3);background-image:-o-linear-gradient(top,#08c,#0077b3);background-image:linear-gradient(to bottom,#08c,#0077b3);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff0088cc',endColorstr='#ff0077b3',GradientType=0)}.dropdown-menu>.active>a,.dropdown-menu>.active>a:hover,.dropdown-menu>.active>a:focus{color:#fff;text-decoration:none;background-color:#0081c2;background-image:-moz-linear-gradient(top,#08c,#0077b3);background-image:-webkit-gradient(linear,0 0,0 100%,from(#08c),to(#0077b3));background-image:-webkit-linear-gradient(top,#08c,#0077b3);background-image:-o-linear-gradient(top,#08c,#0077b3);background-image:linear-gradient(to bottom,#08c,#0077b3);background-repeat:repeat-x;outline:0;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff0088cc',endColorstr='#ff0077b3',GradientType=0)}.dropdown-menu>.disabled>a,.dropdown-menu>.disabled>a:hover,.dropdown-menu>.disabled>a:focus{color:#999}.dropdown-menu>.disabled>a:hover,.dropdown-menu>.disabled>a:focus{text-decoration:none;cursor:default;background-color:transparent;background-image:none;filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.open{*z-index:1000}.open>.dropdown-menu{display:block}.pull-right>.dropdown-menu{right:0;left:auto}.dropup .caret,.navbar-fixed-bottom .dropdown .caret{border-top:0;border-bottom:4px solid #000;content:""}.dropup .dropdown-menu,.navbar-fixed-bottom .dropdown .dropdown-menu{top:auto;bottom:100%;margin-bottom:1px}.dropdown-submenu{position:relative}.dropdown-submenu>.dropdown-menu{top:0;left:100%;margin-top:-6px;margin-left:-1px;-webkit-border-radius:0 6px 6px 6px;-moz-border-radius:0 6px 6px 6px;border-radius:0 6px 6px 6px}.dropdown-submenu:hover>.dropdown-menu{display:block}.dropup .dropdown-submenu>.dropdown-menu{top:auto;bottom:0;margin-top:0;margin-bottom:-2px;-webkit-border-radius:5px 5px 5px 0;-moz-border-radius:5px 5px 5px 0;border-radius:5px 5px 5px 0}.dropdown-submenu>a:after{display:block;float:right;width:0;height:0;margin-top:5px;margin-right:-10px;border-color:transparent;border-left-color:#ccc;border-style:solid;border-width:5px 0 5px 5px;content:" "}.dropdown-submenu:hover>a:after{border-left-color:#fff}.dropdown-submenu.pull-left{float:none}.dropdown-submenu.pull-left>.dropdown-menu{left:-100%;margin-left:10px;-webkit-border-radius:6px 0 6px 6px;-moz-border-radius:6px 0 6px 6px;border-radius:6px 0 6px 6px}.dropdown .dropdown-menu .nav-header{padding-right:20px;padding-left:20px}.typeahead{z-index:1051;margin-top:2px;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px}.well{min-height:20px;padding:19px;margin-bottom:20px;background-color:#f5f5f5;border:1px solid #e3e3e3;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.05);-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.05);box-shadow:inset 0 1px 1px rgba(0,0,0,0.05)}.well blockquote{border-color:#ddd;border-color:rgba(0,0,0,0.15)}.well-large{padding:24px;-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px}.well-small{padding:9px;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px}.fade{opacity:0;-webkit-transition:opacity .15s linear;-moz-transition:opacity .15s linear;-o-transition:opacity .15s linear;transition:opacity .15s linear}.fade.in{opacity:1}.collapse{position:relative;height:0;overflow:hidden;-webkit-transition:height .35s ease;-moz-transition:height .35s ease;-o-transition:height .35s ease;transition:height .35s ease}.collapse.in{height:auto}.close{float:right;font-size:20px;font-weight:bold;line-height:20px;color:#000;text-shadow:0 1px 0 #fff;opacity:.2;filter:alpha(opacity=20)}.close:hover,.close:focus{color:#000;text-decoration:none;cursor:pointer;opacity:.4;filter:alpha(opacity=40)}button.close{padding:0;cursor:pointer;background:transparent;border:0;-webkit-appearance:none}.btn{display:inline-block;*display:inline;padding:4px 12px;margin-bottom:0;*margin-left:.3em;font-size:14px;line-height:20px;color:#333;text-align:center;text-shadow:0 1px 1px rgba(255,255,255,0.75);vertical-align:middle;cursor:pointer;background-color:#f5f5f5;*background-color:#e6e6e6;background-image:-moz-linear-gradient(top,#fff,#e6e6e6);background-image:-webkit-gradient(linear,0 0,0 100%,from(#fff),to(#e6e6e6));background-image:-webkit-linear-gradient(top,#fff,#e6e6e6);background-image:-o-linear-gradient(top,#fff,#e6e6e6);background-image:linear-gradient(to bottom,#fff,#e6e6e6);background-repeat:repeat-x;border:1px solid #ccc;*border:0;border-color:#e6e6e6 #e6e6e6 #bfbfbf;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);border-bottom-color:#b3b3b3;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff',endColorstr='#ffe6e6e6',GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);*zoom:1;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,0.2),0 1px 2px rgba(0,0,0,0.05);-moz-box-shadow:inset 0 1px 0 rgba(255,255,255,0.2),0 1px 2px rgba(0,0,0,0.05);box-shadow:inset 0 1px 0 rgba(255,255,255,0.2),0 1px 2px rgba(0,0,0,0.05)}.btn:hover,.btn:focus,.btn:active,.btn.active,.btn.disabled,.btn[disabled]{color:#333;background-color:#e6e6e6;*background-color:#d9d9d9}.btn:active,.btn.active{background-color:#ccc \9}.btn:first-child{*margin-left:0}.btn:hover,.btn:focus{color:#333;text-decoration:none;background-position:0 -15px;-webkit-transition:background-position .1s linear;-moz-transition:background-position .1s linear;-o-transition:background-position .1s linear;transition:background-position .1s linear}.btn:focus{outline:thin dotted #333;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}.btn.active,.btn:active{background-image:none;outline:0;-webkit-box-shadow:inset 0 2px 4px rgba(0,0,0,0.15),0 1px 2px rgba(0,0,0,0.05);-moz-box-shadow:inset 0 2px 4px rgba(0,0,0,0.15),0 1px 2px rgba(0,0,0,0.05);box-shadow:inset 0 2px 4px rgba(0,0,0,0.15),0 1px 2px rgba(0,0,0,0.05)}.btn.disabled,.btn[disabled]{cursor:default;background-image:none;opacity:.65;filter:alpha(opacity=65);-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none}.btn-large{padding:11px 19px;font-size:17.5px;-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px}.btn-large [class^="icon-"],.btn-large [class*=" icon-"]{margin-top:4px}.btn-small{padding:2px 10px;font-size:11.9px;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px}.btn-small [class^="icon-"],.btn-small [class*=" icon-"]{margin-top:0}.btn-mini [class^="icon-"],.btn-mini [class*=" icon-"]{margin-top:-1px}.btn-mini{padding:0 6px;font-size:10.5px;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px}.btn-block{display:block;width:100%;padding-right:0;padding-left:0;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.btn-block+.btn-block{margin-top:5px}input[type="submit"].btn-block,input[type="reset"].btn-block,input[type="button"].btn-block{width:100%}.btn-primary.active,.btn-warning.active,.btn-danger.active,.btn-success.active,.btn-info.active,.btn-inverse.active{color:rgba(255,255,255,0.75)}.btn-primary{color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.25);background-color:#006dcc;*background-color:#04c;background-image:-moz-linear-gradient(top,#08c,#04c);background-image:-webkit-gradient(linear,0 0,0 100%,from(#08c),to(#04c));background-image:-webkit-linear-gradient(top,#08c,#04c);background-image:-o-linear-gradient(top,#08c,#04c);background-image:linear-gradient(to bottom,#08c,#04c);background-repeat:repeat-x;border-color:#04c #04c #002a80;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff0088cc',endColorstr='#ff0044cc',GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.btn-primary:hover,.btn-primary:focus,.btn-primary:active,.btn-primary.active,.btn-primary.disabled,.btn-primary[disabled]{color:#fff;background-color:#04c;*background-color:#003bb3}.btn-primary:active,.btn-primary.active{background-color:#039 \9}.btn-warning{color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.25);background-color:#faa732;*background-color:#f89406;background-image:-moz-linear-gradient(top,#fbb450,#f89406);background-image:-webkit-gradient(linear,0 0,0 100%,from(#fbb450),to(#f89406));background-image:-webkit-linear-gradient(top,#fbb450,#f89406);background-image:-o-linear-gradient(top,#fbb450,#f89406);background-image:linear-gradient(to bottom,#fbb450,#f89406);background-repeat:repeat-x;border-color:#f89406 #f89406 #ad6704;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffbb450',endColorstr='#fff89406',GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.btn-warning:hover,.btn-warning:focus,.btn-warning:active,.btn-warning.active,.btn-warning.disabled,.btn-warning[disabled]{color:#fff;background-color:#f89406;*background-color:#df8505}.btn-warning:active,.btn-warning.active{background-color:#c67605 \9}.btn-danger{color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.25);background-color:#da4f49;*background-color:#bd362f;background-image:-moz-linear-gradient(top,#ee5f5b,#bd362f);background-image:-webkit-gradient(linear,0 0,0 100%,from(#ee5f5b),to(#bd362f));background-image:-webkit-linear-gradient(top,#ee5f5b,#bd362f);background-image:-o-linear-gradient(top,#ee5f5b,#bd362f);background-image:linear-gradient(to bottom,#ee5f5b,#bd362f);background-repeat:repeat-x;border-color:#bd362f #bd362f #802420;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffee5f5b',endColorstr='#ffbd362f',GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.btn-danger:hover,.btn-danger:focus,.btn-danger:active,.btn-danger.active,.btn-danger.disabled,.btn-danger[disabled]{color:#fff;background-color:#bd362f;*background-color:#a9302a}.btn-danger:active,.btn-danger.active{background-color:#942a25 \9}.btn-success{color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.25);background-color:#5bb75b;*background-color:#51a351;background-image:-moz-linear-gradient(top,#62c462,#51a351);background-image:-webkit-gradient(linear,0 0,0 100%,from(#62c462),to(#51a351));background-image:-webkit-linear-gradient(top,#62c462,#51a351);background-image:-o-linear-gradient(top,#62c462,#51a351);background-image:linear-gradient(to bottom,#62c462,#51a351);background-repeat:repeat-x;border-color:#51a351 #51a351 #387038;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff62c462',endColorstr='#ff51a351',GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.btn-success:hover,.btn-success:focus,.btn-success:active,.btn-success.active,.btn-success.disabled,.btn-success[disabled]{color:#fff;background-color:#51a351;*background-color:#499249}.btn-success:active,.btn-success.active{background-color:#408140 \9}.btn-info{color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.25);background-color:#49afcd;*background-color:#2f96b4;background-image:-moz-linear-gradient(top,#5bc0de,#2f96b4);background-image:-webkit-gradient(linear,0 0,0 100%,from(#5bc0de),to(#2f96b4));background-image:-webkit-linear-gradient(top,#5bc0de,#2f96b4);background-image:-o-linear-gradient(top,#5bc0de,#2f96b4);background-image:linear-gradient(to bottom,#5bc0de,#2f96b4);background-repeat:repeat-x;border-color:#2f96b4 #2f96b4 #1f6377;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de',endColorstr='#ff2f96b4',GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.btn-info:hover,.btn-info:focus,.btn-info:active,.btn-info.active,.btn-info.disabled,.btn-info[disabled]{color:#fff;background-color:#2f96b4;*background-color:#2a85a0}.btn-info:active,.btn-info.active{background-color:#24748c \9}.btn-inverse{color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.25);background-color:#363636;*background-color:#222;background-image:-moz-linear-gradient(top,#444,#222);background-image:-webkit-gradient(linear,0 0,0 100%,from(#444),to(#222));background-image:-webkit-linear-gradient(top,#444,#222);background-image:-o-linear-gradient(top,#444,#222);background-image:linear-gradient(to bottom,#444,#222);background-repeat:repeat-x;border-color:#222 #222 #000;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff444444',endColorstr='#ff222222',GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.btn-inverse:hover,.btn-inverse:focus,.btn-inverse:active,.btn-inverse.active,.btn-inverse.disabled,.btn-inverse[disabled]{color:#fff;background-color:#222;*background-color:#151515}.btn-inverse:active,.btn-inverse.active{background-color:#080808 \9}button.btn,input[type="submit"].btn{*padding-top:3px;*padding-bottom:3px}button.btn::-moz-focus-inner,input[type="submit"].btn::-moz-focus-inner{padding:0;border:0}button.btn.btn-large,input[type="submit"].btn.btn-large{*padding-top:7px;*padding-bottom:7px}button.btn.btn-small,input[type="submit"].btn.btn-small{*padding-top:3px;*padding-bottom:3px}button.btn.btn-mini,input[type="submit"].btn.btn-mini{*padding-top:1px;*padding-bottom:1px}.btn-link,.btn-link:active,.btn-link[disabled]{background-color:transparent;background-image:none;-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none}.btn-link{color:#08c;cursor:pointer;border-color:transparent;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.btn-link:hover,.btn-link:focus{color:#005580;text-decoration:underline;background-color:transparent}.btn-link[disabled]:hover,.btn-link[disabled]:focus{color:#333;text-decoration:none}.btn-group{position:relative;display:inline-block;*display:inline;*margin-left:.3em;font-size:0;white-space:nowrap;vertical-align:middle;*zoom:1}.btn-group:first-child{*margin-left:0}.btn-group+.btn-group{margin-left:5px}.btn-toolbar{margin-top:10px;margin-bottom:10px;font-size:0}.btn-toolbar>.btn+.btn,.btn-toolbar>.btn-group+.btn,.btn-toolbar>.btn+.btn-group{margin-left:5px}.btn-group>.btn{position:relative;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.btn-group>.btn+.btn{margin-left:-1px}.btn-group>.btn,.btn-group>.dropdown-menu,.btn-group>.popover{font-size:14px}.btn-group>.btn-mini{font-size:10.5px}.btn-group>.btn-small{font-size:11.9px}.btn-group>.btn-large{font-size:17.5px}.btn-group>.btn:first-child{margin-left:0;-webkit-border-bottom-left-radius:4px;border-bottom-left-radius:4px;-webkit-border-top-left-radius:4px;border-top-left-radius:4px;-moz-border-radius-bottomleft:4px;-moz-border-radius-topleft:4px}.btn-group>.btn:last-child,.btn-group>.dropdown-toggle{-webkit-border-top-right-radius:4px;border-top-right-radius:4px;-webkit-border-bottom-right-radius:4px;border-bottom-right-radius:4px;-moz-border-radius-topright:4px;-moz-border-radius-bottomright:4px}.btn-group>.btn.large:first-child{margin-left:0;-webkit-border-bottom-left-radius:6px;border-bottom-left-radius:6px;-webkit-border-top-left-radius:6px;border-top-left-radius:6px;-moz-border-radius-bottomleft:6px;-moz-border-radius-topleft:6px}.btn-group>.btn.large:last-child,.btn-group>.large.dropdown-toggle{-webkit-border-top-right-radius:6px;border-top-right-radius:6px;-webkit-border-bottom-right-radius:6px;border-bottom-right-radius:6px;-moz-border-radius-topright:6px;-moz-border-radius-bottomright:6px}.btn-group>.btn:hover,.btn-group>.btn:focus,.btn-group>.btn:active,.btn-group>.btn.active{z-index:2}.btn-group .dropdown-toggle:active,.btn-group.open .dropdown-toggle{outline:0}.btn-group>.btn+.dropdown-toggle{*padding-top:5px;padding-right:8px;*padding-bottom:5px;padding-left:8px;-webkit-box-shadow:inset 1px 0 0 rgba(255,255,255,0.125),inset 0 1px 0 rgba(255,255,255,0.2),0 1px 2px rgba(0,0,0,0.05);-moz-box-shadow:inset 1px 0 0 rgba(255,255,255,0.125),inset 0 1px 0 rgba(255,255,255,0.2),0 1px 2px rgba(0,0,0,0.05);box-shadow:inset 1px 0 0 rgba(255,255,255,0.125),inset 0 1px 0 rgba(255,255,255,0.2),0 1px 2px rgba(0,0,0,0.05)}.btn-group>.btn-mini+.dropdown-toggle{*padding-top:2px;padding-right:5px;*padding-bottom:2px;padding-left:5px}.btn-group>.btn-small+.dropdown-toggle{*padding-top:5px;*padding-bottom:4px}.btn-group>.btn-large+.dropdown-toggle{*padding-top:7px;padding-right:12px;*padding-bottom:7px;padding-left:12px}.btn-group.open .dropdown-toggle{background-image:none;-webkit-box-shadow:inset 0 2px 4px rgba(0,0,0,0.15),0 1px 2px rgba(0,0,0,0.05);-moz-box-shadow:inset 0 2px 4px rgba(0,0,0,0.15),0 1px 2px rgba(0,0,0,0.05);box-shadow:inset 0 2px 4px rgba(0,0,0,0.15),0 1px 2px rgba(0,0,0,0.05)}.btn-group.open .btn.dropdown-toggle{background-color:#e6e6e6}.btn-group.open .btn-primary.dropdown-toggle{background-color:#04c}.btn-group.open .btn-warning.dropdown-toggle{background-color:#f89406}.btn-group.open .btn-danger.dropdown-toggle{background-color:#bd362f}.btn-group.open .btn-success.dropdown-toggle{background-color:#51a351}.btn-group.open .btn-info.dropdown-toggle{background-color:#2f96b4}.btn-group.open .btn-inverse.dropdown-toggle{background-color:#222}.btn .caret{margin-top:8px;margin-left:0}.btn-large .caret{margin-top:6px}.btn-large .caret{border-top-width:5px;border-right-width:5px;border-left-width:5px}.btn-mini .caret,.btn-small .caret{margin-top:8px}.dropup .btn-large .caret{border-bottom-width:5px}.btn-primary .caret,.btn-warning .caret,.btn-danger .caret,.btn-info .caret,.btn-success .caret,.btn-inverse .caret{border-top-color:#fff;border-bottom-color:#fff}.btn-group-vertical{display:inline-block;*display:inline;*zoom:1}.btn-group-vertical>.btn{display:block;float:none;max-width:100%;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.btn-group-vertical>.btn+.btn{margin-top:-1px;margin-left:0}.btn-group-vertical>.btn:first-child{-webkit-border-radius:4px 4px 0 0;-moz-border-radius:4px 4px 0 0;border-radius:4px 4px 0 0}.btn-group-vertical>.btn:last-child{-webkit-border-radius:0 0 4px 4px;-moz-border-radius:0 0 4px 4px;border-radius:0 0 4px 4px}.btn-group-vertical>.btn-large:first-child{-webkit-border-radius:6px 6px 0 0;-moz-border-radius:6px 6px 0 0;border-radius:6px 6px 0 0}.btn-group-vertical>.btn-large:last-child{-webkit-border-radius:0 0 6px 6px;-moz-border-radius:0 0 6px 6px;border-radius:0 0 6px 6px}.alert{padding:8px 35px 8px 14px;margin-bottom:20px;text-shadow:0 1px 0 rgba(255,255,255,0.5);background-color:#fcf8e3;border:1px solid #fbeed5;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px}.alert,.alert h4{color:#c09853}.alert h4{margin:0}.alert .close{position:relative;top:-2px;right:-21px;line-height:20px}.alert-success{color:#468847;background-color:#dff0d8;border-color:#d6e9c6}.alert-success h4{color:#468847}.alert-danger,.alert-error{color:#b94a48;background-color:#f2dede;border-color:#eed3d7}.alert-danger h4,.alert-error h4{color:#b94a48}.alert-info{color:#3a87ad;background-color:#d9edf7;border-color:#bce8f1}.alert-info h4{color:#3a87ad}.alert-block{padding-top:14px;padding-bottom:14px}.alert-block>p,.alert-block>ul{margin-bottom:0}.alert-block p+p{margin-top:5px}.nav{margin-bottom:20px;margin-left:0;list-style:none}.nav>li>a{display:block}.nav>li>a:hover,.nav>li>a:focus{text-decoration:none;background-color:#eee}.nav>li>a>img{max-width:none}.nav>.pull-right{float:right}.nav-header{display:block;padding:3px 15px;font-size:11px;font-weight:bold;line-height:20px;color:#999;text-shadow:0 1px 0 rgba(255,255,255,0.5);text-transform:uppercase}.nav li+.nav-header{margin-top:9px}.nav-list{padding-right:15px;padding-left:15px;margin-bottom:0}.nav-list>li>a,.nav-list .nav-header{margin-right:-15px;margin-left:-15px;text-shadow:0 1px 0 rgba(255,255,255,0.5)}.nav-list>li>a{padding:3px 15px}.nav-list>.active>a,.nav-list>.active>a:hover,.nav-list>.active>a:focus{color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.2);background-color:#08c}.nav-list [class^="icon-"],.nav-list [class*=" icon-"]{margin-right:2px}.nav-list .divider{*width:100%;height:1px;margin:9px 1px;*margin:-5px 0 5px;overflow:hidden;background-color:#e5e5e5;border-bottom:1px solid #fff}.nav-tabs,.nav-pills{*zoom:1}.nav-tabs:before,.nav-pills:before,.nav-tabs:after,.nav-pills:after{display:table;line-height:0;content:""}.nav-tabs:after,.nav-pills:after{clear:both}.nav-tabs>li,.nav-pills>li{float:left}.nav-tabs>li>a,.nav-pills>li>a{padding-right:12px;padding-left:12px;margin-right:2px;line-height:14px}.nav-tabs{border-bottom:1px solid #ddd}.nav-tabs>li{margin-bottom:-1px}.nav-tabs>li>a{padding-top:8px;padding-bottom:8px;line-height:20px;border:1px solid transparent;-webkit-border-radius:4px 4px 0 0;-moz-border-radius:4px 4px 0 0;border-radius:4px 4px 0 0}.nav-tabs>li>a:hover,.nav-tabs>li>a:focus{border-color:#eee #eee #ddd}.nav-tabs>.active>a,.nav-tabs>.active>a:hover,.nav-tabs>.active>a:focus{color:#555;cursor:default;background-color:#fff;border:1px solid #ddd;border-bottom-color:transparent}.nav-pills>li>a{padding-top:8px;padding-bottom:8px;margin-top:2px;margin-bottom:2px;-webkit-border-radius:5px;-moz-border-radius:5px;border-radius:5px}.nav-pills>.active>a,.nav-pills>.active>a:hover,.nav-pills>.active>a:focus{color:#fff;background-color:#08c}.nav-stacked>li{float:none}.nav-stacked>li>a{margin-right:0}.nav-tabs.nav-stacked{border-bottom:0}.nav-tabs.nav-stacked>li>a{border:1px solid #ddd;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.nav-tabs.nav-stacked>li:first-child>a{-webkit-border-top-right-radius:4px;border-top-right-radius:4px;-webkit-border-top-left-radius:4px;border-top-left-radius:4px;-moz-border-radius-topright:4px;-moz-border-radius-topleft:4px}.nav-tabs.nav-stacked>li:last-child>a{-webkit-border-bottom-right-radius:4px;border-bottom-right-radius:4px;-webkit-border-bottom-left-radius:4px;border-bottom-left-radius:4px;-moz-border-radius-bottomright:4px;-moz-border-radius-bottomleft:4px}.nav-tabs.nav-stacked>li>a:hover,.nav-tabs.nav-stacked>li>a:focus{z-index:2;border-color:#ddd}.nav-pills.nav-stacked>li>a{margin-bottom:3px}.nav-pills.nav-stacked>li:last-child>a{margin-bottom:1px}.nav-tabs .dropdown-menu{-webkit-border-radius:0 0 6px 6px;-moz-border-radius:0 0 6px 6px;border-radius:0 0 6px 6px}.nav-pills .dropdown-menu{-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px}.nav .dropdown-toggle .caret{margin-top:6px;border-top-color:#08c;border-bottom-color:#08c}.nav .dropdown-toggle:hover .caret,.nav .dropdown-toggle:focus .caret{border-top-color:#005580;border-bottom-color:#005580}.nav-tabs .dropdown-toggle .caret{margin-top:8px}.nav .active .dropdown-toggle .caret{border-top-color:#fff;border-bottom-color:#fff}.nav-tabs .active .dropdown-toggle .caret{border-top-color:#555;border-bottom-color:#555}.nav>.dropdown.active>a:hover,.nav>.dropdown.active>a:focus{cursor:pointer}.nav-tabs .open .dropdown-toggle,.nav-pills .open .dropdown-toggle,.nav>li.dropdown.open.active>a:hover,.nav>li.dropdown.open.active>a:focus{color:#fff;background-color:#999;border-color:#999}.nav li.dropdown.open .caret,.nav li.dropdown.open.active .caret,.nav li.dropdown.open a:hover .caret,.nav li.dropdown.open a:focus .caret{border-top-color:#fff;border-bottom-color:#fff;opacity:1;filter:alpha(opacity=100)}.tabs-stacked .open>a:hover,.tabs-stacked .open>a:focus{border-color:#999}.tabbable{*zoom:1}.tabbable:before,.tabbable:after{display:table;line-height:0;content:""}.tabbable:after{clear:both}.tab-content{overflow:auto}.tabs-below>.nav-tabs,.tabs-right>.nav-tabs,.tabs-left>.nav-tabs{border-bottom:0}.tab-content>.tab-pane,.pill-content>.pill-pane{display:none}.tab-content>.active,.pill-content>.active{display:block}.tabs-below>.nav-tabs{border-top:1px solid #ddd}.tabs-below>.nav-tabs>li{margin-top:-1px;margin-bottom:0}.tabs-below>.nav-tabs>li>a{-webkit-border-radius:0 0 4px 4px;-moz-border-radius:0 0 4px 4px;border-radius:0 0 4px 4px}.tabs-below>.nav-tabs>li>a:hover,.tabs-below>.nav-tabs>li>a:focus{border-top-color:#ddd;border-bottom-color:transparent}.tabs-below>.nav-tabs>.active>a,.tabs-below>.nav-tabs>.active>a:hover,.tabs-below>.nav-tabs>.active>a:focus{border-color:transparent #ddd #ddd #ddd}.tabs-left>.nav-tabs>li,.tabs-right>.nav-tabs>li{float:none}.tabs-left>.nav-tabs>li>a,.tabs-right>.nav-tabs>li>a{min-width:74px;margin-right:0;margin-bottom:3px}.tabs-left>.nav-tabs{float:left;margin-right:19px;border-right:1px solid #ddd}.tabs-left>.nav-tabs>li>a{margin-right:-1px;-webkit-border-radius:4px 0 0 4px;-moz-border-radius:4px 0 0 4px;border-radius:4px 0 0 4px}.tabs-left>.nav-tabs>li>a:hover,.tabs-left>.nav-tabs>li>a:focus{border-color:#eee #ddd #eee #eee}.tabs-left>.nav-tabs .active>a,.tabs-left>.nav-tabs .active>a:hover,.tabs-left>.nav-tabs .active>a:focus{border-color:#ddd transparent #ddd #ddd;*border-right-color:#fff}.tabs-right>.nav-tabs{float:right;margin-left:19px;border-left:1px solid #ddd}.tabs-right>.nav-tabs>li>a{margin-left:-1px;-webkit-border-radius:0 4px 4px 0;-moz-border-radius:0 4px 4px 0;border-radius:0 4px 4px 0}.tabs-right>.nav-tabs>li>a:hover,.tabs-right>.nav-tabs>li>a:focus{border-color:#eee #eee #eee #ddd}.tabs-right>.nav-tabs .active>a,.tabs-right>.nav-tabs .active>a:hover,.tabs-right>.nav-tabs .active>a:focus{border-color:#ddd #ddd #ddd transparent;*border-left-color:#fff}.nav>.disabled>a{color:#999}.nav>.disabled>a:hover,.nav>.disabled>a:focus{text-decoration:none;cursor:default;background-color:transparent}.navbar{*position:relative;*z-index:2;margin-bottom:20px;overflow:visible}.navbar-inner{min-height:40px;padding-right:20px;padding-left:20px;background-color:#fafafa;background-image:-moz-linear-gradient(top,#fff,#f2f2f2);background-image:-webkit-gradient(linear,0 0,0 100%,from(#fff),to(#f2f2f2));background-image:-webkit-linear-gradient(top,#fff,#f2f2f2);background-image:-o-linear-gradient(top,#fff,#f2f2f2);background-image:linear-gradient(to bottom,#fff,#f2f2f2);background-repeat:repeat-x;border:1px solid #d4d4d4;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff',endColorstr='#fff2f2f2',GradientType=0);*zoom:1;-webkit-box-shadow:0 1px 4px rgba(0,0,0,0.065);-moz-box-shadow:0 1px 4px rgba(0,0,0,0.065);box-shadow:0 1px 4px rgba(0,0,0,0.065)}.navbar-inner:before,.navbar-inner:after{display:table;line-height:0;content:""}.navbar-inner:after{clear:both}.navbar .container{width:auto}.nav-collapse.collapse{height:auto;overflow:visible}.navbar .brand{display:block;float:left;padding:10px 20px 10px;margin-left:-20px;font-size:20px;font-weight:200;color:#777;text-shadow:0 1px 0 #fff}.navbar .brand:hover,.navbar .brand:focus{text-decoration:none}.navbar-text{margin-bottom:0;line-height:40px;color:#777}.navbar-link{color:#777}.navbar-link:hover,.navbar-link:focus{color:#333}.navbar .divider-vertical{height:40px;margin:0 9px;border-right:1px solid #fff;border-left:1px solid #f2f2f2}.navbar .btn,.navbar .btn-group{margin-top:5px}.navbar .btn-group .btn,.navbar .input-prepend .btn,.navbar .input-append .btn,.navbar .input-prepend .btn-group,.navbar .input-append .btn-group{margin-top:0}.navbar-form{margin-bottom:0;*zoom:1}.navbar-form:before,.navbar-form:after{display:table;line-height:0;content:""}.navbar-form:after{clear:both}.navbar-form input,.navbar-form select,.navbar-form .radio,.navbar-form .checkbox{margin-top:5px}.navbar-form input,.navbar-form select,.navbar-form .btn{display:inline-block;margin-bottom:0}.navbar-form input[type="image"],.navbar-form input[type="checkbox"],.navbar-form input[type="radio"]{margin-top:3px}.navbar-form .input-append,.navbar-form .input-prepend{margin-top:5px;white-space:nowrap}.navbar-form .input-append input,.navbar-form .input-prepend input{margin-top:0}.navbar-search{position:relative;float:left;margin-top:5px;margin-bottom:0}.navbar-search .search-query{padding:4px 14px;margin-bottom:0;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:13px;font-weight:normal;line-height:1;-webkit-border-radius:15px;-moz-border-radius:15px;border-radius:15px}.navbar-static-top{position:static;margin-bottom:0}.navbar-static-top .navbar-inner{-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.navbar-fixed-top,.navbar-fixed-bottom{position:fixed;right:0;left:0;z-index:1030;margin-bottom:0}.navbar-fixed-top .navbar-inner,.navbar-static-top .navbar-inner{border-width:0 0 1px}.navbar-fixed-bottom .navbar-inner{border-width:1px 0 0}.navbar-fixed-top .navbar-inner,.navbar-fixed-bottom .navbar-inner{padding-right:0;padding-left:0;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.navbar-static-top .container,.navbar-fixed-top .container,.navbar-fixed-bottom .container{width:940px}.navbar-fixed-top{top:0}.navbar-fixed-top .navbar-inner,.navbar-static-top .navbar-inner{-webkit-box-shadow:0 1px 10px rgba(0,0,0,0.1);-moz-box-shadow:0 1px 10px rgba(0,0,0,0.1);box-shadow:0 1px 10px rgba(0,0,0,0.1)}.navbar-fixed-bottom{bottom:0}.navbar-fixed-bottom .navbar-inner{-webkit-box-shadow:0 -1px 10px rgba(0,0,0,0.1);-moz-box-shadow:0 -1px 10px rgba(0,0,0,0.1);box-shadow:0 -1px 10px rgba(0,0,0,0.1)}.navbar .nav{position:relative;left:0;display:block;float:left;margin:0 10px 0 0}.navbar .nav.pull-right{float:right;margin-right:0}.navbar .nav>li{float:left}.navbar .nav>li>a{float:none;padding:10px 15px 10px;color:#777;text-decoration:none;text-shadow:0 1px 0 #fff}.navbar .nav .dropdown-toggle .caret{margin-top:8px}.navbar .nav>li>a:focus,.navbar .nav>li>a:hover{color:#333;text-decoration:none;background-color:transparent}.navbar .nav>.active>a,.navbar .nav>.active>a:hover,.navbar .nav>.active>a:focus{color:#555;text-decoration:none;background-color:#e5e5e5;-webkit-box-shadow:inset 0 3px 8px rgba(0,0,0,0.125);-moz-box-shadow:inset 0 3px 8px rgba(0,0,0,0.125);box-shadow:inset 0 3px 8px rgba(0,0,0,0.125)}.navbar .btn-navbar{display:none;float:right;padding:7px 10px;margin-right:5px;margin-left:5px;color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.25);background-color:#ededed;*background-color:#e5e5e5;background-image:-moz-linear-gradient(top,#f2f2f2,#e5e5e5);background-image:-webkit-gradient(linear,0 0,0 100%,from(#f2f2f2),to(#e5e5e5));background-image:-webkit-linear-gradient(top,#f2f2f2,#e5e5e5);background-image:-o-linear-gradient(top,#f2f2f2,#e5e5e5);background-image:linear-gradient(to bottom,#f2f2f2,#e5e5e5);background-repeat:repeat-x;border-color:#e5e5e5 #e5e5e5 #bfbfbf;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2f2f2',endColorstr='#ffe5e5e5',GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.075);-moz-box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.075);box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.075)}.navbar .btn-navbar:hover,.navbar .btn-navbar:focus,.navbar .btn-navbar:active,.navbar .btn-navbar.active,.navbar .btn-navbar.disabled,.navbar .btn-navbar[disabled]{color:#fff;background-color:#e5e5e5;*background-color:#d9d9d9}.navbar .btn-navbar:active,.navbar .btn-navbar.active{background-color:#ccc \9}.navbar .btn-navbar .icon-bar{display:block;width:18px;height:2px;background-color:#f5f5f5;-webkit-border-radius:1px;-moz-border-radius:1px;border-radius:1px;-webkit-box-shadow:0 1px 0 rgba(0,0,0,0.25);-moz-box-shadow:0 1px 0 rgba(0,0,0,0.25);box-shadow:0 1px 0 rgba(0,0,0,0.25)}.btn-navbar .icon-bar+.icon-bar{margin-top:3px}.navbar .nav>li>.dropdown-menu:before{position:absolute;top:-7px;left:9px;display:inline-block;border-right:7px solid transparent;border-bottom:7px solid #ccc;border-left:7px solid transparent;border-bottom-color:rgba(0,0,0,0.2);content:''}.navbar .nav>li>.dropdown-menu:after{position:absolute;top:-6px;left:10px;display:inline-block;border-right:6px solid transparent;border-bottom:6px solid #fff;border-left:6px solid transparent;content:''}.navbar-fixed-bottom .nav>li>.dropdown-menu:before{top:auto;bottom:-7px;border-top:7px solid #ccc;border-bottom:0;border-top-color:rgba(0,0,0,0.2)}.navbar-fixed-bottom .nav>li>.dropdown-menu:after{top:auto;bottom:-6px;border-top:6px solid #fff;border-bottom:0}.navbar .nav li.dropdown>a:hover .caret,.navbar .nav li.dropdown>a:focus .caret{border-top-color:#333;border-bottom-color:#333}.navbar .nav li.dropdown.open>.dropdown-toggle,.navbar .nav li.dropdown.active>.dropdown-toggle,.navbar .nav li.dropdown.open.active>.dropdown-toggle{color:#555;background-color:#e5e5e5}.navbar .nav li.dropdown>.dropdown-toggle .caret{border-top-color:#777;border-bottom-color:#777}.navbar .nav li.dropdown.open>.dropdown-toggle .caret,.navbar .nav li.dropdown.active>.dropdown-toggle .caret,.navbar .nav li.dropdown.open.active>.dropdown-toggle .caret{border-top-color:#555;border-bottom-color:#555}.navbar .pull-right>li>.dropdown-menu,.navbar .nav>li>.dropdown-menu.pull-right{right:0;left:auto}.navbar .pull-right>li>.dropdown-menu:before,.navbar .nav>li>.dropdown-menu.pull-right:before{right:12px;left:auto}.navbar .pull-right>li>.dropdown-menu:after,.navbar .nav>li>.dropdown-menu.pull-right:after{right:13px;left:auto}.navbar .pull-right>li>.dropdown-menu .dropdown-menu,.navbar .nav>li>.dropdown-menu.pull-right .dropdown-menu{right:100%;left:auto;margin-right:-1px;margin-left:0;-webkit-border-radius:6px 0 6px 6px;-moz-border-radius:6px 0 6px 6px;border-radius:6px 0 6px 6px}.navbar-inverse .navbar-inner{background-color:#1b1b1b;background-image:-moz-linear-gradient(top,#222,#111);background-image:-webkit-gradient(linear,0 0,0 100%,from(#222),to(#111));background-image:-webkit-linear-gradient(top,#222,#111);background-image:-o-linear-gradient(top,#222,#111);background-image:linear-gradient(to bottom,#222,#111);background-repeat:repeat-x;border-color:#252525;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff222222',endColorstr='#ff111111',GradientType=0)}.navbar-inverse .brand,.navbar-inverse .nav>li>a{color:#999;text-shadow:0 -1px 0 rgba(0,0,0,0.25)}.navbar-inverse .brand:hover,.navbar-inverse .nav>li>a:hover,.navbar-inverse .brand:focus,.navbar-inverse .nav>li>a:focus{color:#fff}.navbar-inverse .brand{color:#999}.navbar-inverse .navbar-text{color:#999}.navbar-inverse .nav>li>a:focus,.navbar-inverse .nav>li>a:hover{color:#fff;background-color:transparent}.navbar-inverse .nav .active>a,.navbar-inverse .nav .active>a:hover,.navbar-inverse .nav .active>a:focus{color:#fff;background-color:#111}.navbar-inverse .navbar-link{color:#999}.navbar-inverse .navbar-link:hover,.navbar-inverse .navbar-link:focus{color:#fff}.navbar-inverse .divider-vertical{border-right-color:#222;border-left-color:#111}.navbar-inverse .nav li.dropdown.open>.dropdown-toggle,.navbar-inverse .nav li.dropdown.active>.dropdown-toggle,.navbar-inverse .nav li.dropdown.open.active>.dropdown-toggle{color:#fff;background-color:#111}.navbar-inverse .nav li.dropdown>a:hover .caret,.navbar-inverse .nav li.dropdown>a:focus .caret{border-top-color:#fff;border-bottom-color:#fff}.navbar-inverse .nav li.dropdown>.dropdown-toggle .caret{border-top-color:#999;border-bottom-color:#999}.navbar-inverse .nav li.dropdown.open>.dropdown-toggle .caret,.navbar-inverse .nav li.dropdown.active>.dropdown-toggle .caret,.navbar-inverse .nav li.dropdown.open.active>.dropdown-toggle .caret{border-top-color:#fff;border-bottom-color:#fff}.navbar-inverse .navbar-search .search-query{color:#fff;background-color:#515151;border-color:#111;-webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,0.1),0 1px 0 rgba(255,255,255,0.15);-moz-box-shadow:inset 0 1px 2px rgba(0,0,0,0.1),0 1px 0 rgba(255,255,255,0.15);box-shadow:inset 0 1px 2px rgba(0,0,0,0.1),0 1px 0 rgba(255,255,255,0.15);-webkit-transition:none;-moz-transition:none;-o-transition:none;transition:none}.navbar-inverse .navbar-search .search-query:-moz-placeholder{color:#ccc}.navbar-inverse .navbar-search .search-query:-ms-input-placeholder{color:#ccc}.navbar-inverse .navbar-search .search-query::-webkit-input-placeholder{color:#ccc}.navbar-inverse .navbar-search .search-query:focus,.navbar-inverse .navbar-search .search-query.focused{padding:5px 15px;color:#333;text-shadow:0 1px 0 #fff;background-color:#fff;border:0;outline:0;-webkit-box-shadow:0 0 3px rgba(0,0,0,0.15);-moz-box-shadow:0 0 3px rgba(0,0,0,0.15);box-shadow:0 0 3px rgba(0,0,0,0.15)}.navbar-inverse .btn-navbar{color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.25);background-color:#0e0e0e;*background-color:#040404;background-image:-moz-linear-gradient(top,#151515,#040404);background-image:-webkit-gradient(linear,0 0,0 100%,from(#151515),to(#040404));background-image:-webkit-linear-gradient(top,#151515,#040404);background-image:-o-linear-gradient(top,#151515,#040404);background-image:linear-gradient(to bottom,#151515,#040404);background-repeat:repeat-x;border-color:#040404 #040404 #000;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff151515',endColorstr='#ff040404',GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.navbar-inverse .btn-navbar:hover,.navbar-inverse .btn-navbar:focus,.navbar-inverse .btn-navbar:active,.navbar-inverse .btn-navbar.active,.navbar-inverse .btn-navbar.disabled,.navbar-inverse .btn-navbar[disabled]{color:#fff;background-color:#040404;*background-color:#000}.navbar-inverse .btn-navbar:active,.navbar-inverse .btn-navbar.active{background-color:#000 \9}.breadcrumb{padding:8px 15px;margin:0 0 20px;list-style:none;background-color:#f5f5f5;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px}.breadcrumb>li{display:inline-block;*display:inline;text-shadow:0 1px 0 #fff;*zoom:1}.breadcrumb>li>.divider{padding:0 5px;color:#ccc}.breadcrumb>.active{color:#999}.pagination{margin:20px 0}.pagination ul{display:inline-block;*display:inline;margin-bottom:0;margin-left:0;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;*zoom:1;-webkit-box-shadow:0 1px 2px rgba(0,0,0,0.05);-moz-box-shadow:0 1px 2px rgba(0,0,0,0.05);box-shadow:0 1px 2px rgba(0,0,0,0.05)}.pagination ul>li{display:inline}.pagination ul>li>a,.pagination ul>li>span{float:left;padding:4px 12px;line-height:20px;text-decoration:none;background-color:#fff;border:1px solid #ddd;border-left-width:0}.pagination ul>li>a:hover,.pagination ul>li>a:focus,.pagination ul>.active>a,.pagination ul>.active>span{background-color:#f5f5f5}.pagination ul>.active>a,.pagination ul>.active>span{color:#999;cursor:default}.pagination ul>.disabled>span,.pagination ul>.disabled>a,.pagination ul>.disabled>a:hover,.pagination ul>.disabled>a:focus{color:#999;cursor:default;background-color:transparent}.pagination ul>li:first-child>a,.pagination ul>li:first-child>span{border-left-width:1px;-webkit-border-bottom-left-radius:4px;border-bottom-left-radius:4px;-webkit-border-top-left-radius:4px;border-top-left-radius:4px;-moz-border-radius-bottomleft:4px;-moz-border-radius-topleft:4px}.pagination ul>li:last-child>a,.pagination ul>li:last-child>span{-webkit-border-top-right-radius:4px;border-top-right-radius:4px;-webkit-border-bottom-right-radius:4px;border-bottom-right-radius:4px;-moz-border-radius-topright:4px;-moz-border-radius-bottomright:4px}.pagination-centered{text-align:center}.pagination-right{text-align:right}.pagination-large ul>li>a,.pagination-large ul>li>span{padding:11px 19px;font-size:17.5px}.pagination-large ul>li:first-child>a,.pagination-large ul>li:first-child>span{-webkit-border-bottom-left-radius:6px;border-bottom-left-radius:6px;-webkit-border-top-left-radius:6px;border-top-left-radius:6px;-moz-border-radius-bottomleft:6px;-moz-border-radius-topleft:6px}.pagination-large ul>li:last-child>a,.pagination-large ul>li:last-child>span{-webkit-border-top-right-radius:6px;border-top-right-radius:6px;-webkit-border-bottom-right-radius:6px;border-bottom-right-radius:6px;-moz-border-radius-topright:6px;-moz-border-radius-bottomright:6px}.pagination-mini ul>li:first-child>a,.pagination-small ul>li:first-child>a,.pagination-mini ul>li:first-child>span,.pagination-small ul>li:first-child>span{-webkit-border-bottom-left-radius:3px;border-bottom-left-radius:3px;-webkit-border-top-left-radius:3px;border-top-left-radius:3px;-moz-border-radius-bottomleft:3px;-moz-border-radius-topleft:3px}.pagination-mini ul>li:last-child>a,.pagination-small ul>li:last-child>a,.pagination-mini ul>li:last-child>span,.pagination-small ul>li:last-child>span{-webkit-border-top-right-radius:3px;border-top-right-radius:3px;-webkit-border-bottom-right-radius:3px;border-bottom-right-radius:3px;-moz-border-radius-topright:3px;-moz-border-radius-bottomright:3px}.pagination-small ul>li>a,.pagination-small ul>li>span{padding:2px 10px;font-size:11.9px}.pagination-mini ul>li>a,.pagination-mini ul>li>span{padding:0 6px;font-size:10.5px}.pager{margin:20px 0;text-align:center;list-style:none;*zoom:1}.pager:before,.pager:after{display:table;line-height:0;content:""}.pager:after{clear:both}.pager li{display:inline}.pager li>a,.pager li>span{display:inline-block;padding:5px 14px;background-color:#fff;border:1px solid #ddd;-webkit-border-radius:15px;-moz-border-radius:15px;border-radius:15px}.pager li>a:hover,.pager li>a:focus{text-decoration:none;background-color:#f5f5f5}.pager .next>a,.pager .next>span{float:right}.pager .previous>a,.pager .previous>span{float:left}.pager .disabled>a,.pager .disabled>a:hover,.pager .disabled>a:focus,.pager .disabled>span{color:#999;cursor:default;background-color:#fff}.modal-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1040;background-color:#000}.modal-backdrop.fade{opacity:0}.modal-backdrop,.modal-backdrop.fade.in{opacity:.8;filter:alpha(opacity=80)}.modal{position:fixed;top:10%;left:50%;z-index:1050;width:560px;margin-left:-280px;background-color:#fff;border:1px solid #999;border:1px solid rgba(0,0,0,0.3);*border:1px solid #999;-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px;outline:0;-webkit-box-shadow:0 3px 7px rgba(0,0,0,0.3);-moz-box-shadow:0 3px 7px rgba(0,0,0,0.3);box-shadow:0 3px 7px rgba(0,0,0,0.3);-webkit-background-clip:padding-box;-moz-background-clip:padding-box;background-clip:padding-box}.modal.fade{top:-25%;-webkit-transition:opacity .3s linear,top .3s ease-out;-moz-transition:opacity .3s linear,top .3s ease-out;-o-transition:opacity .3s linear,top .3s ease-out;transition:opacity .3s linear,top .3s ease-out}.modal.fade.in{top:10%}.modal-header{padding:9px 15px;border-bottom:1px solid #eee}.modal-header .close{margin-top:2px}.modal-header h3{margin:0;line-height:30px}.modal-body{position:relative;max-height:400px;padding:15px;overflow-y:auto}.modal-form{margin-bottom:0}.modal-footer{padding:14px 15px 15px;margin-bottom:0;text-align:right;background-color:#f5f5f5;border-top:1px solid #ddd;-webkit-border-radius:0 0 6px 6px;-moz-border-radius:0 0 6px 6px;border-radius:0 0 6px 6px;*zoom:1;-webkit-box-shadow:inset 0 1px 0 #fff;-moz-box-shadow:inset 0 1px 0 #fff;box-shadow:inset 0 1px 0 #fff}.modal-footer:before,.modal-footer:after{display:table;line-height:0;content:""}.modal-footer:after{clear:both}.modal-footer .btn+.btn{margin-bottom:0;margin-left:5px}.modal-footer .btn-group .btn+.btn{margin-left:-1px}.modal-footer .btn-block+.btn-block{margin-left:0}.tooltip{position:absolute;z-index:1030;display:block;font-size:11px;line-height:1.4;opacity:0;filter:alpha(opacity=0);visibility:visible}.tooltip.in{opacity:.8;filter:alpha(opacity=80)}.tooltip.top{padding:5px 0;margin-top:-3px}.tooltip.right{padding:0 5px;margin-left:3px}.tooltip.bottom{padding:5px 0;margin-top:3px}.tooltip.left{padding:0 5px;margin-left:-3px}.tooltip-inner{max-width:200px;padding:8px;color:#fff;text-align:center;text-decoration:none;background-color:#000;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px}.tooltip-arrow{position:absolute;width:0;height:0;border-color:transparent;border-style:solid}.tooltip.top .tooltip-arrow{bottom:0;left:50%;margin-left:-5px;border-top-color:#000;border-width:5px 5px 0}.tooltip.right .tooltip-arrow{top:50%;left:0;margin-top:-5px;border-right-color:#000;border-width:5px 5px 5px 0}.tooltip.left .tooltip-arrow{top:50%;right:0;margin-top:-5px;border-left-color:#000;border-width:5px 0 5px 5px}.tooltip.bottom .tooltip-arrow{top:0;left:50%;margin-left:-5px;border-bottom-color:#000;border-width:0 5px 5px}.popover{position:absolute;top:0;left:0;z-index:1010;display:none;max-width:276px;padding:1px;text-align:left;white-space:normal;background-color:#fff;border:1px solid #ccc;border:1px solid rgba(0,0,0,0.2);-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px;-webkit-box-shadow:0 5px 10px rgba(0,0,0,0.2);-moz-box-shadow:0 5px 10px rgba(0,0,0,0.2);box-shadow:0 5px 10px rgba(0,0,0,0.2);-webkit-background-clip:padding-box;-moz-background-clip:padding;background-clip:padding-box}.popover.top{margin-top:-10px}.popover.right{margin-left:10px}.popover.bottom{margin-top:10px}.popover.left{margin-left:-10px}.popover-title{padding:8px 14px;margin:0;font-size:14px;font-weight:normal;line-height:18px;background-color:#f7f7f7;border-bottom:1px solid #ebebeb;-webkit-border-radius:5px 5px 0 0;-moz-border-radius:5px 5px 0 0;border-radius:5px 5px 0 0}.popover-title:empty{display:none}.popover-content{padding:9px 14px}.popover .arrow,.popover .arrow:after{position:absolute;display:block;width:0;height:0;border-color:transparent;border-style:solid}.popover .arrow{border-width:11px}.popover .arrow:after{border-width:10px;content:""}.popover.top .arrow{bottom:-11px;left:50%;margin-left:-11px;border-top-color:#999;border-top-color:rgba(0,0,0,0.25);border-bottom-width:0}.popover.top .arrow:after{bottom:1px;margin-left:-10px;border-top-color:#fff;border-bottom-width:0}.popover.right .arrow{top:50%;left:-11px;margin-top:-11px;border-right-color:#999;border-right-color:rgba(0,0,0,0.25);border-left-width:0}.popover.right .arrow:after{bottom:-10px;left:1px;border-right-color:#fff;border-left-width:0}.popover.bottom .arrow{top:-11px;left:50%;margin-left:-11px;border-bottom-color:#999;border-bottom-color:rgba(0,0,0,0.25);border-top-width:0}.popover.bottom .arrow:after{top:1px;margin-left:-10px;border-bottom-color:#fff;border-top-width:0}.popover.left .arrow{top:50%;right:-11px;margin-top:-11px;border-left-color:#999;border-left-color:rgba(0,0,0,0.25);border-right-width:0}.popover.left .arrow:after{right:1px;bottom:-10px;border-left-color:#fff;border-right-width:0}.thumbnails{margin-left:-20px;list-style:none;*zoom:1}.thumbnails:before,.thumbnails:after{display:table;line-height:0;content:""}.thumbnails:after{clear:both}.row-fluid .thumbnails{margin-left:0}.thumbnails>li{float:left;margin-bottom:20px;margin-left:20px}.thumbnail{display:block;padding:4px;line-height:20px;border:1px solid #ddd;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;-webkit-box-shadow:0 1px 3px rgba(0,0,0,0.055);-moz-box-shadow:0 1px 3px rgba(0,0,0,0.055);box-shadow:0 1px 3px rgba(0,0,0,0.055);-webkit-transition:all .2s ease-in-out;-moz-transition:all .2s ease-in-out;-o-transition:all .2s ease-in-out;transition:all .2s ease-in-out}a.thumbnail:hover,a.thumbnail:focus{border-color:#08c;-webkit-box-shadow:0 1px 4px rgba(0,105,214,0.25);-moz-box-shadow:0 1px 4px rgba(0,105,214,0.25);box-shadow:0 1px 4px rgba(0,105,214,0.25)}.thumbnail>img{display:block;max-width:100%;margin-right:auto;margin-left:auto}.thumbnail .caption{padding:9px;color:#555}.media,.media-body{overflow:hidden;*overflow:visible;zoom:1}.media,.media .media{margin-top:15px}.media:first-child{margin-top:0}.media-object{display:block}.media-heading{margin:0 0 5px}.media>.pull-left{margin-right:10px}.media>.pull-right{margin-left:10px}.media-list{margin-left:0;list-style:none}.label,.badge{display:inline-block;padding:2px 4px;font-size:11.844px;font-weight:bold;line-height:14px;color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.25);white-space:nowrap;vertical-align:baseline;background-color:#999}.label{-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px}.badge{padding-right:9px;padding-left:9px;-webkit-border-radius:9px;-moz-border-radius:9px;border-radius:9px}.label:empty,.badge:empty{display:none}a.label:hover,a.label:focus,a.badge:hover,a.badge:focus{color:#fff;text-decoration:none;cursor:pointer}.label-important,.badge-important{background-color:#b94a48}.label-important[href],.badge-important[href]{background-color:#953b39}.label-warning,.badge-warning{background-color:#f89406}.label-warning[href],.badge-warning[href]{background-color:#c67605}.label-success,.badge-success{background-color:#468847}.label-success[href],.badge-success[href]{background-color:#356635}.label-info,.badge-info{background-color:#3a87ad}.label-info[href],.badge-info[href]{background-color:#2d6987}.label-inverse,.badge-inverse{background-color:#333}.label-inverse[href],.badge-inverse[href]{background-color:#1a1a1a}.btn .label,.btn .badge{position:relative;top:-1px}.btn-mini .label,.btn-mini .badge{top:0}@-webkit-keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}@-moz-keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}@-ms-keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}@-o-keyframes progress-bar-stripes{from{background-position:0 0}to{background-position:40px 0}}@keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}.progress{height:20px;margin-bottom:20px;overflow:hidden;background-color:#f7f7f7;background-image:-moz-linear-gradient(top,#f5f5f5,#f9f9f9);background-image:-webkit-gradient(linear,0 0,0 100%,from(#f5f5f5),to(#f9f9f9));background-image:-webkit-linear-gradient(top,#f5f5f5,#f9f9f9);background-image:-o-linear-gradient(top,#f5f5f5,#f9f9f9);background-image:linear-gradient(to bottom,#f5f5f5,#f9f9f9);background-repeat:repeat-x;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5',endColorstr='#fff9f9f9',GradientType=0);-webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,0.1);-moz-box-shadow:inset 0 1px 2px rgba(0,0,0,0.1);box-shadow:inset 0 1px 2px rgba(0,0,0,0.1)}.progress .bar{float:left;width:0;height:100%;font-size:12px;color:#fff;text-align:center;text-shadow:0 -1px 0 rgba(0,0,0,0.25);background-color:#0e90d2;background-image:-moz-linear-gradient(top,#149bdf,#0480be);background-image:-webkit-gradient(linear,0 0,0 100%,from(#149bdf),to(#0480be));background-image:-webkit-linear-gradient(top,#149bdf,#0480be);background-image:-o-linear-gradient(top,#149bdf,#0480be);background-image:linear-gradient(to bottom,#149bdf,#0480be);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff149bdf',endColorstr='#ff0480be',GradientType=0);-webkit-box-shadow:inset 0 -1px 0 rgba(0,0,0,0.15);-moz-box-shadow:inset 0 -1px 0 rgba(0,0,0,0.15);box-shadow:inset 0 -1px 0 rgba(0,0,0,0.15);-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;-webkit-transition:width .6s ease;-moz-transition:width .6s ease;-o-transition:width .6s ease;transition:width .6s ease}.progress .bar+.bar{-webkit-box-shadow:inset 1px 0 0 rgba(0,0,0,0.15),inset 0 -1px 0 rgba(0,0,0,0.15);-moz-box-shadow:inset 1px 0 0 rgba(0,0,0,0.15),inset 0 -1px 0 rgba(0,0,0,0.15);box-shadow:inset 1px 0 0 rgba(0,0,0,0.15),inset 0 -1px 0 rgba(0,0,0,0.15)}.progress-striped .bar{background-color:#149bdf;background-image:-webkit-gradient(linear,0 100%,100% 0,color-stop(0.25,rgba(255,255,255,0.15)),color-stop(0.25,transparent),color-stop(0.5,transparent),color-stop(0.5,rgba(255,255,255,0.15)),color-stop(0.75,rgba(255,255,255,0.15)),color-stop(0.75,transparent),to(transparent));background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:-moz-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);-webkit-background-size:40px 40px;-moz-background-size:40px 40px;-o-background-size:40px 40px;background-size:40px 40px}.progress.active .bar{-webkit-animation:progress-bar-stripes 2s linear infinite;-moz-animation:progress-bar-stripes 2s linear infinite;-ms-animation:progress-bar-stripes 2s linear infinite;-o-animation:progress-bar-stripes 2s linear infinite;animation:progress-bar-stripes 2s linear infinite}.progress-danger .bar,.progress .bar-danger{background-color:#dd514c;background-image:-moz-linear-gradient(top,#ee5f5b,#c43c35);background-image:-webkit-gradient(linear,0 0,0 100%,from(#ee5f5b),to(#c43c35));background-image:-webkit-linear-gradient(top,#ee5f5b,#c43c35);background-image:-o-linear-gradient(top,#ee5f5b,#c43c35);background-image:linear-gradient(to bottom,#ee5f5b,#c43c35);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffee5f5b',endColorstr='#ffc43c35',GradientType=0)}.progress-danger.progress-striped .bar,.progress-striped .bar-danger{background-color:#ee5f5b;background-image:-webkit-gradient(linear,0 100%,100% 0,color-stop(0.25,rgba(255,255,255,0.15)),color-stop(0.25,transparent),color-stop(0.5,transparent),color-stop(0.5,rgba(255,255,255,0.15)),color-stop(0.75,rgba(255,255,255,0.15)),color-stop(0.75,transparent),to(transparent));background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:-moz-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent)}.progress-success .bar,.progress .bar-success{background-color:#5eb95e;background-image:-moz-linear-gradient(top,#62c462,#57a957);background-image:-webkit-gradient(linear,0 0,0 100%,from(#62c462),to(#57a957));background-image:-webkit-linear-gradient(top,#62c462,#57a957);background-image:-o-linear-gradient(top,#62c462,#57a957);background-image:linear-gradient(to bottom,#62c462,#57a957);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff62c462',endColorstr='#ff57a957',GradientType=0)}.progress-success.progress-striped .bar,.progress-striped .bar-success{background-color:#62c462;background-image:-webkit-gradient(linear,0 100%,100% 0,color-stop(0.25,rgba(255,255,255,0.15)),color-stop(0.25,transparent),color-stop(0.5,transparent),color-stop(0.5,rgba(255,255,255,0.15)),color-stop(0.75,rgba(255,255,255,0.15)),color-stop(0.75,transparent),to(transparent));background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:-moz-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent)}.progress-info .bar,.progress .bar-info{background-color:#4bb1cf;background-image:-moz-linear-gradient(top,#5bc0de,#339bb9);background-image:-webkit-gradient(linear,0 0,0 100%,from(#5bc0de),to(#339bb9));background-image:-webkit-linear-gradient(top,#5bc0de,#339bb9);background-image:-o-linear-gradient(top,#5bc0de,#339bb9);background-image:linear-gradient(to bottom,#5bc0de,#339bb9);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de',endColorstr='#ff339bb9',GradientType=0)}.progress-info.progress-striped .bar,.progress-striped .bar-info{background-color:#5bc0de;background-image:-webkit-gradient(linear,0 100%,100% 0,color-stop(0.25,rgba(255,255,255,0.15)),color-stop(0.25,transparent),color-stop(0.5,transparent),color-stop(0.5,rgba(255,255,255,0.15)),color-stop(0.75,rgba(255,255,255,0.15)),color-stop(0.75,transparent),to(transparent));background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:-moz-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent)}.progress-warning .bar,.progress .bar-warning{background-color:#faa732;background-image:-moz-linear-gradient(top,#fbb450,#f89406);background-image:-webkit-gradient(linear,0 0,0 100%,from(#fbb450),to(#f89406));background-image:-webkit-linear-gradient(top,#fbb450,#f89406);background-image:-o-linear-gradient(top,#fbb450,#f89406);background-image:linear-gradient(to bottom,#fbb450,#f89406);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffbb450',endColorstr='#fff89406',GradientType=0)}.progress-warning.progress-striped .bar,.progress-striped .bar-warning{background-color:#fbb450;background-image:-webkit-gradient(linear,0 100%,100% 0,color-stop(0.25,rgba(255,255,255,0.15)),color-stop(0.25,transparent),color-stop(0.5,transparent),color-stop(0.5,rgba(255,255,255,0.15)),color-stop(0.75,rgba(255,255,255,0.15)),color-stop(0.75,transparent),to(transparent));background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:-moz-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent)}.accordion{margin-bottom:20px}.accordion-group{margin-bottom:2px;border:1px solid #e5e5e5;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px}.accordion-heading{border-bottom:0}.accordion-heading .accordion-toggle{display:block;padding:8px 15px}.accordion-toggle{cursor:pointer}.accordion-inner{padding:9px 15px;border-top:1px solid #e5e5e5}.carousel{position:relative;margin-bottom:20px;line-height:1}.carousel-inner{position:relative;width:100%;overflow:hidden}.carousel-inner>.item{position:relative;display:none;-webkit-transition:.6s ease-in-out left;-moz-transition:.6s ease-in-out left;-o-transition:.6s ease-in-out left;transition:.6s ease-in-out left}.carousel-inner>.item>img,.carousel-inner>.item>a>img{display:block;line-height:1}.carousel-inner>.active,.carousel-inner>.next,.carousel-inner>.prev{display:block}.carousel-inner>.active{left:0}.carousel-inner>.next,.carousel-inner>.prev{position:absolute;top:0;width:100%}.carousel-inner>.next{left:100%}.carousel-inner>.prev{left:-100%}.carousel-inner>.next.left,.carousel-inner>.prev.right{left:0}.carousel-inner>.active.left{left:-100%}.carousel-inner>.active.right{left:100%}.carousel-control{position:absolute;top:40%;left:15px;width:40px;height:40px;margin-top:-20px;font-size:60px;font-weight:100;line-height:30px;color:#fff;text-align:center;background:#222;border:3px solid #fff;-webkit-border-radius:23px;-moz-border-radius:23px;border-radius:23px;opacity:.5;filter:alpha(opacity=50)}.carousel-control.right{right:15px;left:auto}.carousel-control:hover,.carousel-control:focus{color:#fff;text-decoration:none;opacity:.9;filter:alpha(opacity=90)}.carousel-indicators{position:absolute;top:15px;right:15px;z-index:5;margin:0;list-style:none}.carousel-indicators li{display:block;float:left;width:10px;height:10px;margin-left:5px;text-indent:-999px;background-color:#ccc;background-color:rgba(255,255,255,0.25);border-radius:5px}.carousel-indicators .active{background-color:#fff}.carousel-caption{position:absolute;right:0;bottom:0;left:0;padding:15px;background:#333;background:rgba(0,0,0,0.75)}.carousel-caption h4,.carousel-caption p{line-height:20px;color:#fff}.carousel-caption h4{margin:0 0 5px}.carousel-caption p{margin-bottom:0}.hero-unit{padding:60px;margin-bottom:30px;font-size:18px;font-weight:200;line-height:30px;color:inherit;background-color:#eee;-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px}.hero-unit h1{margin-bottom:0;font-size:60px;line-height:1;letter-spacing:-1px;color:inherit}.hero-unit li{line-height:30px}.pull-right{float:right}.pull-left{float:left}.hide{display:none}.show{display:block}.invisible{visibility:hidden}.affix{position:fixed} diff --git a/app/public/css/flat-ui-no-icons.css b/app/public/css/flat-ui-no-icons.css deleted file mode 100644 index cd2a538eb..000000000 --- a/app/public/css/flat-ui-no-icons.css +++ /dev/null @@ -1,2312 +0,0 @@ -body { - color: #34495e; - font: 14px/1.231 "Lato", sans-serif; } - -a { - color: #1abc9c; - text-decoration: underline; - -webkit-transition: 0.25s; - -moz-transition: 0.25s; - -o-transition: 0.25s; - transition: 0.25s; - -webkit-backface-visibility: hidden; } - a:hover { - color: #2ecc71; - text-decoration: none; } - -h1 { - font-size: 32px; - font-weight: 900; } - -h2 { - font-size: 26px; - font-weight: 700; - margin-bottom: 2px; } - -h3 { - font-size: 24px; - font-weight: 700; - margin-bottom: 4px; - margin-top: 2px; } - -h4 { - font-size: 18px; - font-weight: 500; - margin-top: 4px; } - -h5 { - font-size: 16px; - font-weight: 500; - text-transform: uppercase; } - -h6 { - font-size: 13px; - font-weight: 500; - text-transform: uppercase; } - -.btn { - border: none; - background: #34495e; - color: white; - font-size: 16.5px; - text-decoration: none; - text-shadow: none; - -webkit-box-shadow: none; - -moz-box-shadow: none; - box-shadow: none; - -webkit-transition: 0.25s; - -moz-transition: 0.25s; - -o-transition: 0.25s; - transition: 0.25s; - -webkit-backface-visibility: hidden; } - .btn:hover, .btn:focus { - background-color: #4e6d8d; - color: white; - -webkit-transition: 0.25s; - -moz-transition: 0.25s; - -o-transition: 0.25s; - transition: 0.25s; - -webkit-backface-visibility: hidden; } - .btn:active, .btn.active { - background-color: #2c3e50; - color: rgba(255, 255, 255, 0.75); - -webkit-box-shadow: none; - -moz-box-shadow: none; - box-shadow: none; } - .btn.disabled, .btn[disabled] { - background-color: #95a5a6; - color: white; } - .btn.btn-large { - padding-bottom: 12px; - padding-top: 13px; } - .btn.btn-primary { - background-color: #1abc9c; } - .btn.btn-primary:hover, .btn.btn-primary:focus { - background-color: #2fe2bf; } - .btn.btn-primary:active, .btn.btn-primary.active { - background-color: #16a085; } - .btn.btn-info { - background-color: #3498db; } - .btn.btn-info:hover, .btn.btn-info:focus { - background-color: #5dade2; } - .btn.btn-info:active, .btn.btn-info.active { - background-color: #2383c4; } - .btn.btn-danger { - background-color: #e74c3c; } - .btn.btn-danger:hover, .btn.btn-danger:focus { - background-color: #ec7063; } - .btn.btn-danger:active, .btn.btn-danger.active { - background-color: #dc2d1b; } - .btn.btn-success { - background-color: #2ecc71; } - .btn.btn-success:hover, .btn.btn-success:focus { - background-color: #55d98d; } - .btn.btn-success:active, .btn.btn-success.active { - background-color: #27ad60; } - .btn.btn-warning { - background-color: #f1c40f; } - .btn.btn-warning:hover, .btn.btn-warning:focus { - background-color: #f4d03f; } - .btn.btn-warning:active, .btn.btn-warning.active { - background-color: #cea70c; } - .btn-toolbar .btn { - font-size: 18px; - padding: 10px 14px 9px; } - .btn-toolbar .btn:first-child { - -webkit-border-radius: 6px 0 0 6px; - -moz-border-radius: 6px 0 0 6px; - border-radius: 6px 0 0 6px; } - .btn-toolbar .btn:last-child { - -webkit-border-radius: 0 6px 6px 0; - -moz-border-radius: 0 6px 6px 0; - border-radius: 0 6px 6px 0; } - -.btn-toolbar .btn.active { - color: white; } - -.demo-headline { - padding: 73px 0 110px; - text-align: center; } - -.demo-logo { - font-size: 90px; - font-weight: 900; - letter-spacing: -2px; - line-height: 100px; } - .demo-logo .logo { - background: url("../images/demo/logo-mask.png") center 0 no-repeat; - background-size: 256px 186px; - height: 186px; - margin: 0 auto 26px; - overflow: hidden; - text-indent: -9999em; - width: 256px; } - .demo-logo small { - color: rgba(52, 73, 94, 0.3); - display: block; - font-size: 22px; - font-weight: 700; - letter-spacing: -1px; - padding-top: 5px; } - -.demo-row { - margin-bottom: 20px; } - -.demo-panel-title { - margin-bottom: 20px; - padding-top: 20px; } - .demo-panel-title small { - color: #bfc1c3; - font-size: inherit; - font-weight: 400; } - -.demo-navigation { - margin-bottom: -4px; - margin-top: -10px; } - -.demo-pager { - margin-top: -10px; } - -.demo-tooltips { - height: 126px; } - .demo-tooltips .tooltip { - left: -8px !important; - position: relative !important; - top: -8px !important; } - -.demo-headings { - margin-bottom: 12px; } - -.demo-tiles { - margin-bottom: 46px; } - -.demo-icons { - margin-bottom: 115px; } - -.demo-icons-24 { - font-size: 24px; - margin-bottom: 38px; - position: relative; } - .demo-icons-24 span { - margin: 0 0 0 18px; } - .demo-icons-24 span:first-child { - margin-left: 0; } - -.demo-icons-16 { - font-size: 16px; - margin: 0 0 38px 5px; - position: relative; } - .demo-icons-16 span { - margin: 0 0 0 28px; } - .demo-icons-16 span:first-child { - margin-left: 0; } - -.demo-icons-tooltip { - bottom: 0; - color: #b9c8d8; - font-size: 12px; - left: 100%; - margin-left: 0 !important; - position: absolute; - width: 80px; } - -.demo-illustrations { - margin-bottom: 45px; } - .demo-illustrations img { - height: 100px; - margin-left: 35px; - width: 100px; - vertical-align: bottom; } - .demo-illustrations img:first-child { - margin-left: 0; } - .demo-illustrations img.big-illustration { - height: 111px; - width: 112px; } - .demo-illustrations img.big-retina-illustration { - height: 104px; - margin-right: -24px; - width: 117px; } - .demo-illustrations img.big-illustration-pusher { - margin-right: 12px; } - -.demo-samples { - margin-bottom: 46px; } - -.demo-video { - border-radius: 6px; - padding-top: 95px; } - -.demo-download-section { - float: none; - margin: 0 auto; - padding: 60px 0 90px 20px; - text-align: center; } - .demo-download-section [class*='fui-'] { - margin: 3px 0 -3px; } - -.demo-download { - background-color: #e8edf2; - border-radius: 50%; - height: 120px; - margin: 0 auto 32px; - padding: 40px 28px 30px 32px; - text-align: center; - width: 130px; } - .demo-download img { - height: 104px; - width: 82px; } - -.demo-download-text { - font-size: 15px; - padding: 20px 0; - text-align: center; } - -.demo-text-box a:hover { - color: #1abc9c; } - -.demo-browser { - background: #2c3e50 url("../images/demo/browser.png") 0 0 no-repeat; - background-size: 659px 42px; - border-radius: 0 0 6px 6px; - color: white; - margin: 0 41px 140px 0; - padding-top: 42px; } - -.demo-browser-side { - float: left; - padding: 22px 20px; - width: 111px; } - .demo-browser-side > h5 { - margin-bottom: 3px; - text-transform: none; } - .demo-browser-side > h6 { - font-size: 11px; - font-weight: 300; - line-height: 18px; - margin-top: 3px; - text-transform: none; } - -.demo-browser-author { - background: url("../images/demo/browser-author.jpg") center center no-repeat; - border: 3px solid white; - display: block; - height: 84px; - margin: 0 auto; - width: 84px; - -webkit-border-radius: 50%; - -moz-border-radius: 50%; - border-radius: 50%; } - -.demo-browser-action { - padding: 30px 0 12px; } - .demo-browser-action > .btn { - padding: 9px 0 10px 11px; - text-align: left; - -webkit-border-radius: 3px; - -moz-border-radius: 3px; - border-radius: 3px; } - .demo-browser-action > .btn:before { - color: white; - content: "\e004"; - font-size: 16px; - font-family: "Flat-UI-Icons-16"; - font-weight: 300; - margin-right: 12px; - position: relative; - top: 1px; - -webkit-font-smoothing: antialiased; } - -.demo-browser-content { - background-color: #34495e; - border-radius: 0 0 6px; - overflow: hidden; - padding: 21px 0 0 20px; } - .demo-browser-content > img { - border: 6px solid white; - float: left; - margin: 0 15px 20px 0; - width: 134px; } - -@media only screen and (-webkit-min-device-pixel-ratio: 2), only screen and (-webkit-min-device-pixel-ratio: 1.5), only screen and (-moz-min-device-pixel-ratio: 2), only screen and (-o-min-device-pixel-ratio: 3 / 2), only screen and (-o-min-device-pixel-ratio: 2 / 1), only screen and (min--moz-device-pixel-ratio: 1.5), only screen and (min-device-pixel-ratio: 1.5), only screen and (min-device-pixel-ratio: 2) { - .logo { - background-image: url("../images/demo/logo-mask-2x.png"); } - - .demo-browser { - background-image: url("../images/demo/browser-2x.png"); } } -.navbar { - font-size: 18px; } - .navbar .brand { - color: #1abc9c; - font-size: inherit; - font-weight: 700; - padding-bottom: 16px; - padding-top: 15px; } - .navbar .nav > li:hover > ul { - top: 100%; } - .navbar .nav > li > ul { - padding-top: 13px; - top: 80%; - background-color: white\9; } - .navbar .nav > li > ul:before { - content: ""; - border-style: solid; - border-width: 0 9px 9px 9px; - border-color: transparent transparent #34495e transparent; - height: 0px; - position: absolute; - left: 15px; - top: 5px; - width: 0px; - -webkit-transform: rotate(360deg); } - .navbar .nav > li > ul li:hover ul { - opacity: 1; - -webkit-transform: scale(1, 1); - display: block\9; } - .navbar .nav > li > ul li ul { - left: 100%; } - .navbar .nav > li > a { - padding: 14px 15px 17px; } - .navbar .nav > li > a:hover { - color: #1abc9c; } - .navbar .nav li { - position: relative; } - .navbar .nav li:hover > ul { - opacity: 1; - z-index: 100; - -webkit-transform: scale(1, 1); - display: block\9; } - .navbar .nav ul { - border-radius: 4px; - left: 15px; - list-style-type: none; - margin-left: 0; - opacity: 0; - position: absolute; - top: 0; - width: 234px; - z-index: -100; - -webkit-transform: scale(1, 0.99); - -webkit-transform-origin: 0 0; - display: none\9; - -webkit-transition: 0.3s ease-out; - -moz-transition: 0.3s ease-out; - -o-transition: 0.3s ease-out; - transition: 0.3s ease-out; - -webkit-backface-visibility: hidden; } - .navbar .nav ul ul { - left: 95%; - padding-left: 5px; } - .navbar .nav ul li { - background-color: #34495e; - padding: 0 3px 3px; } - .navbar .nav ul li:first-child { - border-radius: 4px 4px 0 0; - padding-top: 3px; } - .navbar .nav ul li:last-child { - border-radius: 0 0 4px 4px; } - .navbar .nav ul li.active > a, .navbar .nav ul li.active > a:hover { - background-color: #1abc9c; - color: white; } - .navbar .nav ul a { - border-radius: 2px; - color: white; - display: block; - font-size: 14px; - padding: 6px 9px; - text-decoration: none; } - .navbar .nav ul a:hover { - background-color: #1abc9c; } - -.navbar-inner { - border: none; - padding-left: 4px; - padding-right: 4px; - -webkit-border-radius: 6px; - -moz-border-radius: 6px; - border-radius: 6px; - filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); } - -.navbar-inverse .navbar-inner { - background: #34495e; - filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); } -.navbar-inverse .nav > li > a { - color: white; } -.navbar-inverse .nav .active > a, .navbar-inverse .nav .active > a:hover, .navbar-inverse .nav .active > a:focus { - background-color: transparent; - color: #1abc9c; - -webkit-box-shadow: none; - -moz-box-shadow: none; - box-shadow: none; } - -.navbar-unread { - background-color: #e74c3c; - border-radius: 30px; - color: white; - display: none; - font-size: 12px; - font-weight: 500; - line-height: 18px; - min-width: 8px; - padding: 0 5px; - position: absolute; - right: -7px; - text-align: center; - text-shadow: none; - top: 8px; - z-index: 10; } - .active .navbar-unread { - display: block; } - -.dk_container { - cursor: pointer; - font-size: 14px; - margin-bottom: 10px; - outline: none; } - -.dk_toggle { - background-color: #1abc9c; - color: white; - border-radius: 6px; - overflow: hidden; - padding: 11px 45px 11px 13px; - text-decoration: none; - white-space: nowrap; - -webkit-transition: 0.25s; - -moz-transition: 0.25s; - -o-transition: 0.25s; - transition: 0.25s; - -webkit-backface-visibility: hidden; } - .dk_toggle:hover, .dk_toggle:focus, .dk_focus .dk_toggle { - background-color: #2fe2bf; - color: white; - outline: none; } - .dk_toggle:active { - background-color: #16a085; - outline: none; } - .dk_toggle:active .select-icon { - border-left-color: transparent; } - -.select-icon { - background: #1abc9c url("../images/select/toggle.png") no-repeat right center; - border-left: 2px solid rgba(52, 73, 94, 0.15); - border-radius: 0 6px 6px 0; - height: 100%; - position: absolute; - right: 0; - top: 0; - width: 42px; - -webkit-transition: 0.25s; - -moz-transition: 0.25s; - -o-transition: 0.25s; - transition: 0.25s; - -webkit-backface-visibility: hidden; } - -.dk_open { - z-index: 10; } - .dk_open .dk_toggle { - background-color: #1abc9c; } - .dk_open .dk_toggle .select-icon { - background-color: #16a085; - border-left-color: transparent; } - -.dk_options { - padding-top: 14px; } - .dk_options:before { - content: ""; - border-style: solid; - border-width: 0 9px 9px 9px; - border-color: transparent transparent #34495e transparent; - height: 0px; - position: absolute; - left: 15px; - top: 5px; - width: 0px; - -webkit-transform: rotate(360deg); } - .dk_options:before { - left: auto; - right: 12px; } - .dk_options li { - padding-bottom: 3px; } - .dk_options a { - border-radius: 3px; - color: white; - display: block; - padding: 5px 9px; - text-decoration: none; } - .dk_options a:hover { - background-color: #1abc9c; } - -.dk_option_current a { - background-color: #1abc9c; } - -.dk_options_inner { - background-color: #34495e; - border-radius: 5px; - margin: 0; - max-height: 244px; - padding: 3px 3px 0; } - -.dk_touch .dk_options { - max-height: 250px; } - -.dk_container { - display: none; - position: relative; - vertical-align: middle; } - .dk_container.dk_shown { - display: inline-block; - zoom: 1; - *display: inline; } - .dk_container[class*="span"] { - float: none; - margin-left: 0; } - -.dk_toggle { - display: block; - position: relative; } - -.dk_open { - position: relative; } - .dk_open .dk_options { - margin-top: -1px; - opacity: 1; - z-index: 10; - display: block\9; } - .dk_open .dk_label { - color: inherit; } - -.dk_options { - margin-top: -21px; - position: absolute; - left: 0; - opacity: 0; - width: 220px; - z-index: -100; - display: none\9; - -webkit-transition: 0.3s ease-out; - -moz-transition: 0.3s ease-out; - -o-transition: 0.3s ease-out; - transition: 0.3s ease-out; - -webkit-backface-visibility: hidden; } - .select-right .dk_options { - left: auto; - right: 0; } - .dk_options a { - display: block; } - -.dk_options_inner { - overflow: auto; - outline: none; - position: relative; } - -.dk_touch .dk_options { - overflow: hidden; } -.dk_touch .dk_options_inner { - max-height: none; - overflow: visible; } - -.dk_fouc select { - position: relative; - top: -99999em; - visibility: hidden; } - -textarea, -input[type="text"], -input[type="password"], -input[type="datetime"], -input[type="datetime-local"], -input[type="date"], -input[type="month"], -input[type="time"], -input[type="week"], -input[type="number"], -input[type="email"], -input[type="url"], -input[type="search"], -input[type="tel"], -input[type="color"], -.uneditable-input { - border: 2px solid #dce4ec; - color: #34495e; - font-family: "Lato", sans-serif; - font-size: 14px; - padding: 8px 0 9px 10px; - text-indent: 1px; - -webkit-border-radius: 6px; - -moz-border-radius: 6px; - border-radius: 6px; - -webkit-box-shadow: none; - -moz-box-shadow: none; - box-shadow: none; } - textarea:-moz-placeholder, - input[type="text"]:-moz-placeholder, - input[type="password"]:-moz-placeholder, - input[type="datetime"]:-moz-placeholder, - input[type="datetime-local"]:-moz-placeholder, - input[type="date"]:-moz-placeholder, - input[type="month"]:-moz-placeholder, - input[type="time"]:-moz-placeholder, - input[type="week"]:-moz-placeholder, - input[type="number"]:-moz-placeholder, - input[type="email"]:-moz-placeholder, - input[type="url"]:-moz-placeholder, - input[type="search"]:-moz-placeholder, - input[type="tel"]:-moz-placeholder, - input[type="color"]:-moz-placeholder, - .uneditable-input:-moz-placeholder { - color: #acb6c0; } - textarea::-webkit-input-placeholder, - input[type="text"]::-webkit-input-placeholder, - input[type="password"]::-webkit-input-placeholder, - input[type="datetime"]::-webkit-input-placeholder, - input[type="datetime-local"]::-webkit-input-placeholder, - input[type="date"]::-webkit-input-placeholder, - input[type="month"]::-webkit-input-placeholder, - input[type="time"]::-webkit-input-placeholder, - input[type="week"]::-webkit-input-placeholder, - input[type="number"]::-webkit-input-placeholder, - input[type="email"]::-webkit-input-placeholder, - input[type="url"]::-webkit-input-placeholder, - input[type="search"]::-webkit-input-placeholder, - input[type="tel"]::-webkit-input-placeholder, - input[type="color"]::-webkit-input-placeholder, - .uneditable-input::-webkit-input-placeholder { - color: #acb6c0; } - textarea.placeholder, - input[type="text"].placeholder, - input[type="password"].placeholder, - input[type="datetime"].placeholder, - input[type="datetime-local"].placeholder, - input[type="date"].placeholder, - input[type="month"].placeholder, - input[type="time"].placeholder, - input[type="week"].placeholder, - input[type="number"].placeholder, - input[type="email"].placeholder, - input[type="url"].placeholder, - input[type="search"].placeholder, - input[type="tel"].placeholder, - input[type="color"].placeholder, - .uneditable-input.placeholder { - color: #acb6c0; } - textarea:focus, - input[type="text"]:focus, - input[type="password"]:focus, - input[type="datetime"]:focus, - input[type="datetime-local"]:focus, - input[type="date"]:focus, - input[type="month"]:focus, - input[type="time"]:focus, - input[type="week"]:focus, - input[type="number"]:focus, - input[type="email"]:focus, - input[type="url"]:focus, - input[type="search"]:focus, - input[type="tel"]:focus, - input[type="color"]:focus, - .uneditable-input:focus { - border-color: #484948; - -webkit-box-shadow: none; - -moz-box-shadow: none; - box-shadow: none; } - .control-group.error textarea, .control-group.error - input[type="text"], .control-group.error - input[type="password"], .control-group.error - input[type="datetime"], .control-group.error - input[type="datetime-local"], .control-group.error - input[type="date"], .control-group.error - input[type="month"], .control-group.error - input[type="time"], .control-group.error - input[type="week"], .control-group.error - input[type="number"], .control-group.error - input[type="email"], .control-group.error - input[type="url"], .control-group.error - input[type="search"], .control-group.error - input[type="tel"], .control-group.error - input[type="color"], .control-group.error - .uneditable-input { - border-color: #e74c3c; - color: #e74c3c; - -webkit-box-shadow: none; - -moz-box-shadow: none; - box-shadow: none; } - .control-group.error textarea:focus, .control-group.error - input[type="text"]:focus, .control-group.error - input[type="password"]:focus, .control-group.error - input[type="datetime"]:focus, .control-group.error - input[type="datetime-local"]:focus, .control-group.error - input[type="date"]:focus, .control-group.error - input[type="month"]:focus, .control-group.error - input[type="time"]:focus, .control-group.error - input[type="week"]:focus, .control-group.error - input[type="number"]:focus, .control-group.error - input[type="email"]:focus, .control-group.error - input[type="url"]:focus, .control-group.error - input[type="search"]:focus, .control-group.error - input[type="tel"]:focus, .control-group.error - input[type="color"]:focus, .control-group.error - .uneditable-input:focus { - -webkit-box-shadow: none; - -moz-box-shadow: none; - box-shadow: none; } - .control-group.success textarea, .control-group.success - input[type="text"], .control-group.success - input[type="password"], .control-group.success - input[type="datetime"], .control-group.success - input[type="datetime-local"], .control-group.success - input[type="date"], .control-group.success - input[type="month"], .control-group.success - input[type="time"], .control-group.success - input[type="week"], .control-group.success - input[type="number"], .control-group.success - input[type="email"], .control-group.success - input[type="url"], .control-group.success - input[type="search"], .control-group.success - input[type="tel"], .control-group.success - input[type="color"], .control-group.success - .uneditable-input { - border-color: #2ecc71; - color: #2ecc71; - -webkit-box-shadow: none; - -moz-box-shadow: none; - box-shadow: none; } - .control-group.success textarea:focus, .control-group.success - input[type="text"]:focus, .control-group.success - input[type="password"]:focus, .control-group.success - input[type="datetime"]:focus, .control-group.success - input[type="datetime-local"]:focus, .control-group.success - input[type="date"]:focus, .control-group.success - input[type="month"]:focus, .control-group.success - input[type="time"]:focus, .control-group.success - input[type="week"]:focus, .control-group.success - input[type="number"]:focus, .control-group.success - input[type="email"]:focus, .control-group.success - input[type="url"]:focus, .control-group.success - input[type="search"]:focus, .control-group.success - input[type="tel"]:focus, .control-group.success - input[type="color"]:focus, .control-group.success - .uneditable-input:focus { - -webkit-box-shadow: none; - -moz-box-shadow: none; - box-shadow: none; } - .control-group.warning textarea, .control-group.warning - input[type="text"], .control-group.warning - input[type="password"], .control-group.warning - input[type="datetime"], .control-group.warning - input[type="datetime-local"], .control-group.warning - input[type="date"], .control-group.warning - input[type="month"], .control-group.warning - input[type="time"], .control-group.warning - input[type="week"], .control-group.warning - input[type="number"], .control-group.warning - input[type="email"], .control-group.warning - input[type="url"], .control-group.warning - input[type="search"], .control-group.warning - input[type="tel"], .control-group.warning - input[type="color"], .control-group.warning - .uneditable-input { - border-color: #f1c40f; - color: #f1c40f; - -webkit-box-shadow: none; - -moz-box-shadow: none; - box-shadow: none; } - .control-group.warning textarea:focus, .control-group.warning - input[type="text"]:focus, .control-group.warning - input[type="password"]:focus, .control-group.warning - input[type="datetime"]:focus, .control-group.warning - input[type="datetime-local"]:focus, .control-group.warning - input[type="date"]:focus, .control-group.warning - input[type="month"]:focus, .control-group.warning - input[type="time"]:focus, .control-group.warning - input[type="week"]:focus, .control-group.warning - input[type="number"]:focus, .control-group.warning - input[type="email"]:focus, .control-group.warning - input[type="url"]:focus, .control-group.warning - input[type="search"]:focus, .control-group.warning - input[type="tel"]:focus, .control-group.warning - input[type="color"]:focus, .control-group.warning - .uneditable-input:focus { - -webkit-box-shadow: none; - -moz-box-shadow: none; - box-shadow: none; } - .control-group.info textarea, .control-group.info - input[type="text"], .control-group.info - input[type="password"], .control-group.info - input[type="datetime"], .control-group.info - input[type="datetime-local"], .control-group.info - input[type="date"], .control-group.info - input[type="month"], .control-group.info - input[type="time"], .control-group.info - input[type="week"], .control-group.info - input[type="number"], .control-group.info - input[type="email"], .control-group.info - input[type="url"], .control-group.info - input[type="search"], .control-group.info - input[type="tel"], .control-group.info - input[type="color"], .control-group.info - .uneditable-input { - border-color: #3498db; - color: #3498db; - -webkit-box-shadow: none; - -moz-box-shadow: none; - box-shadow: none; } - .control-group.info textarea:focus, .control-group.info - input[type="text"]:focus, .control-group.info - input[type="password"]:focus, .control-group.info - input[type="datetime"]:focus, .control-group.info - input[type="datetime-local"]:focus, .control-group.info - input[type="date"]:focus, .control-group.info - input[type="month"]:focus, .control-group.info - input[type="time"]:focus, .control-group.info - input[type="week"]:focus, .control-group.info - input[type="number"]:focus, .control-group.info - input[type="email"]:focus, .control-group.info - input[type="url"]:focus, .control-group.info - input[type="search"]:focus, .control-group.info - input[type="tel"]:focus, .control-group.info - input[type="color"]:focus, .control-group.info - .uneditable-input:focus { - -webkit-box-shadow: none; - -moz-box-shadow: none; - box-shadow: none; } - -input[disabled], -input[readonly], -textarea[disabled], -textarea[readonly] { - background-color: #eaeded; - border-color: transparent; - color: #cad2d3; - cursor: default; } - -input, -textarea, -.uneditable-input { - width: 192px; } - -.checkbox, -.radio { - margin-bottom: 12px; - padding-left: 32px; - position: relative; - -webkit-transition: 0.25s; - -moz-transition: 0.25s; - -o-transition: 0.25s; - transition: 0.25s; - -webkit-backface-visibility: hidden; } - .checkbox:hover, - .radio:hover { - color: #1abc9c; } - .checkbox input, - .radio input { - outline: none !important; - opacity: 0; - filter: alpha(opacity=0); - zoom: 1; } - .checkbox.checked .icon, - .radio.checked .icon { - background-position: -60px -30px; - opacity: 1; - display: block\9; } - .checkbox.checked .icon-to-fade, - .radio.checked .icon-to-fade { - opacity: 0; - display: none\9; } - .checkbox.disabled, - .radio.disabled { - color: #d7dddd; - cursor: default; } - .checkbox.disabled .icon, - .radio.disabled .icon { - opacity: 0; - display: none\9; } - .checkbox.disabled .icon-to-fade, - .radio.disabled .icon-to-fade { - background-position: -30px -60px; - opacity: 1; - display: block\9; } - .checkbox.disabled.checked .icon, - .radio.disabled.checked .icon { - background-position: 0 -90px; - opacity: 1; - display: block\9; } - .checkbox.disabled.checked .icon-to-fade, - .radio.disabled.checked .icon-to-fade { - opacity: 0; - display: none\9; } - .checkbox .icon, - .checkbox .icon-to-fade, - .radio .icon, - .radio .icon-to-fade { - background: url("../images/checkbox.png") -90px 0 no-repeat; - display: block; - height: 20px; - left: 0; - opacity: 1; - position: absolute; - top: -1px; - width: 20px; - -webkit-transition: opacity 0.1s linear; - -moz-transition: opacity 0.1s linear; - -o-transition: opacity 0.1s linear; - transition: opacity 0.1s linear; - -webkit-backface-visibility: hidden; } - .checkbox .icon, - .radio .icon { - opacity: 0; - top: 0; - z-index: 2; - display: none\9; } - -.radio .icon, -.radio .icon-to-fade { - background-image: url("../images/radio.png"); } - -@media only screen and (-webkit-min-device-pixel-ratio: 2), only screen and (-webkit-min-device-pixel-ratio: 1.5), only screen and (-moz-min-device-pixel-ratio: 2), only screen and (-o-min-device-pixel-ratio: 3 / 2), only screen and (-o-min-device-pixel-ratio: 2 / 1), only screen and (min--moz-device-pixel-ratio: 1.5), only screen and (min-device-pixel-ratio: 1.5), only screen and (min-device-pixel-ratio: 2) { - .checkbox .icon { - background-image: url("../images/checkbox-2x.png"); - background-size: 110px 110px; } - - .radio .icon { - background-image: url("../images/radio-2x.png"); - background-size: 110px 110px; } } -.toggle { - background-color: #34495e; - border-radius: 60px; - color: white; - height: 29px; - margin: 0 12px 12px 0; - overflow: hidden; - *zoom: 1; - display: inline-block; - zoom: 1; - *display: inline; - -webkit-transition: 0.25s; - -moz-transition: 0.25s; - -o-transition: 0.25s; - transition: 0.25s; - -webkit-backface-visibility: hidden; } - .toggle:before, .toggle:after { - display: table; - content: ""; } - .toggle:after { - clear: both; } - .toggle.toggle-off { - background-color: #cbd2d8; } - .toggle.toggle-off .toggle-radio { - background-image: url("../images/toggle/icon-off.png"); - background-position: 0 0; - color: white; - left: 0; - margin-left: 0.5px; - margin-right: -13px; - z-index: 1; } - .toggle.toggle-off .toggle-radio:first-child { - left: -120%; } - .toggle .toggle-radio { - background: url("../images/toggle/icon-on.png") right top no-repeat; - color: #1abc9c; - display: block; - font-weight: 700; - height: 21px; - left: 120%; - margin-left: -13px; - padding: 5px 32px 3px; - position: relative; - text-align: center; - z-index: 2; - -webkit-transition: 0.25s; - -moz-transition: 0.25s; - -o-transition: 0.25s; - transition: 0.25s; - -webkit-backface-visibility: hidden; } - .toggle .toggle-radio:first-child { - margin-bottom: -29px; - left: 0; } - .toggle input { - display: none; - position: absolute; - outline: none !important; - display: block\9; - opacity: 0.01; - filter: alpha(opacity=1); - zoom: 1; } - .toggle.toggle-icon { - border-radius: 6px 7px 7px 6px; } - .toggle.toggle-icon.toggle-off { - border-radius: 7px 6px 6px 7px; } - .toggle.toggle-icon.toggle-off .toggle-radio { - background-image: url("../images/toggle/block-off.png"); - background-position: 0 0; } - .toggle.toggle-icon .toggle-radio { - background-image: url("../images/toggle/block-on.png"); - background-position: 62px 0; - border-radius: 6px; - min-width: 27px; - text-align: right; } - .toggle.toggle-icon .toggle-radio:first-child { - text-align: left; } - -@media only screen and (-webkit-min-device-pixel-ratio: 2), only screen and (-webkit-min-device-pixel-ratio: 1.5), only screen and (-moz-min-device-pixel-ratio: 2), only screen and (-o-min-device-pixel-ratio: 3 / 2), only screen and (-o-min-device-pixel-ratio: 2 / 1), only screen and (min--moz-device-pixel-ratio: 1.5), only screen and (min-device-pixel-ratio: 1.5), only screen and (min-device-pixel-ratio: 2) { - .toggle.toggle-off .toggle-radio { - background-image: url("../images/toggle/icon-off-2x.png"); - background-size: 30px 29px; } - .toggle .toggle-radio { - background-image: url("../images/toggle/icon-on-2x.png"); - background-size: 30px 29px; } } -.tagsinput { - background: white; - border: 2px solid #1abc9c; - border-radius: 6px; - height: 100px; - padding: 6px 1px 1px 6px; - overflow-y: auto; - text-align: left; } - .tagsinput .tag { - border-radius: 4px; - background: #1abc9c; - color: white; - cursor: pointer; - margin-right: 5px; - margin-bottom: 5px; - overflow: hidden; - padding: 6px 13px 6px 19px; - position: relative; - vertical-align: middle; - display: inline-block; - zoom: 1; - *display: inline; - -webkit-transition: 0.14s linear; - -moz-transition: 0.14s linear; - -o-transition: 0.14s linear; - transition: 0.14s linear; - -webkit-backface-visibility: hidden; } - .tagsinput .tag:hover { - background-color: #16a085; - padding-left: 12px; - padding-right: 20px; } - .tagsinput .tag:hover .tagsinput-remove-link { - color: white; - opacity: 1; - display: block\9; } - .tagsinput input { - background: transparent; - border: none; - color: #34495e; - font-family: "Lato", sans-serif; - font-size: 14px; - margin: 0px; - padding: 0 0 0 5px; - outline: 0; - margin-right: 5px; - margin-bottom: 5px; - width: 12px; } - -.tagsinput-remove-link { - bottom: 0; - color: white; - cursor: pointer; - font-size: 12px; - opacity: 0; - padding: 9px 7px 3px 0; - position: absolute; - right: 0; - text-align: right; - text-decoration: none; - top: 0; - width: 100%; - z-index: 2; - display: none\9; } - .tagsinput-remove-link:before { - color: white; } - -.tagsinput-add-container { - vertical-align: middle; - display: inline-block; - zoom: 1; - *display: inline; } - -.tagsinput-add { - background-color: #bbc3cb; - border-radius: 3px; - color: white; - cursor: pointer; - margin-bottom: 5px; - padding: 6px 9px; - display: inline-block; - zoom: 1; - *display: inline; - -webkit-transition: 0.25s; - -moz-transition: 0.25s; - -o-transition: 0.25s; - transition: 0.25s; - -webkit-backface-visibility: hidden; } - .tagsinput-add:hover { - background-color: #1abc9c; } - -.tags_clear { - clear: both; - width: 100%; - height: 0px; } - -.not_valid { - background: #fbd8db !important; - color: #90111a !important; } - -.progress, .ui-slider { - background: #e8edf2; - border-radius: 32px; - height: 12px; - -webkit-box-shadow: none; - -moz-box-shadow: none; - box-shadow: none; - filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); } - .progress .bar, .ui-slider .bar { - background: #1abc9c; - -webkit-box-shadow: none !important; - -moz-box-shadow: none !important; - box-shadow: none !important; - filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); } - .progress .bar-success, .ui-slider .bar-success { - background-color: #2ecc71; - filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); } - .progress .bar-warning, .ui-slider .bar-warning { - background-color: #f1c40f; - filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); } - .progress .bar-danger, .ui-slider .bar-danger { - background-color: #e74c3c; - filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); } - .progress .bar-info, .ui-slider .bar-info { - background-color: #3498db; - filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); } - -.ui-slider { - margin-bottom: 20px; - position: relative; } - -.ui-slider-handle { - background-color: #16a085; - border-radius: 50%; - cursor: pointer; - height: 18px; - margin-left: -9px; - position: absolute; - top: -3px; - width: 18px; - z-index: 2; - -webkit-transition: background 0.25s; - -moz-transition: background 0.25s; - -o-transition: background 0.25s; - transition: background 0.25s; - -webkit-backface-visibility: hidden; } - .ui-slider-handle[style*='100'] { - margin-left: -15px; } - .ui-slider-handle:hover, .ui-slider-handle:focus { - background-color: #2fe2bf; - outline: none; } - .ui-slider-handle:active { - background-color: #16a085; } - -.ui-slider-range { - background-color: #1abc9c; - border-radius: 30px 0 0 30px; - display: block; - height: 100%; - position: absolute; - z-index: 1; } - -.ui-slider-segment { - background-color: #d6dbe0; - border-radius: 50%; - float: left; - height: 6px; - margin: 3px -6px 0 25%; - width: 6px; } - -.pager { - background-color: #34495e; - border-radius: 6px; - color: white; - font-size: 16px; - font-weight: 700; - display: inline-block; - zoom: 1; - *display: inline; } - .pager li:first-child > a, .pager li:first-child > span { - border-left: none; - padding-left: 20px; - -webkit-border-radius: 6px 0 0 6px; - -moz-border-radius: 6px 0 0 6px; - border-radius: 6px 0 0 6px; } - .pager li:first-child > a img, .pager li:first-child > span img { - margin-left: 0; - margin-right: 13px; - margin-left: 0 \9; - margin-right: 9px \9; } - .pager li.pager-center { - padding: 9px 18px 10px; - padding-left: 0; - padding-right: 0; - display: inline-block; - zoom: 1; - *display: inline; } - .pager li.previous img, .pager li.next img { - height: 14px; - margin: -1px 0 0 13px; - margin-left: 9px \9; - vertical-align: middle; } - .pager li > a, .pager li > span { - background: none; - border: none; - border-left: 2px solid #2c3e50; - color: white; - padding: 9px 18px 10px; - padding-left: 7px; - text-decoration: none; - white-space: nowrap; - -webkit-border-radius: 0 6px 6px 0; - -moz-border-radius: 0 6px 6px 0; - border-radius: 0 6px 6px 0; } - .pager li > a:hover, .pager li > a:focus, .pager li > span:hover, .pager li > span:focus { - background-color: #4e6d8d; } - .pager li > a:active, .pager li > span:active { - background-color: #2c3e50; } - -.pagination ul { - background: #d7dce0; - -webkit-border-radius: 6px; - -moz-border-radius: 6px; - border-radius: 6px; - -webkit-box-shadow: none; - -moz-box-shadow: none; - box-shadow: none; } - .pagination ul > li:first-child { - -webkit-border-radius: 6px 0 0 6px; - -moz-border-radius: 6px 0 0 6px; - border-radius: 6px 0 0 6px; } - .pagination ul > li:last-child { - -webkit-border-radius: 0 6px 6px 0; - -moz-border-radius: 0 6px 6px 0; - border-radius: 0 6px 6px 0; } - .pagination ul > li.previous > a, .pagination ul > li.previous > span, .pagination ul > li.next > a, .pagination ul > li.next > span { - background: transparent; - border: none; - border-right: 2px solid white !important; - margin: 0 9px 0 0; - padding: 11px 17px 12px 17px; - -webkit-border-radius: 6px 0 0 6px; - -moz-border-radius: 6px 0 0 6px; - border-radius: 6px 0 0 6px; - -webkit-box-shadow: none !important; - -moz-box-shadow: none !important; - box-shadow: none !important; } - .pagination ul > li.next > a, .pagination ul > li.next > span { - border-left: 2px solid white !important; - margin-left: 9px; - margin-right: 0; - -webkit-border-radius: 0 6px 6px 0; - -moz-border-radius: 0 6px 6px 0; - border-radius: 0 6px 6px 0; } - .pagination ul > li.active > a, .pagination ul > li.active > span { - background-color: white; - border-color: white; - border-width: 2px; - color: #d7dce0; - margin: 10px 5px 9px; } - .pagination ul > li.active > a:hover, .pagination ul > li.active > a:focus, .pagination ul > li.active > span:hover, .pagination ul > li.active > span:focus { - background-color: white; - border-color: white; - color: #d7dce0; - -webkit-box-shadow: none; - -moz-box-shadow: none; - box-shadow: none; } - .pagination ul > li > a, .pagination ul > li > span { - background: white; - border: 5px solid #d7dce0; - border-radius: 50px; - color: white; - line-height: 16px; - margin: 7px 2px 6px; - padding: 0 4px; - -webkit-transition: background 0.2s ease-out, border-color 0s ease-out, color 0.2s ease-out; - -moz-transition: background 0.2s ease-out, border-color 0s ease-out, color 0.2s ease-out; - -o-transition: background 0.2s ease-out, border-color 0s ease-out, color 0.2s ease-out; - transition: background 0.2s ease-out, border-color 0s ease-out, color 0.2s ease-out; - -webkit-backface-visibility: hidden; } - .pagination ul > li > a:hover, .pagination ul > li > a :focus, .pagination ul > li > span:hover, .pagination ul > li > span :focus { - background-color: #F67100; - border-color: #F67100; - color: white; - -webkit-transition: background 0.2s ease-out, border-color 0.2s ease-out, color 0.2s ease-out; - -moz-transition: background 0.2s ease-out, border-color 0.2s ease-out, color 0.2s ease-out; - -o-transition: background 0.2s ease-out, border-color 0.2s ease-out, color 0.2s ease-out; - transition: background 0.2s ease-out, border-color 0.2s ease-out, color 0.2s ease-out; - -webkit-backface-visibility: hidden; } - .pagination ul > li > a:active, .pagination ul > li > span:active { - background-color: #16a085; - border-color: #16a085; } - .pagination ul img { - height: 14px; - margin-top: -1px; - vertical-align: middle; - width: 13px; } - -.share { - background-color: #ecf0f5; - border-radius: 6px; - position: relative; } - .share:before { - content: ""; - border-style: solid; - border-width: 0 9px 9px 9px; - border-color: transparent transparent #ecf0f5 transparent; - height: 0px; - position: absolute; - left: 23px; - top: -9px; - width: 0px; - -webkit-transform: rotate(360deg); } - .share ul { - list-style-type: none; - margin: 0; - padding: 15px; } - .share li { - padding-top: 11px; - *zoom: 1; } - .share li:before, .share li:after { - display: table; - content: ""; } - .share li:after { - clear: both; } - .share li:first-child { - padding-top: 0; } - .share .toggle { - float: right; - margin: 0; } - .share .btn { - -webkit-border-radius: 0 0 6px 6px; - -moz-border-radius: 0 0 6px 6px; - border-radius: 0 0 6px 6px; } - -.share-label { - float: left; - font-size: 15px; - padding-top: 5px; - width: 50%; } - -.palette { - color: white; - margin: 0; - padding: 15px; - text-transform: uppercase; } - .palette dt { - display: block; - font-weight: 500; - opacity: 0.8; } - .palette dd { - font-weight: 200; - margin-left: 0; - opacity: 0.8; } - -.palette-firm { - background-color: #1abc9c; } - -.palette-firm-dark { - background-color: #16a085; } - -.palette-success { - background-color: #2ecc71; } - -.palette-success-dark { - background-color: #27ad60; } - -.palette-info { - background-color: #3498db; } - -.palette-info-dark { - background-color: #2383c4; } - -.palette-warning { - background-color: #f1c40f; } - -.palette-warning-dark { - background-color: #cea70c; } - -.palette-danger { - background-color: #e74c3c; } - -.palette-danger-dark { - background-color: #dc2d1b; } - -.palette-night { - background-color: #34495e; } - -.palette-night-dark { - background-color: #2c3e50; } - -.palette-bright { - background-color: #f1c40f; } - -.palette-bright-dark { - background-color: #cea70c; } - -.palette-success-dark { - background-color: #27ae60; } - -.palette-info-dark { - background-color: #2980b9; } - -.palette-bright-dark { - background-color: #f39c12; } - -.palette-amethyst { - background-color: #9b59b6; } - -.palette-wisteria { - background-color: #8e44ad; } - -.palette-carrot { - background-color: #e67e22; } - -.palette-pumpkin { - background-color: #d35400; } - -.palette-alizarin { - background-color: #e74c3c; } - -.palette-pomegranate { - background-color: #c0392b; } - -.palette-clouds { - background-color: #ecf0f1; - color: #bdc3c7; } - -.palette-silver { - background-color: #bdc3c7; } - -.palette-concrete { - background-color: #95a5a6; } - -.palette-asbestos { - background-color: #7f8c8d; } - -.palette-paragraph { - color: #7f8c8d; - font-size: 12px; - line-height: 17px; } - .palette-paragraph span { - color: #bdc3c7; } - -.palette-headline { - color: #7f8c8d; - font-weight: 700; - margin-top: -5px; } - -.tile { - background-color: #ecf0f5; - border-radius: 6px; - padding: 14px; - position: relative; - text-align: center; } - .tile.tile-hot:before { - background: url("../images/tile/ribbon.png") 0 0 no-repeat; - background-size: 82px 82px; - content: ""; - height: 82px; - position: absolute; - right: -4px; - top: -4px; - width: 82px; } - .tile p { - font-size: 15px; - margin-bottom: 33px; } - -.tile-image { - height: 100px; - margin: 31px 0 27px; - vertical-align: bottom; } - .tile-image.big-illustration { - height: 111px; - margin-top: 20px; - width: 112px; } - -.tile-title { - font-size: 20px; - margin: 0; } - -@media only screen and (-webkit-min-device-pixel-ratio: 2), only screen and (-webkit-min-device-pixel-ratio: 1.5), only screen and (-moz-min-device-pixel-ratio: 2), only screen and (-o-min-device-pixel-ratio: 3 / 2), only screen and (-o-min-device-pixel-ratio: 2 / 1), only screen and (min--moz-device-pixel-ratio: 1.5), only screen and (min-device-pixel-ratio: 1.5), only screen and (min-device-pixel-ratio: 2) { - .tile.tile-hot:before { - background-image: url("../images/tile/ribbon-2x.png"); } } -.todo { - background-color: #2c3e50; - border-radius: 8px 8px 6px 6px; - color: #6285a8; - margin-bottom: 20px; } - .todo ul { - margin: 0; - list-style-type: none; } - .todo li { - background: #34495e url("../images/todo/todo.png") 92% center no-repeat; - background-size: 20px 20px; - cursor: pointer; - margin-top: 2px; - padding: 18px 42px 17px 25px; - position: relative; - -webkit-transition: 0.25s; - -moz-transition: 0.25s; - -o-transition: 0.25s; - transition: 0.25s; - -webkit-backface-visibility: hidden; } - .todo li:first-child { - margin-top: 0; } - .todo li:last-child { - border-radius: 0 0 6px 6px; - padding-bottom: 18px; } - .todo li.todo-done { - background: transparent url("../images/todo/done.png") 92% center no-repeat; - background-size: 20px 20px; - color: #1abc9c; } - .todo li.todo-done .todo-name { - color: #1abc9c; } - -.todo-search { - background: #1abc9c url("../images/todo/search.png") 92% center no-repeat; - background-size: 16px 16px; - border-radius: 6px 6px 0 0; - color: #34495e; - padding: 19px 25px 20px; } - -input.todo-search-field { - background: none; - border: none; - color: #34495e; - font-size: 19px; - font-weight: 700; - margin: 0; - line-height: 23px; - padding: 5px 0; - text-indent: 0; - -webkit-box-shadow: none; - -moz-box-shadow: none; - box-shadow: none; } - input.todo-search-field:-moz-placeholder { - color: #34495e; } - input.todo-search-field::-webkit-input-placeholder { - color: #34495e; } - input.todo-search-field.placeholder { - color: #34495e; } - -.todo-icon { - float: left; - font-size: 24px; - padding: 11px 22px 0 0; } - -.todo-content { - padding-top: 1px; - overflow: hidden; } - -.todo-name { - color: white; - font-size: 17px; - margin: 1px 0 3px; } - -@media only screen and (-webkit-min-device-pixel-ratio: 2), only screen and (-webkit-min-device-pixel-ratio: 1.5), only screen and (-moz-min-device-pixel-ratio: 2), only screen and (-o-min-device-pixel-ratio: 3 / 2), only screen and (-o-min-device-pixel-ratio: 2 / 1), only screen and (min--moz-device-pixel-ratio: 1.5), only screen and (min-device-pixel-ratio: 1.5), only screen and (min-device-pixel-ratio: 2) { - .todo li { - background-image: "../images/todo/todo-2x.png"; } - .todo li.todo-done { - background-image: "../images/todo/done-2x.png"; } - - .todo-search { - background-image: "../images/todo/search-2x.png"; } } -footer { - background-color: #eceff1; - color: #bdc1c5; - font-size: 15px; - padding: 0; } - footer a { - color: #a1a4a7; - font-weight: 700; } - footer p { - font-size: 15px; - line-height: 20px; } - -.footer-title { - margin: 0 0 22px; - padding-top: 21px; } - -.footer-brand { - display: block; - margin-bottom: 26px; - width: 220px; } - .footer-brand img { - width: 216px; } - -.footer-banner { - background-color: #1abc9c; - color: #cff3ec; - margin-left: 42px; - min-height: 286px; - padding: 0 30px 30px; } - .footer-banner .footer-title { - color: white; } - .footer-banner a { - color: #cff3ec; - text-decoration: underline; } - .footer-banner a:hover { - text-decoration: none; } - .footer-banner ul { - list-style-type: none; - margin: 0 0 26px; } - .footer-banner ul li { - border-top: 1px solid #1bc6a5; - line-height: 19px; - padding: 6px 0; } - .footer-banner ul li:first-child { - border-top: none; - padding-top: 1px; } - -.video-js { - background-color: #34495e; - border-radius: 6px 6px 0 0; - margin-top: -95px; - position: relative; - padding: 0; - font-size: 10px; - vertical-align: middle; } - .video-js .vjs-tech { - border-radius: 6px 6px 0 0; - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; } - .video-js:-moz-full-screen { - position: absolute; } - -body.vjs-full-window { - padding: 0; - margin: 0; - height: 100%; - overflow-y: auto; } - -.video-js.vjs-fullscreen { - position: fixed; - overflow: hidden; - z-index: 1000; - left: 0; - top: 0; - bottom: 0; - right: 0; - width: 100% !important; - height: 100% !important; - _position: absolute; } -.video-js:-webkit-full-screen { - width: 100% !important; - height: 100% !important; } - -.vjs-poster { - margin: 0 auto; - padding: 0; - cursor: pointer; - position: relative; - width: 100%; - max-height: 100%; } - -.video-js .vjs-text-track-display { - text-align: center; - position: absolute; - bottom: 4em; - left: 1em; - right: 1em; - font-family: "Lato", sans-serif; } -.video-js .vjs-text-track { - display: none; - color: white; - font-size: 1.4em; - text-align: center; - margin-bottom: 0.1em; - background: black; - background: rgba(0, 0, 0, 0.5); } -.video-js .vjs-subtitles { - color: white; } -.video-js .vjs-captions { - color: #ffcc66; } - -.vjs-tt-cue { - display: block; } - -.vjs-fade-in { - visibility: visible !important; - opacity: 1 !important; - -webkit-transition: visibility 0s linear 0s, opacity 0.3s linear; - -moz-transition: visibility 0s linear 0s, opacity 0.3s linear; - -o-transition: visibility 0s linear 0s, opacity 0.3s linear; - transition: visibility 0s linear 0s, opacity 0.3s linear; - -webkit-backface-visibility: hidden; } - -.vjs-fade-out { - visibility: hidden !important; - opacity: 0 !important; - -webkit-transition: visibility 0s linear 1.5s, opacity 1.5s linear; - -moz-transition: visibility 0s linear 1.5s, opacity 1.5s linear; - -o-transition: visibility 0s linear 1.5s, opacity 1.5s linear; - transition: visibility 0s linear 1.5s, opacity 1.5s linear; - -webkit-backface-visibility: hidden; } - -.vjs-controls { - border-radius: 0 0 6px 6px; - position: absolute; - bottom: -47px; - left: 0; - right: 0; - margin: 0; - padding: 0; - height: 47px; - color: white; - background: #2c3e50; } - .vjs-controls.vjs-fade-out { - visibility: visible !important; - opacity: 1 !important; } - -.vjs-control { - background-position: center center; - background-repeat: no-repeat; - position: relative; - float: left; - text-align: center; - margin: 0; - padding: 0; - height: 18px; - width: 18px; } - .vjs-control:focus { - outline: 0; } - .vjs-control div { - background-position: center center; - background-repeat: no-repeat; } - -.vjs-control-text { - border: 0; - clip: rect(0 0 0 0); - height: 1px; - margin: -1px; - overflow: hidden; - padding: 0; - position: absolute; - width: 1px; } - -.vjs-play-control { - cursor: pointer !important; - height: 47px; - left: 0; - position: absolute; - top: 0; - width: 58px; } - -.vjs-paused .vjs-play-control { - background: url("../images/video/play.png") center -31px no-repeat; - background-size: 16px 64px; } - .vjs-paused .vjs-play-control:hover div { - opacity: 0; } - .vjs-paused .vjs-play-control div { - background: url("../images/video/play.png") center 15px no-repeat; - background-size: 16px 64px; - height: 47px; - -webkit-transition: opacity 0.25s; - -moz-transition: opacity 0.25s; - -o-transition: opacity 0.25s; - transition: opacity 0.25s; - -webkit-backface-visibility: hidden; } - -.vjs-playing .vjs-play-control { - background: url("../images/video/pause.png") center -31px no-repeat; - background-size: 15px 64px; } - .vjs-playing .vjs-play-control:hover div { - opacity: 0; } - .vjs-playing .vjs-play-control div { - background: url("../images/video/pause.png") center 15px no-repeat; - background-size: 15px 64px; - height: 47px; - -webkit-transition: opacity 0.25s; - -moz-transition: opacity 0.25s; - -o-transition: opacity 0.25s; - transition: opacity 0.25s; - -webkit-backface-visibility: hidden; } - -.vjs-rewind-control { - width: 5em; - cursor: pointer !important; } - .vjs-rewind-control div { - width: 19px; - height: 16px; - background: url("video-js.png"); - margin: 0.5em auto 0; } - -.vjs-mute-control { - background: url("../images/video/volume-full.png") center -48px no-repeat; - background-size: 16px 64px; - cursor: pointer !important; - position: absolute; - right: 51px; - top: 14px; } - .vjs-mute-control:hover div, .vjs-mute-control:focus div { - opacity: 0; } - .vjs-mute-control.vjs-vol-0, - .vjs-mute-control.vjs-vol-0 div { - background-image: url("../images/video/volume-off.png"); } - .vjs-mute-control div { - background: #2c3e50 url("../images/video/volume-full.png") no-repeat center 2px; - background-size: 16px 64px; - height: 18px; - -webkit-transition: opacity 0.25s; - -moz-transition: opacity 0.25s; - -o-transition: opacity 0.25s; - transition: opacity 0.25s; - -webkit-backface-visibility: hidden; } - -.vjs-volume-control, -.vjs-volume-level, -.vjs-volume-handle, -.vjs-volume-bar { - display: none; } - -.vjs-progress-control { - border-radius: 32px; - position: absolute; - left: 60px; - right: 180px; - height: 12px; - width: auto; - top: 18px; - background: #eff2f6; } - -.vjs-progress-holder { - position: relative; - cursor: pointer !important; - padding: 0; - margin: 0; - height: 12px; } - -.vjs-play-progress, .vjs-load-progress { - border-radius: 32px; - position: absolute; - display: block; - height: 12px; - margin: 0; - padding: 0; - left: 0; - top: 0; } - -.vjs-play-progress { - background: #1abc9c; - left: -1px; } - -.vjs-load-progress { - background: #d6dbe0; - border-radius: 32px 0 0 32px; } - .vjs-load-progress[style*='100%'], .vjs-load-progress[style*='99%'] { - border-radius: 32px; } - -.vjs-seek-handle { - background-color: #16a085; - border-radius: 50%; - position: absolute; - width: 18px; - height: 18px; - margin: -3px 0 0 1px; - left: 0; - top: 0; - -webkit-transition: background-color 0.25s; - -moz-transition: background-color 0.25s; - -o-transition: background-color 0.25s; - transition: background-color 0.25s; - -webkit-backface-visibility: hidden; } - .vjs-seek-handle[style*='95.'] { - margin-left: 3px; } - .vjs-seek-handle[style='left: 0%;'] { - margin-left: -2px; } - .vjs-seek-handle:hover, .vjs-seek-handle:focus { - background-color: #138d75; } - .vjs-seek-handle:active { - background-color: #117e69; } - -.vjs-time-controls { - position: absolute; - height: 20px; - width: 50px; - top: 16px; - font: 300 13px "Lato", sans-serif; } - -.vjs-current-time { - right: 128px; - text-align: right; } - -.vjs-duration { - color: #667687; - right: 69px; - text-align: left; } - -.vjs-remaining-time { - display: none; } - -.vjs-time-divider { - color: #667687; - font-size: 14px; - position: absolute; - right: 121px; - top: 15px; } - -.vjs-secondary-controls { - float: right; } - -.vjs-fullscreen-control { - background-image: url("../images/video/fullscreen.png"); - background-position: center -47px; - background-size: 15px 64px; - cursor: pointer !important; - position: absolute; - right: 17px; - top: 13px; } - .vjs-fullscreen-control:hover div, .vjs-fullscreen-control:focus div { - opacity: 0; } - .vjs-fullscreen-control div { - height: 18px; - background: url("../images/video/fullscreen.png") no-repeat center 2px; - background-size: 15px 64px; - -webkit-transition: opacity 0.25s; - -moz-transition: opacity 0.25s; - -o-transition: opacity 0.25s; - transition: opacity 0.25s; - -webkit-backface-visibility: hidden; } - -.vjs-menu-button { - display: none !important; } - -@-webkit-keyframes sharp { - 0% { - background: #e74c3c; - border-radius: 10px; - -webkit-transform: rotate(0deg); - -moz-transform: rotate(0deg); - -ms-transform: rotate(0deg); - -o-transform: rotate(0deg); - transform: rotate(0deg); } - - 50% { - background: #ebedee; - border-radius: 0; - -webkit-transform: rotate(180deg); - -moz-transform: rotate(180deg); - -ms-transform: rotate(180deg); - -o-transform: rotate(180deg); - transform: rotate(180deg); } - - 100% { - background: #e74c3c; - border-radius: 10px; - -webkit-transform: rotate(360deg); - -moz-transform: rotate(360deg); - -ms-transform: rotate(360deg); - -o-transform: rotate(360deg); - transform: rotate(360deg); } } - -@-moz-keyframes sharp { - 0% { - background: #e74c3c; - border-radius: 10px; - -webkit-transform: rotate(0deg); - -moz-transform: rotate(0deg); - -ms-transform: rotate(0deg); - -o-transform: rotate(0deg); - transform: rotate(0deg); } - - 50% { - background: #ebedee; - border-radius: 0; - -webkit-transform: rotate(180deg); - -moz-transform: rotate(180deg); - -ms-transform: rotate(180deg); - -o-transform: rotate(180deg); - transform: rotate(180deg); } - - 100% { - background: #e74c3c; - border-radius: 10px; - -webkit-transform: rotate(360deg); - -moz-transform: rotate(360deg); - -ms-transform: rotate(360deg); - -o-transform: rotate(360deg); - transform: rotate(360deg); } } - -@-o-keyframes sharp { - 0% { - background: #e74c3c; - border-radius: 10px; - -webkit-transform: rotate(0deg); - -moz-transform: rotate(0deg); - -ms-transform: rotate(0deg); - -o-transform: rotate(0deg); - transform: rotate(0deg); } - - 50% { - background: #ebedee; - border-radius: 0; - -webkit-transform: rotate(180deg); - -moz-transform: rotate(180deg); - -ms-transform: rotate(180deg); - -o-transform: rotate(180deg); - transform: rotate(180deg); } - - 100% { - background: #e74c3c; - border-radius: 10px; - -webkit-transform: rotate(360deg); - -moz-transform: rotate(360deg); - -ms-transform: rotate(360deg); - -o-transform: rotate(360deg); - transform: rotate(360deg); } } - -@keyframes sharp { - 0% { - background: #e74c3c; - border-radius: 10px; - -webkit-transform: rotate(0deg); - -moz-transform: rotate(0deg); - -ms-transform: rotate(0deg); - -o-transform: rotate(0deg); - transform: rotate(0deg); } - - 50% { - background: #ebedee; - border-radius: 0; - -webkit-transform: rotate(180deg); - -moz-transform: rotate(180deg); - -ms-transform: rotate(180deg); - -o-transform: rotate(180deg); - transform: rotate(180deg); } - - 100% { - background: #e74c3c; - border-radius: 10px; - -webkit-transform: rotate(360deg); - -moz-transform: rotate(360deg); - -ms-transform: rotate(360deg); - -o-transform: rotate(360deg); - transform: rotate(360deg); } } - -.vjs-loading-spinner { - background: #ebedee; - border-radius: 10px; - display: none; - height: 16px; - left: 50%; - margin: -8px 0 0 -8px; - position: absolute; - top: 50%; - width: 16px; - -webkit-animation: sharp 2s ease infinite; - -moz-animation: sharp 2s ease infinite; - -o-animation: sharp 2s ease infinite; - animation: sharp 2s ease infinite; } - -.login { - background: url("../images/login/imac.png") 0 0 no-repeat; - background-size: 940px 778px; - color: white; - margin-bottom: 77px; - padding: 38px 38px 267px; - position: relative; } - -.login-screen { - background-color: #1abc9c; - min-height: 317px; - padding: 123px 199px 33px 306px; } - -.login-icon { - left: 200px; - position: absolute; - top: 160px; - width: 96px; } - .login-icon > img { - display: block; - margin-bottom: 6px; - width: 100%; } - .login-icon > h4 { - font-size: 17px; - font-weight: 200; - line-height: 34px; - opacity: 0.95; } - .login-icon > h4 small { - color: inherit; - display: block; - font-size: inherit; - font-weight: 700; } - -.login-form { - background-color: #eceff1; - border-radius: 6px; - padding: 24px 23px 20px; - position: relative; } - .login-form:before { - content: ""; - border-style: solid; - border-width: 12px 12px 12px 0; - border-color: transparent #eceff1 transparent transparent; - height: 0px; - position: absolute; - left: -12px; - top: 35px; - width: 0; - -webkit-transform: rotate(360deg); } - .login-form .control-group { - margin-bottom: 6px; - position: relative; } - .login-form .login-field { - border-color: transparent; - font-size: 17px; - padding-bottom: 11px; - padding-top: 11px; - text-indent: 3px; - width: 299px; } - .login-form .login-field:focus + .login-field-icon { - color: #1abc9c; } - .login-form .login-field-icon { - color: #bfc9ca; - font-size: 16px; - position: absolute; - right: 13px; - top: 14px; - -webkit-transition: 0.25s; - -moz-transition: 0.25s; - -o-transition: 0.25s; - transition: 0.25s; - -webkit-backface-visibility: hidden; } - -.login-link { - color: #bfc9ca; - display: block; - font-size: 13px; - margin-top: 15px; - text-align: center; } - -@media only screen and (-webkit-min-device-pixel-ratio: 2), only screen and (-webkit-min-device-pixel-ratio: 1.5), only screen and (-moz-min-device-pixel-ratio: 2), only screen and (-o-min-device-pixel-ratio: 3 / 2), only screen and (-o-min-device-pixel-ratio: 2 / 1), only screen and (min--moz-device-pixel-ratio: 1.5), only screen and (min-device-pixel-ratio: 1.5), only screen and (min-device-pixel-ratio: 2) { - .login { - background-image: url("../images/login/imac-2x.png"); } } -.ptn, .pvn, .pan { - padding-top: 0; } - -.ptx, .pvx, .pax { - padding-top: 3px; } - -.pts, .pvs, .pas { - padding-top: 5px; } - -.ptm, .pvm, .pam { - padding-top: 10px; } - -.ptl, .pvl, .pal { - padding-top: 20px; } - -.prn, .phn, .pan { - padding-right: 0; } - -.prx, .phx, .pax { - padding-right: 3px; } - -.prs, .phs, .pas { - padding-right: 5px; } - -.prm, .phm, .pam { - padding-right: 10px; } - -.prl, .phl, .pal { - padding-right: 20px; } - -.pbn, .pvn, .pan { - padding-bottom: 0; } - -.pbx, .pvx, .pax { - padding-bottom: 3px; } - -.pbs, .pvs, .pas { - padding-bottom: 5px; } - -.pbm, .pvm, .pam { - padding-bottom: 10px; } - -.pbl, .pvl, .pal { - padding-bottom: 20px; } - -.pln, .phn, .pan { - padding-left: 0; } - -.plx, .phx, .pax { - padding-left: 3px; } - -.pls, .phs, .pas { - padding-left: 5px; } - -.plm, .phm, .pam { - padding-left: 10px; } - -.pll, .phl, .pal { - padding-left: 20px; } - -.mtn, .mvn, .man { - margin-top: 0px; } - -.mtx, .mvx, .max { - margin-top: 3px; } - -.mts, .mvs, .mas { - margin-top: 5px; } - -.mtm, .mvm, .mam { - margin-top: 10px; } - -.mtl, .mvl, .mal { - margin-top: 20px; } - -.mrn, .mhn, .man { - margin-right: 0px; } - -.mrx, .mhx, .max { - margin-right: 3px; } - -.mrs, .mhs, .mas { - margin-right: 5px; } - -.mrm, .mhm, .mam { - margin-right: 10px; } - -.mrl, .mhl, .mal { - margin-right: 20px; } - -.mbn, .mvn, .man { - margin-bottom: 0px; } - -.mbx, .mvx, .max { - margin-bottom: 3px; } - -.mbs, .mvs, .mas { - margin-bottom: 5px; } - -.mbm, .mvm, .mam { - margin-bottom: 10px; } - -.mbl, .mvl, .mal { - margin-bottom: 20px; } - -.mln, .mhn, .man { - margin-left: 0px; } - -.mlx, .mhx, .max { - margin-left: 3px; } - -.mls, .mhs, .mas { - margin-left: 5px; } - -.mlm, .mhm, .mam { - margin-left: 10px; } - -.mll, .mhl, .mal { - margin-left: 20px; } diff --git a/app/public/css/font-awesome-min.css b/app/public/css/font-awesome-min.css deleted file mode 100644 index 08f00c5db..000000000 --- a/app/public/css/font-awesome-min.css +++ /dev/null @@ -1,35 +0,0 @@ -/*! - * Font Awesome 3.1.0 - * the iconic font designed for Bootstrap - * ------------------------------------------------------- - * The full suite of pictographic icons, examples, and documentation - * can be found at: http://fontawesome.io - * - * License - * ------------------------------------------------------- - * - The Font Awesome font is licensed under the SIL Open Font License v1.1 - - * http://scripts.sil.org/OFL - * - Font Awesome CSS, LESS, and SASS files are licensed under the MIT License - - * http://opensource.org/licenses/mit-license.html - * - Font Awesome documentation licensed under CC BY 3.0 License - - * http://creativecommons.org/licenses/by/3.0/ - * - Attribution is no longer required in Font Awesome 3.0, but much appreciated: - * "Font Awesome by Dave Gandy - http://fontawesome.io" - - * Contact - * ------------------------------------------------------- - * Email: dave@fontawesome.io - * Twitter: http://twitter.com/fortaweso_me - * Work: Lead Product Designer @ http://kyruus.com - */ - @font-face{ - font-family:'FontAwesome'; - src:url('/fonts/fontawesome-webfont.eot?v=3.1.0'); - src:url('/fonts/fontawesome-webfont.eot?#iefix&v=3.1.0') format('embedded-opentype'), - url('/fonts/fontawesome-webfont.woff?v=3.1.0') format('woff'), - url('/fonts/fontawesome-webfont.ttf?v=3.1.0') format('truetype'), - url('/fonts/fontawesome-webfont.svg#fontawesomeregular?v=3.1.0') format('svg'); - font-weight:normal; - font-style:normal} - -[class^="icon-"],[class*=" icon-"]{font-family:FontAwesome;font-weight:normal;font-style:normal;text-decoration:inherit;-webkit-font-smoothing:antialiased;*margin-right:.3em}[class^="icon-"]:before,[class*=" icon-"]:before{text-decoration:inherit;display:inline-block;speak:none}.icon-large:before{vertical-align:-10%;font-size:1.3333333333333333em}a [class^="icon-"],a [class*=" icon-"],a [class^="icon-"]:before,a [class*=" icon-"]:before{display:inline}[class^="icon-"].icon-fixed-width,[class*=" icon-"].icon-fixed-width{display:inline-block;width:1.2857142857142858em;text-align:center}[class^="icon-"].icon-fixed-width.icon-large,[class*=" icon-"].icon-fixed-width.icon-large{width:1.5714285714285714em}ul.icons-ul{list-style-type:none;text-indent:-0.7142857142857143em;margin-left:2.142857142857143em}ul.icons-ul>li .icon-li{width:.7142857142857143em;display:inline-block;text-align:center}[class^="icon-"].hide,[class*=" icon-"].hide{display:none}.icon-muted{color:#eee}.icon-light{color:#fff}.icon-dark{color:#333}.icon-border{border:solid 1px #eee;padding:.2em .25em .15em;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px}.icon-2x{font-size:2em}.icon-2x.icon-border{border-width:2px;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px}.icon-3x{font-size:3em}.icon-3x.icon-border{border-width:3px;-webkit-border-radius:5px;-moz-border-radius:5px;border-radius:5px}.icon-4x{font-size:4em}.icon-4x.icon-border{border-width:4px;-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px}.icon-5x{font-size:5em}.icon-5x.icon-border{border-width:5px;-webkit-border-radius:7px;-moz-border-radius:7px;border-radius:7px}.pull-right{float:right}.pull-left{float:left}[class^="icon-"].pull-left,[class*=" icon-"].pull-left{margin-right:.3em}[class^="icon-"].pull-right,[class*=" icon-"].pull-right{margin-left:.3em}[class^="icon-"],[class*=" icon-"]{display:inline;width:auto;height:auto;line-height:normal;vertical-align:baseline;background-image:none;background-position:0 0;background-repeat:repeat;margin-top:0}.icon-white,.nav-pills>.active>a>[class^="icon-"],.nav-pills>.active>a>[class*=" icon-"],.nav-list>.active>a>[class^="icon-"],.nav-list>.active>a>[class*=" icon-"],.navbar-inverse .nav>.active>a>[class^="icon-"],.navbar-inverse .nav>.active>a>[class*=" icon-"],.dropdown-menu>li>a:hover>[class^="icon-"],.dropdown-menu>li>a:hover>[class*=" icon-"],.dropdown-menu>.active>a>[class^="icon-"],.dropdown-menu>.active>a>[class*=" icon-"],.dropdown-submenu:hover>a>[class^="icon-"],.dropdown-submenu:hover>a>[class*=" icon-"]{background-image:none}.btn [class^="icon-"].icon-large,.nav [class^="icon-"].icon-large,.btn [class*=" icon-"].icon-large,.nav [class*=" icon-"].icon-large{line-height:.9em}.btn [class^="icon-"].icon-spin,.nav [class^="icon-"].icon-spin,.btn [class*=" icon-"].icon-spin,.nav [class*=" icon-"].icon-spin{display:inline-block}.nav-tabs [class^="icon-"],.nav-pills [class^="icon-"],.nav-tabs [class*=" icon-"],.nav-pills [class*=" icon-"],.nav-tabs [class^="icon-"].icon-large,.nav-pills [class^="icon-"].icon-large,.nav-tabs [class*=" icon-"].icon-large,.nav-pills [class*=" icon-"].icon-large{line-height:.9em}.btn [class^="icon-"].pull-left.icon-2x,.btn [class*=" icon-"].pull-left.icon-2x,.btn [class^="icon-"].pull-right.icon-2x,.btn [class*=" icon-"].pull-right.icon-2x{margin-top:.18em}.btn [class^="icon-"].icon-spin.icon-large,.btn [class*=" icon-"].icon-spin.icon-large{line-height:.8em}.btn.btn-small [class^="icon-"].pull-left.icon-2x,.btn.btn-small [class*=" icon-"].pull-left.icon-2x,.btn.btn-small [class^="icon-"].pull-right.icon-2x,.btn.btn-small [class*=" icon-"].pull-right.icon-2x{margin-top:.25em}.btn.btn-large [class^="icon-"],.btn.btn-large [class*=" icon-"]{margin-top:0}.btn.btn-large [class^="icon-"].pull-left.icon-2x,.btn.btn-large [class*=" icon-"].pull-left.icon-2x,.btn.btn-large [class^="icon-"].pull-right.icon-2x,.btn.btn-large [class*=" icon-"].pull-right.icon-2x{margin-top:.05em}.btn.btn-large [class^="icon-"].pull-left.icon-2x,.btn.btn-large [class*=" icon-"].pull-left.icon-2x{margin-right:.2em}.btn.btn-large [class^="icon-"].pull-right.icon-2x,.btn.btn-large [class*=" icon-"].pull-right.icon-2x{margin-left:.2em}.icon-stack{position:relative;display:inline-block;width:2em;height:2em;line-height:2em;vertical-align:-35%}.icon-stack [class^="icon-"],.icon-stack [class*=" icon-"]{display:block;text-align:center;position:absolute;width:100%;height:100%;font-size:1em;line-height:inherit;*line-height:2em}.icon-stack .icon-stack-base{font-size:2em;*line-height:1em}.icon-spin{display:inline-block;-moz-animation:spin 2s infinite linear;-o-animation:spin 2s infinite linear;-webkit-animation:spin 2s infinite linear;animation:spin 2s infinite linear}@-moz-keyframes spin{0%{-moz-transform:rotate(0deg)}100%{-moz-transform:rotate(359deg)}}@-webkit-keyframes spin{0%{-webkit-transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg)}}@-o-keyframes spin{0%{-o-transform:rotate(0deg)}100%{-o-transform:rotate(359deg)}}@-ms-keyframes spin{0%{-ms-transform:rotate(0deg)}100%{-ms-transform:rotate(359deg)}}@keyframes spin{0%{transform:rotate(0deg)}100%{transform:rotate(359deg)}}.icon-rotate-90:before{-webkit-transform:rotate(90deg);-moz-transform:rotate(90deg);-ms-transform:rotate(90deg);-o-transform:rotate(90deg);transform:rotate(90deg);filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=1)}.icon-rotate-180:before{-webkit-transform:rotate(180deg);-moz-transform:rotate(180deg);-ms-transform:rotate(180deg);-o-transform:rotate(180deg);transform:rotate(180deg);filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=2)}.icon-rotate-270:before{-webkit-transform:rotate(270deg);-moz-transform:rotate(270deg);-ms-transform:rotate(270deg);-o-transform:rotate(270deg);transform:rotate(270deg);filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=3)}.icon-flip-horizontal:before{-webkit-transform:scale(-1,1);-moz-transform:scale(-1,1);-ms-transform:scale(-1,1);-o-transform:scale(-1,1);transform:scale(-1,1)}.icon-flip-vertical:before{-webkit-transform:scale(1,-1);-moz-transform:scale(1,-1);-ms-transform:scale(1,-1);-o-transform:scale(1,-1);transform:scale(1,-1)}.icon-glass:before{content:"\f000"}.icon-music:before{content:"\f001"}.icon-search:before{content:"\f002"}.icon-envelope:before{content:"\f003"}.icon-heart:before{content:"\f004"}.icon-star:before{content:"\f005"}.icon-star-empty:before{content:"\f006"}.icon-user:before{content:"\f007"}.icon-film:before{content:"\f008"}.icon-th-large:before{content:"\f009"}.icon-th:before{content:"\f00a"}.icon-th-list:before{content:"\f00b"}.icon-ok:before{content:"\f00c"}.icon-remove:before{content:"\f00d"}.icon-zoom-in:before{content:"\f00e"}.icon-zoom-out:before{content:"\f010"}.icon-off:before{content:"\f011"}.icon-signal:before{content:"\f012"}.icon-cog:before{content:"\f013"}.icon-trash:before{content:"\f014"}.icon-home:before{content:"\f015"}.icon-file:before{content:"\f016"}.icon-time:before{content:"\f017"}.icon-road:before{content:"\f018"}.icon-download-alt:before{content:"\f019"}.icon-download:before{content:"\f01a"}.icon-upload:before{content:"\f01b"}.icon-inbox:before{content:"\f01c"}.icon-play-circle:before{content:"\f01d"}.icon-repeat:before,.icon-rotate-right:before{content:"\f01e"}.icon-refresh:before{content:"\f021"}.icon-list-alt:before{content:"\f022"}.icon-lock:before{content:"\f023"}.icon-flag:before{content:"\f024"}.icon-headphones:before{content:"\f025"}.icon-volume-off:before{content:"\f026"}.icon-volume-down:before{content:"\f027"}.icon-volume-up:before{content:"\f028"}.icon-qrcode:before{content:"\f029"}.icon-barcode:before{content:"\f02a"}.icon-tag:before{content:"\f02b"}.icon-tags:before{content:"\f02c"}.icon-book:before{content:"\f02d"}.icon-bookmark:before{content:"\f02e"}.icon-print:before{content:"\f02f"}.icon-camera:before{content:"\f030"}.icon-font:before{content:"\f031"}.icon-bold:before{content:"\f032"}.icon-italic:before{content:"\f033"}.icon-text-height:before{content:"\f034"}.icon-text-width:before{content:"\f035"}.icon-align-left:before{content:"\f036"}.icon-align-center:before{content:"\f037"}.icon-align-right:before{content:"\f038"}.icon-align-justify:before{content:"\f039"}.icon-list:before{content:"\f03a"}.icon-indent-left:before{content:"\f03b"}.icon-indent-right:before{content:"\f03c"}.icon-facetime-video:before{content:"\f03d"}.icon-picture:before{content:"\f03e"}.icon-pencil:before{content:"\f040"}.icon-map-marker:before{content:"\f041"}.icon-adjust:before{content:"\f042"}.icon-tint:before{content:"\f043"}.icon-edit:before{content:"\f044"}.icon-share:before{content:"\f045"}.icon-check:before{content:"\f046"}.icon-move:before{content:"\f047"}.icon-step-backward:before{content:"\f048"}.icon-fast-backward:before{content:"\f049"}.icon-backward:before{content:"\f04a"}.icon-play:before{content:"\f04b"}.icon-pause:before{content:"\f04c"}.icon-stop:before{content:"\f04d"}.icon-forward:before{content:"\f04e"}.icon-fast-forward:before{content:"\f050"}.icon-step-forward:before{content:"\f051"}.icon-eject:before{content:"\f052"}.icon-chevron-left:before{content:"\f053"}.icon-chevron-right:before{content:"\f054"}.icon-plus-sign:before{content:"\f055"}.icon-minus-sign:before{content:"\f056"}.icon-remove-sign:before{content:"\f057"}.icon-ok-sign:before{content:"\f058"}.icon-question-sign:before{content:"\f059"}.icon-info-sign:before{content:"\f05a"}.icon-screenshot:before{content:"\f05b"}.icon-remove-circle:before{content:"\f05c"}.icon-ok-circle:before{content:"\f05d"}.icon-ban-circle:before{content:"\f05e"}.icon-arrow-left:before{content:"\f060"}.icon-arrow-right:before{content:"\f061"}.icon-arrow-up:before{content:"\f062"}.icon-arrow-down:before{content:"\f063"}.icon-share-alt:before,.icon-mail-forward:before{content:"\f064"}.icon-resize-full:before{content:"\f065"}.icon-resize-small:before{content:"\f066"}.icon-plus:before{content:"\f067"}.icon-minus:before{content:"\f068"}.icon-asterisk:before{content:"\f069"}.icon-exclamation-sign:before{content:"\f06a"}.icon-gift:before{content:"\f06b"}.icon-leaf:before{content:"\f06c"}.icon-fire:before{content:"\f06d"}.icon-eye-open:before{content:"\f06e"}.icon-eye-close:before{content:"\f070"}.icon-warning-sign:before{content:"\f071"}.icon-plane:before{content:"\f072"}.icon-calendar:before{content:"\f073"}.icon-random:before{content:"\f074"}.icon-comment:before{content:"\f075"}.icon-magnet:before{content:"\f076"}.icon-chevron-up:before{content:"\f077"}.icon-chevron-down:before{content:"\f078"}.icon-retweet:before{content:"\f079"}.icon-shopping-cart:before{content:"\f07a"}.icon-folder-close:before{content:"\f07b"}.icon-folder-open:before{content:"\f07c"}.icon-resize-vertical:before{content:"\f07d"}.icon-resize-horizontal:before{content:"\f07e"}.icon-bar-chart:before{content:"\f080"}.icon-twitter-sign:before{content:"\f081"}.icon-facebook-sign:before{content:"\f082"}.icon-camera-retro:before{content:"\f083"}.icon-key:before{content:"\f084"}.icon-cogs:before{content:"\f085"}.icon-comments:before{content:"\f086"}.icon-thumbs-up:before{content:"\f087"}.icon-thumbs-down:before{content:"\f088"}.icon-star-half:before{content:"\f089"}.icon-heart-empty:before{content:"\f08a"}.icon-signout:before{content:"\f08b"}.icon-linkedin-sign:before{content:"\f08c"}.icon-pushpin:before{content:"\f08d"}.icon-external-link:before{content:"\f08e"}.icon-signin:before{content:"\f090"}.icon-trophy:before{content:"\f091"}.icon-github-sign:before{content:"\f092"}.icon-upload-alt:before{content:"\f093"}.icon-lemon:before{content:"\f094"}.icon-phone:before{content:"\f095"}.icon-check-empty:before{content:"\f096"}.icon-bookmark-empty:before{content:"\f097"}.icon-phone-sign:before{content:"\f098"}.icon-twitter:before{content:"\f099"}.icon-facebook:before{content:"\f09a"}.icon-github:before{content:"\f09b"}.icon-unlock:before{content:"\f09c"}.icon-credit-card:before{content:"\f09d"}.icon-rss:before{content:"\f09e"}.icon-hdd:before{content:"\f0a0"}.icon-bullhorn:before{content:"\f0a1"}.icon-bell:before{content:"\f0a2"}.icon-certificate:before{content:"\f0a3"}.icon-hand-right:before{content:"\f0a4"}.icon-hand-left:before{content:"\f0a5"}.icon-hand-up:before{content:"\f0a6"}.icon-hand-down:before{content:"\f0a7"}.icon-circle-arrow-left:before{content:"\f0a8"}.icon-circle-arrow-right:before{content:"\f0a9"}.icon-circle-arrow-up:before{content:"\f0aa"}.icon-circle-arrow-down:before{content:"\f0ab"}.icon-globe:before{content:"\f0ac"}.icon-wrench:before{content:"\f0ad"}.icon-tasks:before{content:"\f0ae"}.icon-filter:before{content:"\f0b0"}.icon-briefcase:before{content:"\f0b1"}.icon-fullscreen:before{content:"\f0b2"}.icon-group:before{content:"\f0c0"}.icon-link:before{content:"\f0c1"}.icon-cloud:before{content:"\f0c2"}.icon-beaker:before{content:"\f0c3"}.icon-cut:before{content:"\f0c4"}.icon-copy:before{content:"\f0c5"}.icon-paper-clip:before{content:"\f0c6"}.icon-save:before{content:"\f0c7"}.icon-sign-blank:before{content:"\f0c8"}.icon-reorder:before{content:"\f0c9"}.icon-list-ul:before{content:"\f0ca"}.icon-list-ol:before{content:"\f0cb"}.icon-strikethrough:before{content:"\f0cc"}.icon-underline:before{content:"\f0cd"}.icon-table:before{content:"\f0ce"}.icon-magic:before{content:"\f0d0"}.icon-truck:before{content:"\f0d1"}.icon-pinterest:before{content:"\f0d2"}.icon-pinterest-sign:before{content:"\f0d3"}.icon-google-plus-sign:before{content:"\f0d4"}.icon-google-plus:before{content:"\f0d5"}.icon-money:before{content:"\f0d6"}.icon-caret-down:before{content:"\f0d7"}.icon-caret-up:before{content:"\f0d8"}.icon-caret-left:before{content:"\f0d9"}.icon-caret-right:before{content:"\f0da"}.icon-columns:before{content:"\f0db"}.icon-sort:before{content:"\f0dc"}.icon-sort-down:before{content:"\f0dd"}.icon-sort-up:before{content:"\f0de"}.icon-envelope-alt:before{content:"\f0e0"}.icon-linkedin:before{content:"\f0e1"}.icon-undo:before,.icon-rotate-left:before{content:"\f0e2"}.icon-legal:before{content:"\f0e3"}.icon-dashboard:before{content:"\f0e4"}.icon-comment-alt:before{content:"\f0e5"}.icon-comments-alt:before{content:"\f0e6"}.icon-bolt:before{content:"\f0e7"}.icon-sitemap:before{content:"\f0e8"}.icon-umbrella:before{content:"\f0e9"}.icon-paste:before{content:"\f0ea"}.icon-lightbulb:before{content:"\f0eb"}.icon-exchange:before{content:"\f0ec"}.icon-cloud-download:before{content:"\f0ed"}.icon-cloud-upload:before{content:"\f0ee"}.icon-user-md:before{content:"\f0f0"}.icon-stethoscope:before{content:"\f0f1"}.icon-suitcase:before{content:"\f0f2"}.icon-bell-alt:before{content:"\f0f3"}.icon-coffee:before{content:"\f0f4"}.icon-food:before{content:"\f0f5"}.icon-file-alt:before{content:"\f0f6"}.icon-building:before{content:"\f0f7"}.icon-hospital:before{content:"\f0f8"}.icon-ambulance:before{content:"\f0f9"}.icon-medkit:before{content:"\f0fa"}.icon-fighter-jet:before{content:"\f0fb"}.icon-beer:before{content:"\f0fc"}.icon-h-sign:before{content:"\f0fd"}.icon-plus-sign-alt:before{content:"\f0fe"}.icon-double-angle-left:before{content:"\f100"}.icon-double-angle-right:before{content:"\f101"}.icon-double-angle-up:before{content:"\f102"}.icon-double-angle-down:before{content:"\f103"}.icon-angle-left:before{content:"\f104"}.icon-angle-right:before{content:"\f105"}.icon-angle-up:before{content:"\f106"}.icon-angle-down:before{content:"\f107"}.icon-desktop:before{content:"\f108"}.icon-laptop:before{content:"\f109"}.icon-tablet:before{content:"\f10a"}.icon-mobile-phone:before{content:"\f10b"}.icon-circle-blank:before{content:"\f10c"}.icon-quote-left:before{content:"\f10d"}.icon-quote-right:before{content:"\f10e"}.icon-spinner:before{content:"\f110"}.icon-circle:before{content:"\f111"}.icon-reply:before,.icon-mail-reply:before{content:"\f112"}.icon-folder-close-alt:before{content:"\f114"}.icon-folder-open-alt:before{content:"\f115"}.icon-expand-alt:before{content:"\f116"}.icon-collapse-alt:before{content:"\f117"}.icon-smile:before{content:"\f118"}.icon-frown:before{content:"\f119"}.icon-meh:before{content:"\f11a"}.icon-gamepad:before{content:"\f11b"}.icon-keyboard:before{content:"\f11c"}.icon-flag-alt:before{content:"\f11d"}.icon-flag-checkered:before{content:"\f11e"}.icon-terminal:before{content:"\f120"}.icon-code:before{content:"\f121"}.icon-reply-all:before{content:"\f122"}.icon-mail-reply-all:before{content:"\f122"}.icon-star-half-full:before,.icon-star-half-empty:before{content:"\f123"}.icon-location-arrow:before{content:"\f124"}.icon-crop:before{content:"\f125"}.icon-code-fork:before{content:"\f126"}.icon-unlink:before{content:"\f127"}.icon-question:before{content:"\f128"}.icon-info:before{content:"\f129"}.icon-exclamation:before{content:"\f12a"}.icon-superscript:before{content:"\f12b"}.icon-subscript:before{content:"\f12c"}.icon-eraser:before{content:"\f12d"}.icon-puzzle-piece:before{content:"\f12e"}.icon-microphone:before{content:"\f130"}.icon-microphone-off:before{content:"\f131"}.icon-shield:before{content:"\f132"}.icon-calendar-empty:before{content:"\f133"}.icon-fire-extinguisher:before{content:"\f134"}.icon-rocket:before{content:"\f135"}.icon-maxcdn:before{content:"\f136"}.icon-chevron-sign-left:before{content:"\f137"}.icon-chevron-sign-right:before{content:"\f138"}.icon-chevron-sign-up:before{content:"\f139"}.icon-chevron-sign-down:before{content:"\f13a"}.icon-html5:before{content:"\f13b"}.icon-css3:before{content:"\f13c"}.icon-anchor:before{content:"\f13d"}.icon-unlock-alt:before{content:"\f13e"}.icon-bullseye:before{content:"\f140"}.icon-ellipsis-horizontal:before{content:"\f141"}.icon-ellipsis-vertical:before{content:"\f142"}.icon-rss-sign:before{content:"\f143"}.icon-play-sign:before{content:"\f144"}.icon-ticket:before{content:"\f145"}.icon-minus-sign-alt:before{content:"\f146"}.icon-check-minus:before{content:"\f147"}.icon-level-up:before{content:"\f148"}.icon-level-down:before{content:"\f149"}.icon-check-sign:before{content:"\f14a"}.icon-edit-sign:before{content:"\f14b"}.icon-external-link-sign:before{content:"\f14c"}.icon-share-sign:before{content:"\f14d"} \ No newline at end of file diff --git a/app/public/fonts/FontAwesome.otf b/app/public/fonts/FontAwesome.otf deleted file mode 100644 index 32dd8b1cd..000000000 Binary files a/app/public/fonts/FontAwesome.otf and /dev/null differ diff --git a/app/public/fonts/fontawesome-webfont.eot b/app/public/fonts/fontawesome-webfont.eot deleted file mode 100755 index c080283bd..000000000 Binary files a/app/public/fonts/fontawesome-webfont.eot and /dev/null differ diff --git a/app/public/fonts/fontawesome-webfont.svg b/app/public/fonts/fontawesome-webfont.svg deleted file mode 100755 index 10a1e1bbf..000000000 --- a/app/public/fonts/fontawesome-webfont.svg +++ /dev/null @@ -1,339 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/public/fonts/fontawesome-webfont.ttf b/app/public/fonts/fontawesome-webfont.ttf deleted file mode 100755 index 908f69ec9..000000000 Binary files a/app/public/fonts/fontawesome-webfont.ttf and /dev/null differ diff --git a/app/public/fonts/fontawesome-webfont.woff b/app/public/fonts/fontawesome-webfont.woff deleted file mode 100755 index a33af950a..000000000 Binary files a/app/public/fonts/fontawesome-webfont.woff and /dev/null differ diff --git a/app/public/img/glyphicons-halflings-white.png b/app/public/img/glyphicons-halflings-white.png deleted file mode 100644 index ba731534e..000000000 Binary files a/app/public/img/glyphicons-halflings-white.png and /dev/null differ diff --git a/app/public/js/backbone-min.js b/app/public/js/backbone-min.js deleted file mode 100644 index 3541019c5..000000000 --- a/app/public/js/backbone-min.js +++ /dev/null @@ -1,4 +0,0 @@ -(function(){var t=this;var e=t.Backbone;var i=[];var r=i.push;var s=i.slice;var n=i.splice;var a;if(typeof exports!=="undefined"){a=exports}else{a=t.Backbone={}}a.VERSION="1.0.0";var h=t._;if(!h&&typeof require!=="undefined")h=require("underscore");a.$=t.jQuery||t.Zepto||t.ender||t.$;a.noConflict=function(){t.Backbone=e;return this};a.emulateHTTP=false;a.emulateJSON=false;var o=a.Events={on:function(t,e,i){if(!l(this,"on",t,[e,i])||!e)return this;this._events||(this._events={});var r=this._events[t]||(this._events[t]=[]);r.push({callback:e,context:i,ctx:i||this});return this},once:function(t,e,i){if(!l(this,"once",t,[e,i])||!e)return this;var r=this;var s=h.once(function(){r.off(t,s);e.apply(this,arguments)});s._callback=e;return this.on(t,s,i)},off:function(t,e,i){var r,s,n,a,o,u,c,f;if(!this._events||!l(this,"off",t,[e,i]))return this;if(!t&&!e&&!i){this._events={};return this}a=t?[t]:h.keys(this._events);for(o=0,u=a.length;o").attr(t);this.setElement(e,false)}else{this.setElement(h.result(this,"el"),false)}}});a.sync=function(t,e,i){var r=k[t];h.defaults(i||(i={}),{emulateHTTP:a.emulateHTTP,emulateJSON:a.emulateJSON});var s={type:r,dataType:"json"};if(!i.url){s.url=h.result(e,"url")||U()}if(i.data==null&&e&&(t==="create"||t==="update"||t==="patch")){s.contentType="application/json";s.data=JSON.stringify(i.attrs||e.toJSON(i))}if(i.emulateJSON){s.contentType="application/x-www-form-urlencoded";s.data=s.data?{model:s.data}:{}}if(i.emulateHTTP&&(r==="PUT"||r==="DELETE"||r==="PATCH")){s.type="POST";if(i.emulateJSON)s.data._method=r;var n=i.beforeSend;i.beforeSend=function(t){t.setRequestHeader("X-HTTP-Method-Override",r);if(n)return n.apply(this,arguments)}}if(s.type!=="GET"&&!i.emulateJSON){s.processData=false}if(s.type==="PATCH"&&window.ActiveXObject&&!(window.external&&window.external.msActiveXFilteringEnabled)){s.xhr=function(){return new ActiveXObject("Microsoft.XMLHTTP")}}var o=i.xhr=a.ajax(h.extend(s,i));e.trigger("request",e,o,i);return o};var k={create:"POST",update:"PUT",patch:"PATCH","delete":"DELETE",read:"GET"};a.ajax=function(){return a.$.ajax.apply(a.$,arguments)};var S=a.Router=function(t){t||(t={});if(t.routes)this.routes=t.routes;this._bindRoutes();this.initialize.apply(this,arguments)};var $=/\((.*?)\)/g;var T=/(\(\?)?:\w+/g;var H=/\*\w+/g;var A=/[\-{}\[\]+?.,\\\^$|#\s]/g;h.extend(S.prototype,o,{initialize:function(){},route:function(t,e,i){if(!h.isRegExp(t))t=this._routeToRegExp(t);if(h.isFunction(e)){i=e;e=""}if(!i)i=this[e];var r=this;a.history.route(t,function(s){var n=r._extractParameters(t,s);i&&i.apply(r,n);r.trigger.apply(r,["route:"+e].concat(n));r.trigger("route",e,n);a.history.trigger("route",r,e,n)});return this},navigate:function(t,e){a.history.navigate(t,e);return this},_bindRoutes:function(){if(!this.routes)return;this.routes=h.result(this,"routes");var t,e=h.keys(this.routes);while((t=e.pop())!=null){this.route(t,this.routes[t])}},_routeToRegExp:function(t){t=t.replace(A,"\\$&").replace($,"(?:$1)?").replace(T,function(t,e){return e?t:"([^/]+)"}).replace(H,"(.*?)");return new RegExp("^"+t+"$")},_extractParameters:function(t,e){var i=t.exec(e).slice(1);return h.map(i,function(t){return t?decodeURIComponent(t):null})}});var I=a.History=function(){this.handlers=[];h.bindAll(this,"checkUrl");if(typeof window!=="undefined"){this.location=window.location;this.history=window.history}};var N=/^[#\/]|\s+$/g;var P=/^\/+|\/+$/g;var O=/msie [\w.]+/;var C=/\/$/;I.started=false;h.extend(I.prototype,o,{interval:50,getHash:function(t){var e=(t||this).location.href.match(/#(.*)$/);return e?e[1]:""},getFragment:function(t,e){if(t==null){if(this._hasPushState||!this._wantsHashChange||e){t=this.location.pathname;var i=this.root.replace(C,"");if(!t.indexOf(i))t=t.substr(i.length)}else{t=this.getHash()}}return t.replace(N,"")},start:function(t){if(I.started)throw new Error("Backbone.history has already been started");I.started=true;this.options=h.extend({},{root:"/"},this.options,t);this.root=this.options.root;this._wantsHashChange=this.options.hashChange!==false;this._wantsPushState=!!this.options.pushState;this._hasPushState=!!(this.options.pushState&&this.history&&this.history.pushState);var e=this.getFragment();var i=document.documentMode;var r=O.exec(navigator.userAgent.toLowerCase())&&(!i||i<=7);this.root=("/"+this.root+"/").replace(P,"/");if(r&&this._wantsHashChange){this.iframe=a.$('