├── .dockerignore ├── .env.sample ├── .eslintrc.json ├── .github ├── FUNDING.yml └── workflows │ ├── pr.yml │ └── prod.yml ├── .gitignore ├── .gitmodules ├── .nvmrc ├── .prettierrc ├── .rubocop.yml ├── .vscode └── settings.json ├── Capfile ├── Dockerfile ├── Gemfile ├── Gemfile.lock ├── README.md ├── Rakefile ├── app ├── assets │ ├── config │ │ └── manifest.js │ ├── images │ │ └── .keep │ ├── javascripts │ │ └── reactions.js │ └── stylesheets │ │ ├── application.scss │ │ ├── carded-blog.scss │ │ ├── condensed-cover.scss │ │ ├── reactions.scss │ │ └── rouge.css.erb ├── channels │ └── application_cable │ │ ├── channel.rb │ │ └── connection.rb ├── constraints │ └── custom_domain_constraint.rb ├── controllers │ ├── application_controller.rb │ ├── authors_controller.rb │ ├── concerns │ │ └── .keep │ ├── credentials_controller.rb │ ├── guestbook_entries_controller.rb │ ├── health_check_controller.rb │ ├── help_controller.rb │ ├── posts_controller.rb │ ├── reactions_controller.rb │ ├── robots_controller.rb │ ├── sitemap_controller.rb │ ├── subscriptions_controller.rb │ └── usage_controller.rb ├── helpers │ ├── application_helper.rb │ ├── authors_helper.rb │ ├── captcha_helper.rb │ ├── encryption_helper.rb │ ├── guestbook_entry_helper.rb │ ├── guestbook_helper.rb │ ├── posts_helper.rb │ ├── reactions_helper.rb │ ├── subscriptions_helper.rb │ └── usage_helper.rb ├── jobs │ ├── application_job.rb │ └── ssl_certificate_create_job.rb ├── mailers │ ├── admin_mailer.rb │ ├── application_mailer.rb │ ├── authors_mailer.rb │ └── subscription_mailer.rb ├── models │ ├── application_record.rb │ ├── author.rb │ ├── concerns │ │ ├── .keep │ │ └── tokenable.rb │ ├── credential.rb │ ├── domain.rb │ ├── guestbook_entry.rb │ ├── post.rb │ ├── reaction.rb │ ├── ssl_certificate.rb │ ├── subscriber.rb │ └── subscription.rb ├── services │ └── sns_publisher.rb ├── views │ ├── admin_mailer │ │ └── new_domain_request.html.erb │ ├── authors │ │ ├── _author_header.html.erb │ │ ├── all.html.erb │ │ ├── feed.rss.builder │ │ ├── settings.html.erb │ │ ├── show.html.erb │ │ ├── subscribe.html.erb │ │ ├── tip.html.erb │ │ └── verify_email.html.haml │ ├── authors_mailer │ │ ├── domain_approved.html.erb │ │ ├── domain_invalid.html.erb │ │ ├── featured.html.erb │ │ ├── new_reaction.html.erb │ │ ├── unread_guestbook_entries.html.erb │ │ └── verify_email.html.erb │ ├── guestbook_entries │ │ └── index.html.erb │ ├── help │ │ └── index.html.erb │ ├── layouts │ │ ├── application.html.erb │ │ ├── mailer.html.erb │ │ ├── mailer.html.haml │ │ ├── mailer.text.erb │ │ └── mailer.text.haml │ ├── posts │ │ ├── index.html.erb │ │ └── show.html.erb │ ├── reactions │ │ ├── create_via_email.html.erb │ │ └── new.html.erb │ ├── robots │ │ └── index.text.erb │ ├── shared │ │ ├── _footer.html.erb │ │ └── _header.html.haml │ ├── sitemap │ │ ├── authors.xml.haml │ │ ├── custom_domain.xml.haml │ │ ├── index.xml.haml │ │ └── posts.xml.haml │ ├── subscription_mailer │ │ ├── new_post.html.erb │ │ ├── new_subscription.html.erb │ │ ├── privacy_policy_update.html.erb │ │ ├── subscription_success.html.erb │ │ └── weekly_digest.html.erb │ ├── subscriptions │ │ ├── confirm.html.erb │ │ ├── unsubscribe.html.erb │ │ ├── update_frequency.html.erb │ │ └── validate.html.erb │ └── usage │ │ ├── index.html.erb │ │ └── new_author.html.erb └── workers │ └── account_creation_worker.rb ├── babel.config.js ├── bin ├── bundle ├── rails ├── rake ├── setup ├── spring ├── update ├── webpack └── webpack-dev-server ├── certificates └── .gtikeep ├── client └── app │ ├── assets │ ├── fonts │ │ ├── Merriweather-Bold.ttf │ │ ├── Roboto-Bold.ttf │ │ └── Roboto-Regular.ttf │ ├── gifs │ │ ├── index.js │ │ ├── listed-getting-started.gif │ │ ├── listed-publish.gif │ │ └── listed-settings.gif │ ├── icons │ │ ├── ic-arrow-long.svg │ │ ├── ic-arrow-up.svg │ │ ├── ic-book.svg │ │ ├── ic-checkbox-checked.svg │ │ ├── ic-checkbox-empty.svg │ │ ├── ic-chevron-down.svg │ │ ├── ic-chevron-up.svg │ │ ├── ic-close.svg │ │ ├── ic-code.svg │ │ ├── ic-credit-card.svg │ │ ├── ic-earth.svg │ │ ├── ic-edit.svg │ │ ├── ic-email.svg │ │ ├── ic-eye-off-filled.svg │ │ ├── ic-eye-off.svg │ │ ├── ic-lifebuoy.svg │ │ ├── ic-link.svg │ │ ├── ic-listed.svg │ │ ├── ic-menu.svg │ │ ├── ic-more-horizontal.svg │ │ ├── ic-open-in.svg │ │ ├── ic-palette.svg │ │ ├── ic-quote.svg │ │ ├── ic-radio-button-empty.svg │ │ ├── ic-radio-button-selected.svg │ │ ├── ic-settings-filled.svg │ │ ├── ic-star-circle-filled.svg │ │ ├── ic-text-rich.svg │ │ ├── ic-trash.svg │ │ ├── ic-wallet-filled.svg │ │ └── index.js │ ├── images │ │ ├── carded-blog.svg │ │ ├── condensed-cover.svg │ │ ├── full-cover.svg │ │ ├── index.js │ │ └── vertical-blog.svg │ └── styles │ │ ├── application.scss │ │ ├── mixins.scss │ │ └── vars.scss │ ├── components │ ├── admin_mailer │ │ └── NewDomainRequest.jsx │ ├── authors │ │ ├── All.jsx │ │ ├── All.scss │ │ ├── HeaderContainer.jsx │ │ ├── HeaderContainer.scss │ │ ├── SettingsPage.jsx │ │ ├── SettingsPage.scss │ │ ├── Show.jsx │ │ ├── Show.scss │ │ ├── Subscribe.jsx │ │ ├── Subscribe.scss │ │ ├── SubscriptionForm.jsx │ │ ├── SubscriptionForm.scss │ │ ├── Tip.jsx │ │ ├── Tip.scss │ │ ├── header │ │ │ ├── AuthorInfo.jsx │ │ │ ├── AuthorInfo.scss │ │ │ ├── MenuContainer.jsx │ │ │ ├── MenuContainer.scss │ │ │ ├── index.js │ │ │ └── menu │ │ │ │ ├── AuthorMenu.jsx │ │ │ │ ├── AuthorMenu.scss │ │ │ │ ├── HomepageMenu.jsx │ │ │ │ └── index.js │ │ └── settings │ │ │ ├── Appearance.jsx │ │ │ ├── Appearance.scss │ │ │ ├── ConfirmationModal.jsx │ │ │ ├── ConfirmationModal.scss │ │ │ ├── CredentialForm.jsx │ │ │ ├── CredentialForm.scss │ │ │ ├── CustomDomain.jsx │ │ │ ├── CustomDomain.scss │ │ │ ├── DeleteBlog.jsx │ │ │ ├── DeleteBlog.scss │ │ │ ├── General.jsx │ │ │ ├── GuestbookEntries.jsx │ │ │ ├── GuestbookEntries.scss │ │ │ ├── MyPosts.jsx │ │ │ ├── MyPosts.scss │ │ │ ├── PaymentDetails.jsx │ │ │ ├── PaymentDetails.scss │ │ │ └── index.js │ ├── authors_mailer │ │ ├── DomainApproved.jsx │ │ ├── DomainInvalid.jsx │ │ ├── Featured.jsx │ │ ├── NewReaction.jsx │ │ ├── UnreadGuestbookEntries.jsx │ │ └── VerifyEmail.jsx │ ├── guestbook_entries │ │ ├── Guestbook.jsx │ │ ├── Guestbook.scss │ │ ├── New.jsx │ │ └── New.scss │ ├── help │ │ └── Help.jsx │ ├── posts │ │ ├── Post.jsx │ │ ├── Post.scss │ │ ├── Posts.jsx │ │ ├── Show.jsx │ │ └── Show.scss │ ├── reactions │ │ ├── New.jsx │ │ ├── New.scss │ │ └── ReactionSuccess.jsx │ ├── shared │ │ ├── Checkbox.jsx │ │ ├── Checkbox.scss │ │ ├── Dropdown.jsx │ │ ├── Dropdown.scss │ │ ├── ErrorToast.jsx │ │ ├── ErrorToast.scss │ │ ├── Footer.jsx │ │ ├── Footer.scss │ │ ├── LeftNavBarPage.jsx │ │ ├── LeftNavBarPage.scss │ │ ├── RadioButton.jsx │ │ ├── RadioButton.scss │ │ ├── RadioButtonGroup.jsx │ │ ├── RadioButtonGroup.scss │ │ ├── Resources.jsx │ │ ├── Resources.scss │ │ ├── ScrollToTopButton.jsx │ │ ├── ScrollToTopButton.scss │ │ ├── StartWriting.jsx │ │ └── left_nav_bar_page │ │ │ ├── NavBar.jsx │ │ │ ├── NavBar.scss │ │ │ ├── Section.jsx │ │ │ ├── Section.scss │ │ │ └── index.js │ ├── subscription_mailer │ │ ├── NewPost.jsx │ │ ├── NewSubscription.jsx │ │ ├── PrivacyUpdate.jsx │ │ ├── SubscriptionSuccess.jsx │ │ └── WeeklyDigest.jsx │ ├── subscriptions │ │ ├── Confirm.jsx │ │ ├── Unsubscribe.jsx │ │ ├── UpdateFrequency.jsx │ │ ├── Validate.jsx │ │ └── Validate.scss │ └── usage │ │ ├── NewAuthor.jsx │ │ ├── NewAuthor.scss │ │ ├── Usage.jsx │ │ ├── Usage.scss │ │ ├── authors_list │ │ ├── AuthorListItem.jsx │ │ ├── AuthorListItem.scss │ │ ├── AuthorsList.jsx │ │ ├── AuthorsList.scss │ │ ├── MasonryLayout.jsx │ │ └── MasonryLayout.scss │ │ └── new_author │ │ ├── GettingStarted.jsx │ │ ├── GettingStarted.scss │ │ ├── GifSection.jsx │ │ ├── GifSection.scss │ │ └── index.js │ ├── constants │ ├── author-appearance.js │ └── index.js │ ├── helpers │ ├── author.js │ └── index.js │ ├── packs │ └── clientRegistration.js │ ├── startup │ └── serverRegistration.js │ ├── types │ └── author.js │ └── utils │ └── getAuthToken.js ├── config.ru ├── config ├── application.rb ├── boot.rb ├── brakeman.ignore ├── cable.yml ├── database.yml ├── deploy.rb ├── deploy │ ├── production.rb │ ├── staging.rb │ └── test.rb ├── environment.rb ├── environments │ ├── development.rb │ ├── production.rb │ └── test.rb ├── initializers │ ├── application_controller_renderer.rb │ ├── assets.rb │ ├── backtrace_silencers.rb │ ├── cookies_serializer.rb │ ├── css_sanitizer.rb │ ├── filter_parameter_logging.rb │ ├── hcaptcha.rb │ ├── inflections.rb │ ├── letsencrypt.rb │ ├── lograge.rb │ ├── mime_types.rb │ ├── new_framework_defaults.rb │ ├── react_on_rails.rb │ ├── session_store.rb │ └── wrap_parameters.rb ├── locales │ └── en.yml ├── puma.rb ├── routes.rb ├── schedule.rb ├── secrets.yml ├── shoryuken.yml ├── spring.rb ├── webpack │ ├── client.js │ ├── development.js │ ├── environment.js │ ├── production.js │ └── server.js └── webpacker.yml ├── db ├── migrate │ ├── 20170503193438_create_posts.rb │ ├── 20170503193546_create_authors.rb │ ├── 20171009234151_author_fields.rb │ ├── 20171016182828_create_subscribers.rb │ ├── 20171016222312_add_email_to_authors.rb │ ├── 20171030232158_frequency_params_to_subscription.rb │ ├── 20171103152017_add_timestamps_to_subscriptions.rb │ ├── 20171204200644_twitter_links.rb │ ├── 20180123044046_add_word_count.rb │ ├── 20180203213949_add_canonical_to_posts.rb │ ├── 20180203222546_add_metafields_to_posts.rb │ ├── 20180204021959_add_indexes.rb │ ├── 20180214205116_add_image_to_post.rb │ ├── 20180220041541_add_email_send_date_to_posts.rb │ ├── 20180223163928_add_last_word_count_to_authors.rb │ ├── 20180223171252_add_hidden_to_posts.rb │ ├── 20180318162046_add_paid_fields_to_posts.rb │ ├── 20180318181956_create_purchases.rb │ ├── 20180326190731_create_tips.rb │ ├── 20180415155041_add_featured_to_authors.rb │ ├── 20180604203211_add_published_to_posts.rb │ ├── 20180610182702_add_image_url_to_authors.rb │ ├── 20180613194549_add_domain_to_authors.rb │ ├── 20180614140542_create_domains.rb │ ├── 20180615134514_add_hide_from_home_to_author.rb │ ├── 20180820200721_create_credentials.rb │ ├── 20180821144535_create_guestbooks.rb │ ├── 20180821163359_add_guestbook_options_to_authors.rb │ ├── 20181005190145_add_email_verification_to_authors.rb │ ├── 20181201104051_create_simple_captcha_data.rb │ ├── 20181201171259_add_verification_sent_to_subscriptions.rb │ ├── 20190220004015_disable_newsletter.rb │ ├── 20200201203629_add_pages.rb │ ├── 20200202152431_add_read_status_to_guestbook.rb │ ├── 20200806085900_create_letsencrypt_certificates.rb │ ├── 20200826113044_remove_aws_colums_from_certificates.rb │ ├── 20210326165901_add_appearance_settings_to_authors.rb │ ├── 20210326210639_add_on_homepage_to_authors.rb │ ├── 20210326223431_add_author_show_to_posts.rb │ ├── 20210405134917_perform_launch_prod_migration.rb │ ├── 20210818164947_path_and_desc_frontmatter.rb │ ├── 20210819133742_posts_link.rb │ ├── 20220304012050_add_custom_author_to_posts.rb │ └── 20230224140201_create_reactions.rb ├── schema.rb └── seed.rb ├── docker-compose.yml ├── docker └── entrypoint.sh ├── lib ├── assets │ └── .keep └── tasks │ ├── .keep │ ├── migrate_ignore_concurrent.rake │ ├── privacy_notification.rake │ ├── renew_ssl_certificates.rake │ └── seed_authors.rake ├── log └── .keep ├── package.json ├── public ├── 404.html ├── 422.html ├── 500.html ├── apple-touch-icon-precomposed.png ├── apple-touch-icon.png └── favicon.ico ├── test ├── controllers │ ├── .keep │ ├── authors_controller_test.rb │ ├── credentials_controller_test.rb │ ├── extension_controller_test.rb │ ├── guestbook_controller_test.rb │ ├── guestbook_entry_controller_test.rb │ ├── posts_controller_test.rb │ ├── reactions_controller_test.rb │ ├── subscriptions_controller_test.rb │ └── usage_controller_test.rb ├── fixtures │ ├── .keep │ ├── authors.yml │ ├── domains.yml │ ├── files │ │ └── .keep │ ├── guestbook_entries.yml │ ├── guestbooks.yml │ ├── posts.yml │ ├── purchases.yml │ ├── reactions.yml │ ├── subscribers.yml │ ├── subscriptions.yml │ └── tips.yml ├── helpers │ └── .keep ├── integration │ └── .keep ├── mailers │ ├── .keep │ ├── authors_mailer_test.rb │ ├── previews │ │ ├── admin_mailer_preview.rb │ │ ├── authors_mailer_preview.rb │ │ └── subscription_mailer_preview.rb │ └── subscription_mailer_test.rb ├── models │ ├── .keep │ ├── author_test.rb │ ├── credential_test.rb │ ├── domain_test.rb │ ├── guestbook_entry_test.rb │ ├── guestbook_test.rb │ ├── post_test.rb │ ├── purchase_test.rb │ ├── reaction_test.rb │ ├── subscriber_test.rb │ ├── subscription_test.rb │ └── tip_test.rb └── test_helper.rb ├── wait-for.sh └── yarn.lock /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | /tmp/ 3 | .vscode 4 | /public/assets/ 5 | /public/packs/ 6 | -------------------------------------------------------------------------------- /.env.sample: -------------------------------------------------------------------------------- 1 | RAILS_ENV=development 2 | NODE_ENV=development 3 | WEB_CONCURRENCY=0 4 | RAILS_SERVE_STATIC_FILES=true 5 | RAILS_LOG_TO_STDOUT=true 6 | 7 | SECRET_KEY_BASE= 8 | 9 | DB_HOST=127.0.0.1 10 | DB_PORT=3306 11 | DB_DATABASE=sn_listed 12 | DB_USERNAME=std_listed_user 13 | DB_PASSWORD=changeme123 14 | DB_ROOT_PASSWORD=changeme123 15 | 16 | PORT=3000 17 | 18 | HOST=http://localhost:3000 19 | 20 | # Custom Domains 21 | LETSENCRYPT_PRIVATE_KEY= 22 | CUSTOM_DOMAIN_IP= 23 | 24 | # SSL Certificates Renewal Script 25 | NGINX_CONFIG_PATH= 26 | DOMAINS_FOLDER_PATH= 27 | CERTIFICATES_FOLDER_PATH= 28 | 29 | # hCaptcha 30 | HCAPTCHA_SECRET_KEY=0x0000000000000000000000000000000000000000 31 | HCAPTCHA_SITE_KEY=10000000-ffff-ffff-ffff-000000000001 32 | 33 | # Domain Events 34 | #SNS_TOPIC_ARN= 35 | #SQS_QUEUE_URL= 36 | AWS_REGION="us-east-1" 37 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es6": true 5 | }, 6 | "extends": "airbnb", 7 | "globals": { 8 | "Atomics": "readonly", 9 | "SharedArrayBuffer": "readonly", 10 | "Turbolinks": "readonly" 11 | }, 12 | "parserOptions": { 13 | "ecmaFeatures": { 14 | "jsx": true 15 | }, 16 | "ecmaVersion": 2018, 17 | "sourceType": "module" 18 | }, 19 | "plugins": ["react", "prettier"], 20 | "rules": { 21 | "indent": ["error", 4], 22 | "jsx-a11y/label-has-associated-control": [ 23 | "error", 24 | { 25 | "labelAttributes": ["htmlFor"] 26 | } 27 | ], 28 | "react/no-unescaped-entities": "off", 29 | "react/jsx-one-expression-per-line": "off", 30 | "max-len": ["warn", { "code": 120 }], 31 | "object-curly-newline": "off", 32 | "quotes": ["error", "double"], 33 | "react/jsx-indent": [2, 4], 34 | "react/jsx-indent-props": [2, 4], 35 | "react/jsx-props-no-spreading": [0] 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [standardnotes] 4 | -------------------------------------------------------------------------------- /.github/workflows/pr.yml: -------------------------------------------------------------------------------- 1 | name: PR 2 | 3 | on: 4 | pull_request: 5 | branches: [ develop ] 6 | 7 | jobs: 8 | test: 9 | 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - uses: actions/checkout@v2 14 | - name: Runs Brakeman vulnerability scanner 15 | uses: standardnotes/brakeman-action@v1.0.0 16 | with: 17 | options: "--color" 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files for more about ignoring files. 2 | # 3 | # If you find yourself ignoring temporary files generated by your text editor 4 | # or operating system, you probably want to add a global ignore instead: 5 | # git config --global core.excludesfile '~/.gitignore_global' 6 | 7 | # OS & IDE 8 | .DS_Store 9 | 10 | # Ignore bundler config. 11 | /.bundle 12 | 13 | # Ignore all logfiles and tempfiles. 14 | /log/* 15 | !/log/.keep 16 | /tmp 17 | 18 | /config/cap.yml 19 | /config/domains.yml 20 | 21 | /app/assets/templates/generated/ 22 | 23 | # Ignore bower components 24 | /node_modules 25 | /.sass-cache 26 | 27 | # Ignore ENV variables config 28 | .env 29 | .env.* 30 | !.env.sample 31 | .ssh 32 | 33 | dump.rdb 34 | 35 | # Ignore compiled assets 36 | /public/assets 37 | 38 | # Ignore user uploads 39 | /public/uploads/* 40 | !/public/uploads/.keep 41 | 42 | # Container MySQL data 43 | /data/ 44 | 45 | /public/packs 46 | /node_modules 47 | 48 | # letsencrypt 49 | config/letsencrypt.key 50 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/standardnotes/listed/5750e416b82060a6058631a2d7fdfc301c3107f6/.gitmodules -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 14.18.2 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": false, 3 | "trailingComma": "all", 4 | "printWidth": 120, 5 | "semi": true, 6 | "tabWidth": 4 7 | } 8 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | AllCops: 2 | NewCops: enable 3 | Exclude: 4 | - 'db/**/*' 5 | - 'config/**/*' 6 | - 'script/**/*' 7 | - 'bin/{rails,rake}' 8 | Metrics/MethodLength: 9 | Enabled: false 10 | Metrics/AbcSize: 11 | Enabled: false 12 | Layout/LineLength: 13 | Max: 120 14 | Style/ConditionalAssignment: 15 | Enabled: false 16 | Style/Documentation: 17 | Enabled: false 18 | Style/FrozenStringLiteralComment: 19 | Enabled: false 20 | Metrics/PerceivedComplexity: 21 | Enabled: false 22 | Metrics/CyclomaticComplexity: 23 | Enabled: false -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | 3 | } -------------------------------------------------------------------------------- /Capfile: -------------------------------------------------------------------------------- 1 | # Load DSL and set up stages 2 | require "capistrano/setup" 3 | 4 | # Include default deployment tasks 5 | require "capistrano/deploy" 6 | 7 | # Include tasks from other gems included in your Gemfile 8 | # 9 | # For documentation on these, see for example: 10 | # 11 | # https://github.com/capistrano/rvm 12 | # https://github.com/capistrano/rbenv 13 | # https://github.com/capistrano/chruby 14 | # https://github.com/capistrano/bundler 15 | # https://github.com/capistrano/rails 16 | # https://github.com/capistrano/passenger 17 | # 18 | require 'capistrano/rvm' 19 | # require 'capistrano/rbenv' 20 | # require 'capistrano/chruby' 21 | require 'capistrano/bundler' 22 | require 'capistrano/rails/assets' 23 | require 'capistrano/rails/migrations' 24 | require 'capistrano/passenger' 25 | require 'capistrano/shoryuken' 26 | require 'capistrano/git-submodule-strategy' 27 | require "whenever/capistrano" # Update crontab on deploy 28 | 29 | 30 | # Load custom tasks from `lib/capistrano/tasks` if you have any defined 31 | Dir.glob("lib/capistrano/tasks/*.rake").each { |r| import r } 32 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ruby:2.6.5-slim-stretch 2 | 3 | ARG UID=1000 4 | ARG GID=1000 5 | 6 | RUN addgroup --system listed --gid $GID && adduser --disabled-password --system listed --gid $GID --uid $UID 7 | 8 | RUN rm /bin/sh && ln -s /bin/bash /bin/sh 9 | 10 | RUN apt-get update \ 11 | && apt-get install -y git build-essential libmariadb-dev curl imagemagick python \ 12 | && apt-get -y autoclean 13 | 14 | RUN mkdir -p /usr/local/nvm 15 | ENV NVM_DIR /usr/local/nvm 16 | ENV NODE_VERSION 14.18.2 17 | ENV NVM_INSTALL_PATH $NVM_DIR/versions/node/v$NODE_VERSION 18 | ENV WEBPACKER_NODE_MODULES_BIN_PATH=value 19 | 20 | RUN curl --silent -o- https://raw.githubusercontent.com/creationix/nvm/master/install.sh | bash 21 | 22 | RUN source $NVM_DIR/nvm.sh \ 23 | && nvm install $NODE_VERSION \ 24 | && nvm alias default $NODE_VERSION \ 25 | && nvm use default 26 | 27 | ENV NODE_PATH $NVM_INSTALL_PATH/lib/node_modules 28 | ENV PATH $NVM_INSTALL_PATH/bin:$PATH 29 | 30 | RUN npm install -g yarn 31 | 32 | WORKDIR /listed 33 | 34 | RUN chown -R $UID:$GID . 35 | 36 | USER listed 37 | 38 | COPY --chown=$UID:$GID package.json yarn.lock Gemfile Gemfile.lock /listed/ 39 | 40 | RUN yarn install --pure-lockfile 41 | 42 | RUN gem install bundler && bundle install 43 | 44 | COPY --chown=$UID:$GID . /listed 45 | 46 | RUN bundle exec rake assets:precompile 47 | 48 | EXPOSE 3000 49 | 50 | ENTRYPOINT [ "docker/entrypoint.sh" ] 51 | 52 | CMD [ "start" ] 53 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # Add your own tasks in files placed in lib/tasks ending in .rake, 2 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. 3 | 4 | require_relative 'config/application' 5 | 6 | Rails.application.load_tasks 7 | -------------------------------------------------------------------------------- /app/assets/config/manifest.js: -------------------------------------------------------------------------------- 1 | //= link_tree ../images 2 | //= link_directory ../javascripts .js 3 | //= link_directory ../stylesheets .css 4 | -------------------------------------------------------------------------------- /app/assets/images/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/standardnotes/listed/5750e416b82060a6058631a2d7fdfc301c3107f6/app/assets/images/.keep -------------------------------------------------------------------------------- /app/assets/javascripts/reactions.js: -------------------------------------------------------------------------------- 1 | // Place all the behaviors and hooks related to the matching controller here. 2 | // All this logic will automatically be available in application.js. 3 | -------------------------------------------------------------------------------- /app/assets/stylesheets/application.scss: -------------------------------------------------------------------------------- 1 | /* 2 | * This is a manifest file that'll be compiled into application.css, which will include all the files 3 | * listed below. 4 | * 5 | * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets, 6 | * or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path. 7 | * 8 | * You're free to add application-wide styles to this file and they'll appear at the bottom of the 9 | * compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS 10 | * files in this directory. Styles in this file should be added after the last require_* statement. 11 | * It is generally better to create a new file per style scope. 12 | * 13 | *= require_self 14 | */ 15 | 16 | @import "stylekit"; 17 | @import "rouge"; 18 | 19 | #main-container { 20 | display: flex; 21 | min-height: 100%; 22 | flex-direction: column; 23 | } 24 | 25 | #content-container { 26 | height: 100%; 27 | flex: 1; 28 | } 29 | -------------------------------------------------------------------------------- /app/assets/stylesheets/carded-blog.scss: -------------------------------------------------------------------------------- 1 | #author-posts { 2 | padding-top: 48px; 3 | } 4 | 5 | #author-posts .navigation { 6 | width: 100%; 7 | } 8 | 9 | #author-posts .author-post { 10 | padding: 16px; 11 | margin-bottom: 16px; 12 | border: var(--card-post-border); 13 | border-radius: 4px; 14 | background-color: var(--card-post-background-color); 15 | } 16 | 17 | #author-posts .post-body { 18 | display: none; 19 | } 20 | 21 | #author-posts .author-post .post-content { 22 | display: flex; 23 | flex-direction: column; 24 | height: 100%; 25 | justify-content: space-between; 26 | } 27 | 28 | #author-posts .post-title { 29 | font-size: 28px; 30 | line-height: 42px; 31 | word-wrap: break-word; 32 | } 33 | 34 | #author-posts .post-header { 35 | height: 100%; 36 | display: flex; 37 | flex-direction: column; 38 | justify-content: space-between; 39 | } 40 | 41 | #author-posts h2 { 42 | display: -webkit-box; 43 | -webkit-box-orient: vertical; 44 | -webkit-line-clamp: 3; 45 | overflow: hidden; 46 | } 47 | 48 | @media (min-width: 992px) { 49 | #author-posts { 50 | padding-top: 56px; 51 | display: flex; 52 | flex-direction: row; 53 | flex-wrap: wrap; 54 | margin-right: -16px; 55 | } 56 | 57 | #author-posts .author-post { 58 | width: calc(25% - 16px); 59 | margin-right: 16px; 60 | } 61 | } -------------------------------------------------------------------------------- /app/assets/stylesheets/condensed-cover.scss: -------------------------------------------------------------------------------- 1 | .page-header__container .header-author-info { 2 | padding-top: 0; 3 | padding-bottom: 0; 4 | } 5 | 6 | .page-header__container .header-author-info__items { 7 | min-height: 0; 8 | flex-direction: column; 9 | margin-bottom: 0; 10 | @media (min-width: 992px) { 11 | flex-direction: column-reverse; 12 | align-items: flex-start; 13 | } 14 | } 15 | 16 | .header-author-info__items .h1 { 17 | display: none; 18 | } 19 | 20 | .page-header__container .header-author-links { 21 | margin-bottom: 16px; 22 | } 23 | 24 | .page-header__container .header-image-container { 25 | width: 100%; 26 | margin-right: 0; 27 | margin-top: 24px; 28 | height: 200px; 29 | @media (min-width: 992px) { 30 | margin: 0; 31 | } 32 | } 33 | 34 | .page-header__container .header-author-info .word-count__button { 35 | margin-top: 24px; 36 | padding-top: 16px; 37 | padding-bottom: 8px; 38 | } 39 | -------------------------------------------------------------------------------- /app/assets/stylesheets/reactions.scss: -------------------------------------------------------------------------------- 1 | // Place all the styles related to the reactions controller here. 2 | // They will automatically be included in application.css. 3 | // You can use Sass (SCSS) here: http://sass-lang.com/ 4 | -------------------------------------------------------------------------------- /app/assets/stylesheets/rouge.css.erb: -------------------------------------------------------------------------------- 1 | <% require 'rouge' %> 2 | <%= Rouge::Themes::Github.new.render %> 3 | -------------------------------------------------------------------------------- /app/channels/application_cable/channel.rb: -------------------------------------------------------------------------------- 1 | module ApplicationCable 2 | class Channel < ActionCable::Channel::Base 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /app/channels/application_cable/connection.rb: -------------------------------------------------------------------------------- 1 | module ApplicationCable 2 | class Connection < ActionCable::Connection::Base 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /app/constraints/custom_domain_constraint.rb: -------------------------------------------------------------------------------- 1 | class CustomDomainConstraint 2 | def self.matches? request 3 | matching_blog?(request) 4 | end 5 | 6 | def self.matching_blog? request 7 | Domain.where(:domain => request.host, active: true).any? 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /app/controllers/concerns/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/standardnotes/listed/5750e416b82060a6058631a2d7fdfc301c3107f6/app/controllers/concerns/.keep -------------------------------------------------------------------------------- /app/controllers/credentials_controller.rb: -------------------------------------------------------------------------------- 1 | class CredentialsController < ApplicationController 2 | 3 | def new 4 | @credential = Credential.new 5 | end 6 | 7 | def create 8 | @credential = @author.credentials.new(credential_params) 9 | @credential.save 10 | redirect_to_authenticated_settings(@author) 11 | end 12 | 13 | def edit 14 | @credential = Credential.find(params[:id]) 15 | end 16 | 17 | def update 18 | @credential = Credential.find(params[:id]) 19 | @credential.update(credential_params) 20 | redirect_to_authenticated_settings(@author) 21 | end 22 | 23 | def destroy 24 | @credential = Credential.find(params[:id]) 25 | @credential.destroy 26 | redirect_to_authenticated_settings(@author) 27 | end 28 | 29 | def credential_params 30 | params.require(:credential).permit(:key, :value) 31 | end 32 | 33 | end 34 | -------------------------------------------------------------------------------- /app/controllers/health_check_controller.rb: -------------------------------------------------------------------------------- 1 | class HealthCheckController < ApplicationController 2 | def index 3 | render :plain => "OK" 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /app/controllers/help_controller.rb: -------------------------------------------------------------------------------- 1 | class HelpController < ApplicationController 2 | 3 | def index 4 | 5 | end 6 | 7 | end 8 | -------------------------------------------------------------------------------- /app/controllers/reactions_controller.rb: -------------------------------------------------------------------------------- 1 | class ReactionsController < ApplicationController 2 | include CaptchaHelper 3 | 4 | before_action :find_post 5 | 6 | def create_via_email 7 | creation_token = params[:creation_token] 8 | subscriber = Subscriber.find_by(id: params[:subscriber_id]) 9 | 10 | return not_found if !subscriber || subscriber.reaction_creation_token(@post) != creation_token 11 | 12 | reaction = Reaction.find_or_initialize_by(post: @post, subscriber: subscriber) 13 | 14 | return if reaction.persisted? 15 | 16 | reaction.reaction_string = params[:reaction] 17 | reaction.save! 18 | 19 | AuthorsMailer.new_reaction(reaction.id).deliver_now 20 | end 21 | 22 | def new 23 | @reaction = Reaction.new(post: @post) 24 | @reaction_string = params[:reaction] 25 | end 26 | 27 | def create 28 | captcha_verification = JSON.parse(CaptchaHelper.verify_hcaptcha(params[:token])) 29 | is_valid_captcha = captcha_verification['success'] 30 | 31 | if is_valid_captcha 32 | reaction = Reaction.new(post: @post) 33 | reaction.reaction_string = params[:reaction_string] 34 | reaction.save! 35 | 36 | AuthorsMailer.new_reaction(reaction.id).deliver_now 37 | else 38 | render json: { error: captcha_verification['error'] } 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /app/controllers/robots_controller.rb: -------------------------------------------------------------------------------- 1 | class RobotsController < ApplicationController 2 | 3 | before_action do 4 | domain = Domain.find_by(domain: request.host) 5 | if domain 6 | if !domain.active 7 | render file: "#{Rails.root}/public/404.html", status: 404 8 | else 9 | @domain_author = domain.author 10 | end 11 | else 12 | unless ["https://#{request.host}", "http://#{request.host}:#{request.port}"].include?(ENV['HOST']) 13 | render file: "#{Rails.root}/public/404.html", status: 404 14 | end 15 | end 16 | end 17 | 18 | def index 19 | if @domain_author 20 | @sitemap = "#{@domain_author.url}/author-sitemap.xml" 21 | else 22 | @sitemap = "#{ENV['HOST']}/sitemap.xml" 23 | end 24 | 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /app/controllers/usage_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class UsageController < ApplicationController 4 | def index 5 | active_authors_query = Author 6 | .includes(:domain) 7 | .where.not(homepage_activity: nil) 8 | .order(homepage_activity: :desc) 9 | active_authors = active_authors_query.to_a 10 | easter_egg_index = active_authors.size.positive? ? rand(1..active_authors.size) : 0 11 | easter_egg = { 12 | id: 'easter-egg', 13 | title: 'This could be you :)', 14 | bio: 'Share your experience in its truest form. Start writing now.', 15 | featured: false 16 | } 17 | active_authors.insert(easter_egg_index, easter_egg) 18 | @active_authors = active_authors 19 | @featured_authors = active_authors_query.where(featured: true) 20 | @canonical = ENV['HOST'] 21 | end 22 | 23 | def new_author 24 | @title = 'New Author | Listed' 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /app/helpers/application_helper.rb: -------------------------------------------------------------------------------- 1 | module ApplicationHelper 2 | end 3 | 4 | class String 5 | def is_integer? 6 | self.to_i.to_s == self 7 | end 8 | 9 | def contains_url? 10 | match(/\b(?:(?:mailto:\S+|(?:https?|ftp|file):\/\/)?(?:\w+\.)+[a-z]{2,6})\b/) != nil 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /app/helpers/authors_helper.rb: -------------------------------------------------------------------------------- 1 | module AuthorsHelper 2 | end 3 | -------------------------------------------------------------------------------- /app/helpers/captcha_helper.rb: -------------------------------------------------------------------------------- 1 | module CaptchaHelper 2 | require 'net/https' 3 | 4 | def self.verify_hcaptcha(token) 5 | verify_url = Rails.application.config.hcaptcha_verify_url 6 | secret_key = Rails.application.config.hcaptcha_secret_key 7 | site_key = Rails.application.config.hcaptcha_site_key 8 | 9 | uri = URI.parse(verify_url) 10 | response = Net::HTTP.post_form(uri, 'secret' => secret_key, 'response' => token, 'sitekey' => site_key) 11 | json = JSON.parse(response.body) 12 | 13 | if response.body["success"] 14 | ApplicationController.render :json => { success: true} 15 | else 16 | ApplicationController.render :json => { success: false, error: "Please try again."} 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /app/helpers/encryption_helper.rb: -------------------------------------------------------------------------------- 1 | module EncryptionHelper 2 | 3 | require 'securerandom' 4 | require "openssl" 5 | require 'base64' 6 | 7 | ALG = "AES-256-CBC" 8 | 9 | def self.generate_random_key 10 | Digest::SHA256.hexdigest(SecureRandom.hex) 11 | end 12 | 13 | def self.encrypt(message, key) 14 | aes = OpenSSL::Cipher::Cipher.new(ALG) 15 | aes.encrypt 16 | aes.key = [key].pack('H*') 17 | cipher = aes.update(message) 18 | cipher << aes.final 19 | cipher64 = [cipher].pack('m') 20 | cipher64 21 | end 22 | 23 | def self.decrypt(cipher64, key) 24 | decode_cipher = OpenSSL::Cipher::Cipher.new(ALG) 25 | decode_cipher.decrypt 26 | decode_cipher.key = [key].pack('H*') 27 | plain = decode_cipher.update(cipher64.unpack('m')[0]) 28 | plain << decode_cipher.final 29 | plain 30 | end 31 | 32 | def self.sha256(string) 33 | Digest::SHA256.hexdigest(string) 34 | end 35 | 36 | end 37 | -------------------------------------------------------------------------------- /app/helpers/guestbook_entry_helper.rb: -------------------------------------------------------------------------------- 1 | module GuestbookEntryHelper 2 | end 3 | -------------------------------------------------------------------------------- /app/helpers/guestbook_helper.rb: -------------------------------------------------------------------------------- 1 | module GuestbookHelper 2 | end 3 | -------------------------------------------------------------------------------- /app/helpers/posts_helper.rb: -------------------------------------------------------------------------------- 1 | module PostsHelper 2 | POST_TITLE_NO_SYMBOLS_PATTERN = /\A([\w]+[\s]*)+\z/.freeze 3 | end 4 | -------------------------------------------------------------------------------- /app/helpers/reactions_helper.rb: -------------------------------------------------------------------------------- 1 | module ReactionsHelper 2 | end 3 | -------------------------------------------------------------------------------- /app/helpers/subscriptions_helper.rb: -------------------------------------------------------------------------------- 1 | module SubscriptionsHelper 2 | end 3 | -------------------------------------------------------------------------------- /app/helpers/usage_helper.rb: -------------------------------------------------------------------------------- 1 | module UsageHelper 2 | end 3 | -------------------------------------------------------------------------------- /app/jobs/application_job.rb: -------------------------------------------------------------------------------- 1 | class ApplicationJob < ActiveJob::Base 2 | end 3 | -------------------------------------------------------------------------------- /app/jobs/ssl_certificate_create_job.rb: -------------------------------------------------------------------------------- 1 | class SslCertificateCreateJob < ApplicationJob 2 | def perform(domain) 3 | SSLCertificate.find_or_create_by(domain: domain) 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /app/mailers/admin_mailer.rb: -------------------------------------------------------------------------------- 1 | class AdminMailer < ApplicationMailer 2 | 3 | def new_domain_request(author) 4 | @username = author.username 5 | @id = author.id 6 | @domain = author.domain.domain 7 | @email = author.domain.extended_email 8 | 9 | mail(to: "admin@standardnotes.com", subject: "New Listed domain request") 10 | end 11 | 12 | end 13 | -------------------------------------------------------------------------------- /app/mailers/application_mailer.rb: -------------------------------------------------------------------------------- 1 | class ApplicationMailer < ActionMailer::Base 2 | add_template_helper(ReactOnRailsHelper) 3 | default from: 'Listed ', 4 | reply_to: 'help@standardnotes.com' 5 | layout 'mailer' 6 | end 7 | -------------------------------------------------------------------------------- /app/mailers/authors_mailer.rb: -------------------------------------------------------------------------------- 1 | class AuthorsMailer < ApplicationMailer 2 | def featured(author) 3 | return if author.email_verified == false 4 | 5 | mail( 6 | to: author.email, 7 | subject: 'Congratulations! You are now a featured author on Listed.' 8 | ) 9 | end 10 | 11 | def domain_approved(author) 12 | @url = author.url 13 | mail(to: author.domain.extended_email, subject: 'Your custom domain is live!') 14 | end 15 | 16 | def domain_invalid(email) 17 | mail(to: email, subject: 'Invalid Listed Domain') 18 | end 19 | 20 | def verify_email(author, email) 21 | @display_name = author.title 22 | @verification_link = author.email_verification_link 23 | mail(to: email, subject: 'Verify your Listed author email.') 24 | end 25 | 26 | def unread_guestbook_entries(author_id, entry_ids) 27 | @author = Author.find(author_id) 28 | @entries = GuestbookEntry.where(id: entry_ids) 29 | return if @author.email_verified == false || @entries.empty? 30 | 31 | mail( 32 | to: @author.email, 33 | subject: "You have #{@entries.length} unread guestbook entries" 34 | ) 35 | end 36 | 37 | def new_reaction(reaction_id) 38 | @reaction = Reaction.find(reaction_id) 39 | @post = @reaction.post 40 | @author = @post.author 41 | return if @post.author.email_verified == false 42 | 43 | mail( 44 | to: @author.email, 45 | subject: "New reaction to your post \"#{@post.title}\"" 46 | ) 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /app/models/application_record.rb: -------------------------------------------------------------------------------- 1 | class ApplicationRecord < ActiveRecord::Base 2 | self.abstract_class = true 3 | end 4 | -------------------------------------------------------------------------------- /app/models/concerns/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/standardnotes/listed/5750e416b82060a6058631a2d7fdfc301c3107f6/app/models/concerns/.keep -------------------------------------------------------------------------------- /app/models/concerns/tokenable.rb: -------------------------------------------------------------------------------- 1 | module Tokenable 2 | extend ActiveSupport::Concern 3 | 4 | included do 5 | before_create :generate_token 6 | end 7 | 8 | protected 9 | 10 | def generate_token 11 | token_length = 10 12 | self.token = loop do 13 | range = [*'0'..'9', *'a'..'z', *'A'..'Z'] 14 | random_token = token_length.times.map { range.sample }.join 15 | break random_token unless self.class.exists?(token: random_token) 16 | end 17 | end 18 | 19 | end 20 | -------------------------------------------------------------------------------- /app/models/credential.rb: -------------------------------------------------------------------------------- 1 | class Credential < ApplicationRecord 2 | belongs_to :author 3 | end 4 | -------------------------------------------------------------------------------- /app/models/domain.rb: -------------------------------------------------------------------------------- 1 | class Domain < ApplicationRecord 2 | belongs_to :author 3 | 4 | before_destroy do 5 | SSLCertificate.where(domain: domain).first.destroy 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /app/models/guestbook_entry.rb: -------------------------------------------------------------------------------- 1 | class GuestbookEntry < ApplicationRecord 2 | include Tokenable 3 | belongs_to :author 4 | 5 | def approval_url 6 | "#{author.get_host}/authors/#{author.id}/guestbook/#{id}/approve?token=#{token}" 7 | end 8 | 9 | def unapproval_url 10 | "#{author.get_host}/authors/#{author.id}/guestbook/#{id}/unapprove?token=#{token}" 11 | end 12 | 13 | def deletion_url 14 | "#{author.get_host}/authors/#{author.id}/guestbook/#{id}/delete?token=#{token}" 15 | end 16 | 17 | def spam_url 18 | "#{author.get_host}/authors/#{author.id}/guestbook/#{id}/spam?token=#{token}" 19 | end 20 | 21 | def mark_as_read 22 | self.unread = false 23 | save 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /app/models/reaction.rb: -------------------------------------------------------------------------------- 1 | class Reaction < ApplicationRecord 2 | include Tokenable 3 | 4 | REACTIONS = %w[👍 ❤️ 🫶 👏 👌 🤯 🤔 😂 😍 😭 😢 😡 😮].freeze 5 | 6 | validates :reaction_string, inclusion: { in: REACTIONS } 7 | 8 | belongs_to :post 9 | belongs_to :subscriber, optional: true 10 | 11 | def self.reactions_to_string(reactions) 12 | reactions.group_by(&:reaction_string).map do |reaction_string, local_reactions| 13 | "#{reaction_string} #{local_reactions.length} \u000B\u000B" 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /app/models/ssl_certificate.rb: -------------------------------------------------------------------------------- 1 | class SSLCertificate < LetsEncrypt::Certificate 2 | before_create -> { self.key = OpenSSL::PKey::RSA.new(2048).to_s } 3 | 4 | public :create_order, :start_challenge, :wait_verify_status, :check_verify_status, :retry_on_verify_error 5 | 6 | def renewable? 7 | !renew_after.present? || renew_after <= Time.zone.now 8 | end 9 | 10 | def renew 11 | validation_result = validate 12 | 13 | return validation_result if validation_result != 'valid' 14 | 15 | issue 16 | 17 | 'valid' 18 | end 19 | 20 | # rails-letsencrypt does not return error value on `verify` method, so we can't differentiate 21 | # between a rate limiting error and an invalid domain. Use this method to custom validate 22 | # whether a domain's DNS records are correctly configured. 23 | # Returns 'valid' if valid, 'invalid' if explicitly invalid, and 'error' if API error. 24 | # Based on https://github.com/elct9620/rails-letsencrypt/blob/master/app/models/concerns/lets_encrypt/certificate_verifiable.rb 25 | def validate 26 | create_order 27 | start_challenge 28 | wait_verify_status 29 | status = @challenge.status 30 | return 'invalid' if status == 'invalid' 31 | 32 | 'valid' 33 | rescue StandardError => e 34 | Rails.logger.info "Error validating cerficate #{e.message}" 35 | 'error' 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /app/models/subscriber.rb: -------------------------------------------------------------------------------- 1 | class Subscriber < ApplicationRecord 2 | has_many :subscriptions 3 | has_many :authors, through: :subscriptions 4 | 5 | def subscribed_to_author(author) 6 | authors.include? author 7 | end 8 | 9 | def subscription_for_author(author) 10 | Subscription.find_by(author: author, subscriber: self) 11 | end 12 | 13 | def reaction_creation_token(post) 14 | subscription = subscription_for_author(post.author) 15 | 16 | return nil unless subscription 17 | 18 | Digest::SHA256.hexdigest("#{subscription.token}#{post.id}") 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /app/models/subscription.rb: -------------------------------------------------------------------------------- 1 | class Subscription < ApplicationRecord 2 | include Tokenable 3 | 4 | belongs_to :author 5 | belongs_to :subscriber 6 | 7 | def self.send_weekly_emails 8 | subscriptions = Subscription.where(:frequency => "weekly", :verified => true, :unsubscribed => false) 9 | subscriptions.each do |subscription| 10 | # deliver_later doesn't work for this for some reason as we're calling this from schedule.rb (runner) 11 | # there's an issue where subscription.last_mailing isn't being saved for some subs. I believe it's because 12 | # there may be an exception triggering here causing subscriptions to not save. 13 | begin 14 | SubscriptionMailer.weekly_digest(subscription.id).deliver_now 15 | rescue => e 16 | 17 | end 18 | subscription.last_mailing = DateTime.now 19 | end 20 | subscriptions.each(&:save) 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /app/views/admin_mailer/new_domain_request.html.erb: -------------------------------------------------------------------------------- 1 | <%= react_component("AdminMailerNewDomainRequest", props: { 2 | username: @username, 3 | id: @id, 4 | domain: @domain, 5 | email: @email 6 | }) %> 7 | -------------------------------------------------------------------------------- /app/views/authors/_author_header.html.erb: -------------------------------------------------------------------------------- 1 | <% private_post = post && post.unlisted %> 2 | <% home_url = ENV['HOST'] %> 3 | <% post = local_assigns[:post] %> 4 | <% author = @author || @display_author || (post && post.author) if !private_post %> 5 | <% pages = @pages if !private_post %> 6 | 7 | <%= react_component("AuthorHeader", props: { 8 | homeUrl: home_url, 9 | post: !!post, 10 | isAccessoryPage: @is_accessory_page.nil? ? true : @is_accessory_page, 11 | author: (author if !private_post).as_json( 12 | only: [:guestbook_disabled, :newsletter_disabled, :bio, :link, :twitter, :header_image_url], 13 | methods: [:title, :url, :last_word_count, :personal_link], 14 | include: { 15 | credentials: { 16 | only: :id 17 | } 18 | } 19 | ), 20 | privatePost: private_post, 21 | pages: pages.as_json( 22 | only: [:id, :title, :page_link], 23 | methods: [:author_relative_url] 24 | ), 25 | currentUrl: request.url, 26 | blogPage: (@blog_page if !private_post), 27 | }) %> 28 | -------------------------------------------------------------------------------- /app/views/authors/all.html.erb: -------------------------------------------------------------------------------- 1 | <%= react_component("AuthorAll", props: { 2 | posts: @posts.as_json( 3 | only: [:id, :title, :unlisted, :page, :created_at, :word_count], 4 | methods: [:author_relative_url, :preview_text, :rendered_text] 5 | ), 6 | }) %> 7 | -------------------------------------------------------------------------------- /app/views/authors/feed.rss.builder: -------------------------------------------------------------------------------- 1 | xml.instruct! :xml, :version => "1.0" 2 | xml.rss :version => "2.0" do 3 | xml.channel do 4 | xml.title @author.title 5 | xml.author @author.display_name 6 | xml.description @author.bio 7 | xml.link @author.link 8 | xml.language "en" 9 | 10 | for post in @posts 11 | xml.item do 12 | if post.title 13 | xml.title post.title 14 | else 15 | xml.title "" 16 | end 17 | xml.author @author.display_name 18 | xml.pubDate post.created_at.to_s(:rfc822) 19 | xml.link post.author_relative_url 20 | xml.guid post.author_relative_url 21 | 22 | text = post.rendered_text 23 | xml.description "

" + text + "

" 24 | end 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /app/views/authors/show.html.erb: -------------------------------------------------------------------------------- 1 | <%= react_component("AuthorShow", props: { 2 | posts: @posts.as_json( 3 | only: [:id, :title, :unlisted, :page, :created_at, :word_count, :author_name, :author_link], 4 | methods: [:author_relative_url, :preview_text, :rendered_text] 5 | ), 6 | olderThan: @older_than, 7 | newerThan: @newer_than, 8 | lastPostId: @last_post_id, 9 | displayAuthor: @display_author.as_json( 10 | only: :id 11 | ) 12 | }) %> 13 | -------------------------------------------------------------------------------- /app/views/authors/subscribe.html.erb: -------------------------------------------------------------------------------- 1 | <% subscribed_to_author = @subscriber && @subscriber.subscribed_to_author(@display_author) %> 2 | <% subscription_for_author = @subscriber && @subscriber.subscription_for_author(@display_author).as_json( 3 | only: [:verified, :verification_sent_at] 4 | ) %> 5 | 6 | <%= react_component("AuthorSubscribe", props: { 7 | displayAuthor: @display_author.as_json( 8 | only: [:id, :username], 9 | methods: [:title, :rss_url] 10 | ), 11 | subscribedToAuthor: subscribed_to_author, 12 | subscriptionForAuthor: subscription_for_author, 13 | subscriptionSuccess: flash[:subscription_success], 14 | authenticityToken: form_authenticity_token 15 | }) %> -------------------------------------------------------------------------------- /app/views/authors/tip.html.erb: -------------------------------------------------------------------------------- 1 | <%= react_component("AuthorTip", props: { 2 | displayAuthor: @display_author.as_json( 3 | only: [:display_name], 4 | methods: [:title, :url], 5 | include: { 6 | credentials: { 7 | only: [:id, :key, :value] 8 | } 9 | } 10 | ) 11 | }) %> 12 | -------------------------------------------------------------------------------- /app/views/authors/verify_email.html.haml: -------------------------------------------------------------------------------- 1 | #verify-email.page-container 2 | %p.p2 Your email has been verified. 3 | -------------------------------------------------------------------------------- /app/views/authors_mailer/domain_approved.html.erb: -------------------------------------------------------------------------------- 1 | <%= react_component("AuthorsMailerDomainApproved", props: { 2 | url: @url 3 | }) %> 4 | -------------------------------------------------------------------------------- /app/views/authors_mailer/domain_invalid.html.erb: -------------------------------------------------------------------------------- 1 | <%= react_component("AuthorsMailerDomainInvalid", props: { 2 | customDomainIP: ENV["CUSTOM_DOMAIN_IP"] 3 | }) %> 4 | -------------------------------------------------------------------------------- /app/views/authors_mailer/featured.html.erb: -------------------------------------------------------------------------------- 1 | <%= react_component("AuthorsMailerFeatured") %> 2 | -------------------------------------------------------------------------------- /app/views/authors_mailer/new_reaction.html.erb: -------------------------------------------------------------------------------- 1 | <%= react_component("AuthorsMailerNewReaction", props: { 2 | reaction: @reaction, 3 | author: @author.as_json( 4 | only: :display_name 5 | ), 6 | post: @post.as_json( 7 | only: [:id, :title], 8 | methods: [:url] 9 | ), 10 | }) %> 11 | -------------------------------------------------------------------------------- /app/views/authors_mailer/unread_guestbook_entries.html.erb: -------------------------------------------------------------------------------- 1 | <%= react_component("AuthorsMailerUnreadGuestbookEntries", props: { 2 | author: @author.as_json( 3 | only: :display_name 4 | ), 5 | entries: @entries.as_json( 6 | only: [:id, :created_at, :text, :donation_info, :signer_email], 7 | methods: [:approval_url, :unapproval_url, :spam_url] 8 | ) 9 | }) %> 10 | -------------------------------------------------------------------------------- /app/views/authors_mailer/verify_email.html.erb: -------------------------------------------------------------------------------- 1 | <%= react_component("AuthorsMailerVerifyEmail", props: { 2 | displayName: @display_name, 3 | verificationLink: @verification_link 4 | }) %> 5 | -------------------------------------------------------------------------------- /app/views/guestbook_entries/index.html.erb: -------------------------------------------------------------------------------- 1 | <% sent = params[:sent] %> 2 | <% new_author_guestbook_entry_url = new_author_guestbook_entry_path(@author) %> 3 | <% hcaptcha_site_key = ENV["HCAPTCHA_SITE_KEY"] %> 4 | 5 | <%= react_component("Guestbook", props: { 6 | sent: sent, 7 | author: @author.as_json( 8 | only: :id, 9 | methods: [:title, :url] 10 | ), 11 | entries: @entries.as_json( 12 | only: [:id, :text] 13 | ), 14 | newAuthorGuestbookEntryUrl: new_author_guestbook_entry_url, 15 | hCaptchaSiteKey: hcaptcha_site_key 16 | }) %> 17 | -------------------------------------------------------------------------------- /app/views/help/index.html.erb: -------------------------------------------------------------------------------- 1 | <%= react_component("Help") %> 2 | -------------------------------------------------------------------------------- /app/views/layouts/mailer.html.haml: -------------------------------------------------------------------------------- 1 | %hmtl 2 | %body 3 | = yield -------------------------------------------------------------------------------- /app/views/layouts/mailer.text.erb: -------------------------------------------------------------------------------- 1 | <%= yield %> 2 | -------------------------------------------------------------------------------- /app/views/layouts/mailer.text.haml: -------------------------------------------------------------------------------- 1 | = yield -------------------------------------------------------------------------------- /app/views/posts/index.html.erb: -------------------------------------------------------------------------------- 1 | <%= react_component("Posts", props: { 2 | days: @days 3 | }) %> 4 | -------------------------------------------------------------------------------- /app/views/posts/show.html.erb: -------------------------------------------------------------------------------- 1 | <% subscribed_to_author = @subscriber && @subscriber.subscribed_to_author(@post.author) %> 2 | <% subscription_for_author = @subscriber && @subscriber.subscription_for_author(@post.author).as_json( 3 | only: [:verified, :verification_sent_at] 4 | ) %> 5 | <% private_post = @post.unlisted %> 6 | 7 | <%= react_component("PostShow", props: { 8 | reactionLinks: @reaction_links, 9 | post: @post.as_json( 10 | only: [:title, :unlisted, :page, :created_at, :word_count, :author_name, :author_link], 11 | methods: private_post ? [:preview_text, :rendered_text] : [:author_relative_url, :preview_text, :rendered_text], 12 | include: private_post ? nil : { author: { 13 | only: [:id, :username, :newsletter_disabled], 14 | methods: [:title, :url] 15 | } 16 | } 17 | ), 18 | previous: @previous.as_json( 19 | only: [:title, :unlisted, :page, :created_at, :word_count], 20 | methods: [:author_relative_url, :preview_text, :rendered_text] 21 | ), 22 | next: @next.as_json( 23 | only: [:title, :unlisted, :page, :created_at, :word_count], 24 | methods: [:author_relative_url, :preview_text, :rendered_text] 25 | ), 26 | subscribedToAuthor: subscribed_to_author, 27 | subscriptionForAuthor: subscription_for_author, 28 | subscriptionSuccess: flash[:subscription_success] 29 | }) %> 30 | -------------------------------------------------------------------------------- /app/views/reactions/create_via_email.html.erb: -------------------------------------------------------------------------------- 1 | <%= react_component("ReactionSuccess") %> 2 | -------------------------------------------------------------------------------- /app/views/reactions/new.html.erb: -------------------------------------------------------------------------------- 1 | <% hcaptcha_site_key = ENV["HCAPTCHA_SITE_KEY"] %> 2 | <% private_post = @post.unlisted %> 3 | 4 | <%= react_component("NewReaction", props: { 5 | reactionString: @reaction_string, 6 | post: @post.as_json( 7 | only: [:id, :title, :unlisted, :page, :created_at, :word_count, :author_name, :author_link], 8 | methods: private_post ? [:preview_text, :rendered_text] : [:author_relative_url, :preview_text, :rendered_text, :url], 9 | include: private_post ? nil : { author: { 10 | only: [:id, :username], 11 | methods: [:title, :url] 12 | } 13 | } 14 | ), 15 | hCaptchaSiteKey: hcaptcha_site_key 16 | }) %> 17 | -------------------------------------------------------------------------------- /app/views/robots/index.text.erb: -------------------------------------------------------------------------------- 1 | # See http://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file 2 | # 3 | # To ban all spiders from the entire site uncomment the next two lines: 4 | # Disallow: / 5 | 6 | User-agent: * 7 | Allow: / 8 | 9 | Sitemap: <%= @sitemap %> 10 | -------------------------------------------------------------------------------- /app/views/shared/_footer.html.erb: -------------------------------------------------------------------------------- 1 | <%= react_component("SharedFooter", props: { 2 | blogPage: @blog_page, 3 | author: (@display_author || @author || @post && @post.author if !@post || !@post.unlisted).as_json( 4 | methods: [:title], 5 | only: :title, 6 | ), 7 | privatePost: @post && @post.unlisted 8 | }) %> 9 | -------------------------------------------------------------------------------- /app/views/shared/_header.html.haml: -------------------------------------------------------------------------------- 1 | = render("authors/author_header", 2 | :author => @author, 3 | :post => @post, 4 | :is_accessory_page => @is_accessory_page) -------------------------------------------------------------------------------- /app/views/sitemap/authors.xml.haml: -------------------------------------------------------------------------------- 1 | !!! XML 2 | %urlset{:xmlns => "http://www.sitemaps.org/schemas/sitemap/0.9"} 3 | - for author in @authors 4 | %url 5 | %loc= author.url 6 | %changefreq weekly 7 | %priority 0.75 8 | -------------------------------------------------------------------------------- /app/views/sitemap/custom_domain.xml.haml: -------------------------------------------------------------------------------- 1 | !!! XML 2 | %urlset{:xmlns => "http://www.sitemaps.org/schemas/sitemap/0.9"} 3 | %url 4 | %loc= @domain_author.url 5 | %changefreq weekly 6 | %priority 0.75 7 | 8 | - for post in @posts 9 | %url 10 | %loc= post.author_relative_url 11 | %lastmod=post.updated_at.strftime('%Y-%m-%d') 12 | %changefreq monthly -------------------------------------------------------------------------------- /app/views/sitemap/index.xml.haml: -------------------------------------------------------------------------------- 1 | !!! XML 2 | - if @domain_author 3 | %urlset{:xmlns => "http://www.sitemaps.org/schemas/sitemap/0.9"} 4 | %url 5 | %loc= @domain_author.url 6 | %changefreq weekly 7 | %priority 0.75 8 | 9 | - for post in @posts 10 | %url 11 | %loc= post.author_relative_url 12 | %lastmod=post.updated_at.strftime('%Y-%m-%d') 13 | %changefreq monthly 14 | - else 15 | %sitemapindex{:xmlns => "http://www.sitemaps.org/schemas/sitemap/0.9"} 16 | - for page in @post_pages 17 | %sitemap 18 | %loc= "#{ENV['HOST']}/posts-sitemap-#{page}.xml" 19 | - for page in @author_pages 20 | %sitemap 21 | %loc= "#{ENV['HOST']}/authors-sitemap-#{page}.xml" -------------------------------------------------------------------------------- /app/views/sitemap/posts.xml.haml: -------------------------------------------------------------------------------- 1 | !!! XML 2 | %urlset{:xmlns => "http://www.sitemaps.org/schemas/sitemap/0.9"} 3 | - for post in @posts 4 | %url 5 | %loc= post.author_relative_url 6 | %lastmod=post.updated_at.strftime('%Y-%m-%d') 7 | %changefreq monthly 8 | -------------------------------------------------------------------------------- /app/views/subscription_mailer/new_post.html.erb: -------------------------------------------------------------------------------- 1 | <%= react_component("SubscriptionMailerNewPost", props: { 2 | hiddenText: @post.text.html_safe.slice(0, 100) + "...", 3 | reactionLinks: @reaction_links, 4 | post: @post.as_json( 5 | only: :title, 6 | methods: :url, 7 | include: { 8 | author: { 9 | only: :email, 10 | methods: [:url, :title] 11 | } 12 | } 13 | ), 14 | renderedText: @rendered_text, 15 | weeklyUrl: @weekly_url, 16 | unsubscribeUrl: @unsubscribe_url 17 | }) %> 18 | -------------------------------------------------------------------------------- /app/views/subscription_mailer/new_subscription.html.erb: -------------------------------------------------------------------------------- 1 | <%= react_component("SubscriptionMailerNewSubscription", props: { 2 | author: @author.as_json( 3 | only: [], 4 | methods: :verified_subscriptions.as_json( 5 | only: :id 6 | ) 7 | ) 8 | }) %> 9 | -------------------------------------------------------------------------------- /app/views/subscription_mailer/privacy_policy_update.html.erb: -------------------------------------------------------------------------------- 1 | <%= react_component("SubscriptionMailerPrivacyUpdate", props: { 2 | subscriber: @subscriber.as_json( 3 | only: :email 4 | ), 5 | author: @author.as_json( 6 | only: [], 7 | methods: [:title, :url] 8 | ), 9 | unsubscribeUrl: @unsubscribe_url 10 | }) %> -------------------------------------------------------------------------------- /app/views/subscription_mailer/subscription_success.html.erb: -------------------------------------------------------------------------------- 1 | <%= react_component("SubscriptionMailerSubscriptionSuccess", props: { 2 | authorUrl: @author_url, 3 | unsubscribeUrl: @unsubscribe_url, 4 | author: @author.as_json( 5 | only: [], 6 | methods: :title 7 | ) 8 | }) %> 9 | -------------------------------------------------------------------------------- /app/views/subscription_mailer/weekly_digest.html.erb: -------------------------------------------------------------------------------- 1 | <%= react_component("SubscriptionMailerWeeklyDigest", props: { 2 | author: @author.as_json( 3 | only: :email, 4 | methods: :title 5 | ), 6 | posts: @posts.as_json( 7 | only: [:id, :title, :created_at], 8 | methods: [:author_relative_url, :rendered_text] 9 | ), 10 | unsubscribeUrl: @unsubscribe_url 11 | }) %> 12 | -------------------------------------------------------------------------------- /app/views/subscriptions/confirm.html.erb: -------------------------------------------------------------------------------- 1 | <%= react_component("SubscriptionConfirm") %> 2 | -------------------------------------------------------------------------------- /app/views/subscriptions/unsubscribe.html.erb: -------------------------------------------------------------------------------- 1 | <%= react_component("SubscriptionUnsubscribe", props: { 2 | subscription: @subscription.as_json( 3 | only: [], 4 | include: { 5 | author: { 6 | only: [], 7 | methods: :title 8 | } 9 | } 10 | ) 11 | }) %> 12 | -------------------------------------------------------------------------------- /app/views/subscriptions/update_frequency.html.erb: -------------------------------------------------------------------------------- 1 | <%= react_component("SubscriptionUpdateFrequency", props: { 2 | subscription: @subscription.as_json( 3 | only: [], 4 | include: { 5 | author: { 6 | only: [], 7 | methods: :title 8 | } 9 | } 10 | ) 11 | }) %> 12 | -------------------------------------------------------------------------------- /app/views/subscriptions/validate.html.erb: -------------------------------------------------------------------------------- 1 | <% hcaptcha_site_key = ENV["HCAPTCHA_SITE_KEY"] %> 2 | 3 | <%= react_component("SubscriptionValidate", props: { 4 | subscription: @subscription.as_json( 5 | only: :id, 6 | include: { 7 | author: { 8 | only: [], 9 | methods: :title 10 | } 11 | } 12 | ), 13 | hCaptchaSiteKey: hcaptcha_site_key 14 | }) %> 15 | -------------------------------------------------------------------------------- /app/views/usage/index.html.erb: -------------------------------------------------------------------------------- 1 | <%= react_component("Usage", props: { 2 | activeAuthors: @active_authors.as_json( 3 | only: [:id, :featured, :bio, :title, :last_word_count], 4 | methods: [:url, :title] 5 | ), 6 | featuredAuthors: @featured_authors.as_json( 7 | only: [:id, :featured, :bio, :title, :last_word_count], 8 | methods: [:url, :title] 9 | ) 10 | }) %> 11 | -------------------------------------------------------------------------------- /app/views/usage/new_author.html.erb: -------------------------------------------------------------------------------- 1 | <%= react_component("UsageNewAuthor", props: { 2 | secretUrl: @secret_url 3 | }) %> 4 | -------------------------------------------------------------------------------- /app/workers/account_creation_worker.rb: -------------------------------------------------------------------------------- 1 | require 'zlib' 2 | 3 | class AccountCreationWorker 4 | include Shoryuken::Worker 5 | 6 | shoryuken_options queue: ENV.fetch('SQS_QUEUE_URL'), auto_delete: true 7 | 8 | def perform(sqs_msg, body) 9 | parsed_body = JSON.parse(body) 10 | decompressed_message = decompress_message(parsed_body['Message']) 11 | 12 | Rails.logger.info "Received account creation event for user #{decompressed_message['payload']['userEmail']}" 13 | 14 | author = Author.new 15 | secret = EncryptionHelper.generate_random_key 16 | author.secret = secret 17 | author.email = decompressed_message['payload']['userEmail'] 18 | author.save 19 | 20 | AuthorsMailer.verify_email(author, author.email).deliver_later 21 | end 22 | 23 | private 24 | 25 | def decompress_message(message) 26 | decoded_message = Base64.decode64(message) 27 | zstream = Zlib::Inflate.new 28 | buffer = zstream.inflate(decoded_message) 29 | zstream.finish 30 | zstream.close 31 | 32 | return JSON.parse(buffer) 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = (api) => { 2 | const validEnv = ["development", "production"]; 3 | const currentEnv = api.env(); 4 | const isDevelopmentEnv = api.env("development"); 5 | 6 | if (!validEnv.includes(currentEnv)) { 7 | throw new Error( 8 | `${"Please specify a valid `NODE_ENV` or " 9 | + "`BABEL_ENV` environment variables. Valid values are \"development\", " 10 | + "\"test\", and \"production\". Instead, received: "}${ 11 | JSON.stringify(currentEnv) 12 | }.`, 13 | ); 14 | } 15 | 16 | return { 17 | presets: [ 18 | [ 19 | "@babel/preset-env", 20 | { 21 | forceAllTransforms: true, 22 | useBuiltIns: "entry", 23 | corejs: 3, 24 | modules: false, 25 | exclude: ["transform-typeof-symbol"], 26 | }, 27 | ], 28 | [ 29 | "@babel/preset-react", 30 | { 31 | development: isDevelopmentEnv, 32 | useBuiltIns: true, 33 | }, 34 | ], 35 | ], 36 | plugins: ["@babel/plugin-transform-runtime"], 37 | }; 38 | }; 39 | -------------------------------------------------------------------------------- /bin/bundle: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) 3 | load Gem.bin_path('bundler', 'bundle') 4 | -------------------------------------------------------------------------------- /bin/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | begin 3 | load File.expand_path('../spring', __FILE__) 4 | rescue LoadError => e 5 | raise unless e.message.include?('spring') 6 | end 7 | APP_PATH = File.expand_path('../config/application', __dir__) 8 | require_relative '../config/boot' 9 | require 'rails/commands' 10 | -------------------------------------------------------------------------------- /bin/rake: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | begin 3 | load File.expand_path('../spring', __FILE__) 4 | rescue LoadError => e 5 | raise unless e.message.include?('spring') 6 | end 7 | require_relative '../config/boot' 8 | require 'rake' 9 | Rake.application.run 10 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require 'pathname' 3 | require 'fileutils' 4 | include FileUtils 5 | 6 | # path to your application root. 7 | APP_ROOT = Pathname.new File.expand_path('../../', __FILE__) 8 | 9 | def system!(*args) 10 | system(*args) || abort("\n== Command #{args} failed ==") 11 | end 12 | 13 | chdir APP_ROOT do 14 | # This script is a starting point to setup your application. 15 | # Add necessary setup steps to this file. 16 | 17 | puts '== Installing dependencies ==' 18 | system! 'gem install bundler --conservative' 19 | system('bundle check') || system!('bundle install') 20 | 21 | # puts "\n== Copying sample files ==" 22 | # unless File.exist?('config/database.yml') 23 | # cp 'config/database.yml.sample', 'config/database.yml' 24 | # end 25 | 26 | puts "\n== Preparing database ==" 27 | system! 'bin/rails db:setup' 28 | 29 | puts "\n== Removing old logs and tempfiles ==" 30 | system! 'bin/rails log:clear tmp:clear' 31 | 32 | puts "\n== Restarting application server ==" 33 | system! 'bin/rails restart' 34 | end 35 | -------------------------------------------------------------------------------- /bin/spring: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | # This file loads spring without using Bundler, in order to be fast. 4 | # It gets overwritten when you run the `spring binstub` command. 5 | 6 | unless defined?(Spring) 7 | require 'rubygems' 8 | require 'bundler' 9 | 10 | lockfile = Bundler::LockfileParser.new(Bundler.default_lockfile.read) 11 | spring = lockfile.specs.detect { |spec| spec.name == "spring" } 12 | if spring 13 | Gem.use_paths Gem.dir, Bundler.bundle_path.to_s, *Gem.path 14 | gem 'spring', spring.version 15 | require 'spring/binstub' 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /bin/update: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require 'pathname' 3 | require 'fileutils' 4 | include FileUtils 5 | 6 | # path to your application root. 7 | APP_ROOT = Pathname.new File.expand_path('../../', __FILE__) 8 | 9 | def system!(*args) 10 | system(*args) || abort("\n== Command #{args} failed ==") 11 | end 12 | 13 | chdir APP_ROOT do 14 | # This script is a way to update your development environment automatically. 15 | # Add necessary update steps to this file. 16 | 17 | puts '== Installing dependencies ==' 18 | system! 'gem install bundler --conservative' 19 | system('bundle check') || system!('bundle install') 20 | 21 | puts "\n== Updating database ==" 22 | system! 'bin/rails db:migrate' 23 | 24 | puts "\n== Removing old logs and tempfiles ==" 25 | system! 'bin/rails log:clear tmp:clear' 26 | 27 | puts "\n== Restarting application server ==" 28 | system! 'bin/rails restart' 29 | end 30 | -------------------------------------------------------------------------------- /bin/webpack: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | ENV["RAILS_ENV"] ||= ENV["RACK_ENV"] || "development" 4 | ENV["NODE_ENV"] ||= "development" 5 | 6 | require "pathname" 7 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile", 8 | Pathname.new(__FILE__).realpath) 9 | 10 | require "bundler/setup" 11 | 12 | require "webpacker" 13 | require "webpacker/webpack_runner" 14 | 15 | APP_ROOT = File.expand_path("..", __dir__) 16 | Dir.chdir(APP_ROOT) do 17 | Webpacker::WebpackRunner.run(ARGV) 18 | end 19 | -------------------------------------------------------------------------------- /bin/webpack-dev-server: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | ENV["RAILS_ENV"] ||= ENV["RACK_ENV"] || "development" 4 | ENV["NODE_ENV"] ||= "development" 5 | 6 | require "pathname" 7 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile", 8 | Pathname.new(__FILE__).realpath) 9 | 10 | require "bundler/setup" 11 | 12 | require "webpacker" 13 | require "webpacker/dev_server_runner" 14 | 15 | APP_ROOT = File.expand_path("..", __dir__) 16 | Dir.chdir(APP_ROOT) do 17 | Webpacker::DevServerRunner.run(ARGV) 18 | end 19 | -------------------------------------------------------------------------------- /certificates/.gtikeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/standardnotes/listed/5750e416b82060a6058631a2d7fdfc301c3107f6/certificates/.gtikeep -------------------------------------------------------------------------------- /client/app/assets/fonts/Merriweather-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/standardnotes/listed/5750e416b82060a6058631a2d7fdfc301c3107f6/client/app/assets/fonts/Merriweather-Bold.ttf -------------------------------------------------------------------------------- /client/app/assets/fonts/Roboto-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/standardnotes/listed/5750e416b82060a6058631a2d7fdfc301c3107f6/client/app/assets/fonts/Roboto-Bold.ttf -------------------------------------------------------------------------------- /client/app/assets/fonts/Roboto-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/standardnotes/listed/5750e416b82060a6058631a2d7fdfc301c3107f6/client/app/assets/fonts/Roboto-Regular.ttf -------------------------------------------------------------------------------- /client/app/assets/gifs/index.js: -------------------------------------------------------------------------------- 1 | import PublishGif from "./listed-publish.gif"; 2 | import SettingsGif from "./listed-settings.gif"; 3 | 4 | export { 5 | PublishGif, 6 | SettingsGif, 7 | }; 8 | -------------------------------------------------------------------------------- /client/app/assets/gifs/listed-getting-started.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/standardnotes/listed/5750e416b82060a6058631a2d7fdfc301c3107f6/client/app/assets/gifs/listed-getting-started.gif -------------------------------------------------------------------------------- /client/app/assets/gifs/listed-publish.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/standardnotes/listed/5750e416b82060a6058631a2d7fdfc301c3107f6/client/app/assets/gifs/listed-publish.gif -------------------------------------------------------------------------------- /client/app/assets/gifs/listed-settings.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/standardnotes/listed/5750e416b82060a6058631a2d7fdfc301c3107f6/client/app/assets/gifs/listed-settings.gif -------------------------------------------------------------------------------- /client/app/assets/icons/ic-arrow-long.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /client/app/assets/icons/ic-arrow-up.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /client/app/assets/icons/ic-book.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /client/app/assets/icons/ic-checkbox-checked.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /client/app/assets/icons/ic-checkbox-empty.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /client/app/assets/icons/ic-chevron-down.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /client/app/assets/icons/ic-chevron-up.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /client/app/assets/icons/ic-close.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /client/app/assets/icons/ic-code.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /client/app/assets/icons/ic-credit-card.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /client/app/assets/icons/ic-earth.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /client/app/assets/icons/ic-edit.svg: -------------------------------------------------------------------------------- 1 | Create -------------------------------------------------------------------------------- /client/app/assets/icons/ic-email.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /client/app/assets/icons/ic-eye-off-filled.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /client/app/assets/icons/ic-eye-off.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /client/app/assets/icons/ic-lifebuoy.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /client/app/assets/icons/ic-link.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /client/app/assets/icons/ic-listed.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /client/app/assets/icons/ic-menu.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /client/app/assets/icons/ic-more-horizontal.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /client/app/assets/icons/ic-open-in.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /client/app/assets/icons/ic-palette.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /client/app/assets/icons/ic-radio-button-empty.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /client/app/assets/icons/ic-radio-button-selected.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /client/app/assets/icons/ic-settings-filled.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /client/app/assets/icons/ic-star-circle-filled.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /client/app/assets/icons/ic-text-rich.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /client/app/assets/icons/ic-trash.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /client/app/assets/icons/ic-wallet-filled.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /client/app/assets/images/carded-blog.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /client/app/assets/images/condensed-cover.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /client/app/assets/images/full-cover.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /client/app/assets/images/index.js: -------------------------------------------------------------------------------- 1 | import CardedBlogImage from "./carded-blog.svg"; 2 | import CondensedCoverImage from "./condensed-cover.svg"; 3 | import FullCoverImage from "./full-cover.svg"; 4 | import VerticalBlogImage from "./vertical-blog.svg"; 5 | 6 | export { 7 | CardedBlogImage, 8 | CondensedCoverImage, 9 | FullCoverImage, 10 | VerticalBlogImage, 11 | }; 12 | -------------------------------------------------------------------------------- /client/app/assets/images/vertical-blog.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /client/app/assets/styles/mixins.scss: -------------------------------------------------------------------------------- 1 | @mixin Desktop() { 2 | @media (min-width: 992px) { 3 | @content; 4 | } 5 | } 6 | 7 | @mixin DesktopWide() { 8 | @media (min-width: 1400px) { 9 | @content; 10 | } 11 | } 12 | 13 | @mixin MenuHover($color) { 14 | color: $color; 15 | 16 | &.button--no-fill { 17 | background-color: transparent; 18 | position: relative; 19 | 20 | &::before { 21 | background-image: 22 | linear-gradient(to top, $color 50%, transparent 50%); 23 | opacity: 0.08; 24 | } 25 | 26 | &:hover { 27 | color: $color; 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /client/app/components/admin_mailer/NewDomainRequest.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | 4 | const NewDomainRequest = ({ 5 | username, id, domain, email, 6 | }) => ( 7 |
8 |

A new domain request is ready for review.

9 |

10 | Author: 11 | {username} 12 |

13 |

14 | Author ID: 15 | {id} 16 |

17 |

18 | Request domain: 19 | {domain} 20 |

21 |

22 | Author email: 23 | {email} 24 |

25 |
26 | ); 27 | 28 | NewDomainRequest.propTypes = { 29 | username: PropTypes.string.isRequired, 30 | id: PropTypes.number.isRequired, 31 | domain: PropTypes.string.isRequired, 32 | email: PropTypes.string.isRequired, 33 | }; 34 | 35 | export default NewDomainRequest; 36 | -------------------------------------------------------------------------------- /client/app/components/authors/All.jsx: -------------------------------------------------------------------------------- 1 | import PropTypes from "prop-types"; 2 | import React from "react"; 3 | import Post from "../posts/Post"; 4 | import ScrollToTopButton from "../shared/ScrollToTopButton"; 5 | import "./All.scss"; 6 | 7 | const All = ({ posts }) => ( 8 |
9 |
10 | {posts.map((post) => ( 11 |
12 | 13 |
14 | ))} 15 |
16 | 17 |
18 | ); 19 | 20 | All.propTypes = { 21 | posts: PropTypes.arrayOf( 22 | PropTypes.shape({ 23 | id: PropTypes.number.isRequired, 24 | }), 25 | ).isRequired, 26 | }; 27 | 28 | export default (props) => ; 29 | -------------------------------------------------------------------------------- /client/app/components/authors/All.scss: -------------------------------------------------------------------------------- 1 | #author-all { 2 | padding: 0 $spacing-16; 3 | 4 | @include Desktop() { 5 | padding: 0 $spacing-11-vw; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /client/app/components/authors/SettingsPage.scss: -------------------------------------------------------------------------------- 1 | .accessible-via { 2 | list-style-type: none; 3 | margin: 0; 4 | padding: 0; 5 | } 6 | -------------------------------------------------------------------------------- /client/app/components/authors/Show.scss: -------------------------------------------------------------------------------- 1 | #author-profile { 2 | padding: 0 $spacing-16; 3 | background-color: var(--author-posts-background-color); 4 | 5 | .older { 6 | padding: $spacing-48 0; 7 | display: flex; 8 | justify-content: center; 9 | 10 | button { 11 | color: var(--link-color); 12 | display: flex; 13 | 14 | svg { 15 | margin-left: $spacing-4; 16 | 17 | path { 18 | fill: var(--link-color); 19 | } 20 | } 21 | } 22 | 23 | @include Desktop() { 24 | padding: $spacing-56 0; 25 | } 26 | } 27 | 28 | @include Desktop() { 29 | padding: 0 $spacing-11-vw; 30 | } 31 | } 32 | 33 | .author-post { 34 | padding: $spacing-48 0; 35 | 36 | &:not(:last-child) { 37 | border-bottom: var(--border); 38 | } 39 | 40 | @include Desktop() { 41 | padding: $spacing-56 $spacing-60; 42 | } 43 | 44 | @include DesktopWide() { 45 | padding: $spacing-56 $spacing-136; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /client/app/components/authors/Subscribe.scss: -------------------------------------------------------------------------------- 1 | #subscribe-page { 2 | .h1 { 3 | margin-bottom: $spacing-8; 4 | } 5 | 6 | #subscription-form { 7 | margin: $spacing-16 0; 8 | } 9 | 10 | .subscribe-page__rss-feed { 11 | text-align: center; 12 | 13 | @include Desktop() { 14 | text-align: left; 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /client/app/components/authors/SubscriptionForm.scss: -------------------------------------------------------------------------------- 1 | #subscription-form { 2 | .text-field { 3 | width: 100%; 4 | margin: 0 $spacing-12 $spacing-12 0; 5 | 6 | @include Desktop() { 7 | width: 268px; 8 | margin-bottom: 0; 9 | } 10 | } 11 | 12 | .button { 13 | width: 100%; 14 | 15 | @include Desktop() { 16 | width: auto; 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /client/app/components/authors/Tip.scss: -------------------------------------------------------------------------------- 1 | #tip { 2 | .h1, .p1 { 3 | margin-bottom: $spacing-8; 4 | } 5 | 6 | .tip__credentials { 7 | margin-top: $spacing-24; 8 | } 9 | 10 | .callout { 11 | width: 100%; 12 | 13 | &:not(:last-child) { 14 | margin-bottom: $spacing-12; 15 | } 16 | } 17 | } -------------------------------------------------------------------------------- /client/app/components/authors/header/MenuContainer.jsx: -------------------------------------------------------------------------------- 1 | import PropTypes from "prop-types"; 2 | import React from "react"; 3 | import { HomepageMenu, AuthorMenu } from "./menu"; 4 | import "./MenuContainer.scss"; 5 | 6 | const MenuContainer = ({ 7 | isMobileMenuOpen, 8 | isDesktopMenu, 9 | author, 10 | pages, 11 | privatePost, 12 | currentUrl, 13 | }) => ( 14 |
15 | {!author && !privatePost && ( 16 | 17 | )} 18 | {author && !privatePost && ( 19 | 26 | )} 27 |
28 | ); 29 | 30 | MenuContainer.propTypes = { 31 | author: PropTypes.shape({}), 32 | currentUrl: PropTypes.string.isRequired, 33 | isDesktopMenu: PropTypes.bool.isRequired, 34 | isMobileMenuOpen: PropTypes.bool.isRequired, 35 | pages: PropTypes.arrayOf( 36 | PropTypes.shape({}), 37 | ), 38 | privatePost: PropTypes.bool, 39 | }; 40 | 41 | MenuContainer.defaultProps = { 42 | author: null, 43 | pages: null, 44 | privatePost: false, 45 | }; 46 | 47 | export default MenuContainer; 48 | -------------------------------------------------------------------------------- /client/app/components/authors/header/MenuContainer.scss: -------------------------------------------------------------------------------- 1 | .pages-menu__container { 2 | overflow: hidden; 3 | } 4 | 5 | .pages-menu--mobile { 6 | display: flex; 7 | flex-direction: column; 8 | text-align: center; 9 | padding: $spacing-16 $spacing-16 $spacing-6; 10 | visibility: hidden; 11 | margin-top: -100%; 12 | transition: all 0.3s ease-in-out; 13 | 14 | .button { 15 | width: 100%; 16 | 17 | &:not(:last-child) { 18 | margin-bottom: $spacing-26; 19 | } 20 | 21 | &.button--primary { 22 | margin-bottom: $spacing-14; 23 | } 24 | } 25 | 26 | &.pages-menu--mobile-visible { 27 | visibility: visible; 28 | margin-top: 0; 29 | } 30 | 31 | @include Desktop() { 32 | display: none; 33 | } 34 | } 35 | 36 | .pages-menu--desktop { 37 | display: none; 38 | 39 | @include Desktop() { 40 | display: flex; 41 | align-items: flex-start; 42 | 43 | .button { 44 | margin-left: $spacing-8; 45 | 46 | &.button--primary { 47 | margin-left: $spacing-20; 48 | } 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /client/app/components/authors/header/index.js: -------------------------------------------------------------------------------- 1 | import MenuContainer from "./MenuContainer"; 2 | import AuthorInfo from "./AuthorInfo"; 3 | 4 | export { MenuContainer, AuthorInfo }; 5 | -------------------------------------------------------------------------------- /client/app/components/authors/header/menu/AuthorMenu.scss: -------------------------------------------------------------------------------- 1 | .pages-menu .page-link { 2 | @include MenuHover(var(--page-menu-link-color)); 3 | 4 | &.button--active::before { 5 | background-color: var(--page-menu-link-color); 6 | opacity: 0.08; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /client/app/components/authors/header/menu/HomepageMenu.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import StartWriting from "../../../shared/StartWriting"; 4 | 5 | const HomepageMenu = ({ isMobileMenuOpen, isDesktopMenu }) => { 6 | const navClassName = () => { 7 | if (isDesktopMenu) { 8 | return "pages-menu pages-menu--desktop"; 9 | } 10 | 11 | return `pages-menu pages-menu--mobile ${isMobileMenuOpen ? "pages-menu--mobile-visible" : ""}`; 12 | }; 13 | 14 | return ( 15 | 32 | ); 33 | }; 34 | 35 | HomepageMenu.propTypes = { 36 | isMobileMenuOpen: PropTypes.bool.isRequired, 37 | isDesktopMenu: PropTypes.bool.isRequired, 38 | }; 39 | 40 | export default HomepageMenu; 41 | -------------------------------------------------------------------------------- /client/app/components/authors/header/menu/index.js: -------------------------------------------------------------------------------- 1 | import HomepageMenu from "./HomepageMenu"; 2 | import AuthorMenu from "./AuthorMenu"; 3 | 4 | export { HomepageMenu, AuthorMenu }; 5 | -------------------------------------------------------------------------------- /client/app/components/authors/settings/Appearance.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/standardnotes/listed/5750e416b82060a6058631a2d7fdfc301c3107f6/client/app/components/authors/settings/Appearance.scss -------------------------------------------------------------------------------- /client/app/components/authors/settings/ConfirmationModal.scss: -------------------------------------------------------------------------------- 1 | .confirmation-modal { 2 | &__overlay { 3 | position: fixed; 4 | background-color: var(--color-contrast-opacity-86); 5 | width: 100%; 6 | height: 100%; 7 | top: 0; 8 | left: 0; 9 | display: flex; 10 | flex-direction: column; 11 | justify-content: space-around; 12 | z-index: 1; 13 | } 14 | 15 | &__modal { 16 | margin: 0 $spacing-36; 17 | 18 | @include Desktop() { 19 | margin: auto; 20 | } 21 | } 22 | 23 | &__buttons { 24 | margin-top: $spacing-24; 25 | display: flex; 26 | justify-content: flex-end; 27 | 28 | .button--primary { 29 | margin-left: $spacing-12; 30 | background-image: linear-gradient(to top, var(--color-contrast) 50%, $color-error 50%); 31 | } 32 | 33 | .button--no-fill { 34 | @include MenuHover(var(--color-contrast)); 35 | } 36 | 37 | .button--secondary-disabled { 38 | color: var(--color-contrast-opacity-36); 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /client/app/components/authors/settings/CredentialForm.scss: -------------------------------------------------------------------------------- 1 | .credential-form .form-row { 2 | align-items: flex-end; 3 | } 4 | -------------------------------------------------------------------------------- /client/app/components/authors/settings/CustomDomain.scss: -------------------------------------------------------------------------------- 1 | .custom-domain { 2 | .form-row { 3 | align-items: flex-end; 4 | } 5 | 6 | .form-row--error .form-section { 7 | padding-top: 0px; 8 | } 9 | 10 | .callout { 11 | margin-top: $spacing-24; 12 | width: 100%; 13 | } 14 | 15 | .hover-container { 16 | display: flex; 17 | align-items: center; 18 | min-height: 30px; 19 | } 20 | 21 | .custom-domain__info { 22 | margin-top: $spacing-8; 23 | } 24 | 25 | &__details { 26 | display: flex; 27 | flex-wrap: wrap; 28 | align-items: center; 29 | flex-grow: 1; 30 | } 31 | } 32 | 33 | .custom-domain__linked-icon { 34 | height: 20px; 35 | width: 20px; 36 | margin-right: $spacing-8; 37 | } 38 | -------------------------------------------------------------------------------- /client/app/components/authors/settings/DeleteBlog.scss: -------------------------------------------------------------------------------- 1 | .delete-blog { 2 | .delete-blog__info { 3 | margin-top: $spacing-8; 4 | } 5 | 6 | .delete-blog__instructions { 7 | font-weight: bold; 8 | margin-top: $spacing-24; 9 | } 10 | 11 | .delete-blog__button-container { 12 | display: block; 13 | } 14 | 15 | &__button { 16 | margin-top: $spacing-12; 17 | width: 100%; 18 | 19 | &.button--primary { 20 | background-image: linear-gradient(to top, var(--color-contrast) 50%, $color-error 50%); 21 | } 22 | 23 | @include Desktop() { 24 | width: auto; 25 | } 26 | } 27 | 28 | &__form .checkbox__container--checked { 29 | color: $color-error; 30 | 31 | .checkbox__icon-container svg path { 32 | fill: $color-error; 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /client/app/components/authors/settings/GuestbookEntries.scss: -------------------------------------------------------------------------------- 1 | .guestbook-entries { 2 | padding: $spacing-16 0 0; 3 | margin: 0; 4 | list-style-type: none; 5 | } 6 | 7 | .guestbook-entries__item { 8 | padding: $spacing-16 0; 9 | border-top: var(--border); 10 | display: flex; 11 | align-items: center; 12 | 13 | &:last-child { 14 | border-bottom: var(--border); 15 | } 16 | 17 | .button--make-public { 18 | display: none; 19 | 20 | @include Desktop() { 21 | display: block; 22 | margin-right: $spacing-12; 23 | } 24 | } 25 | } 26 | 27 | .guestbook-entries__entry { 28 | flex-grow: 1; 29 | 30 | .entry__details { 31 | color: var(--color-contrast-opacity-86); 32 | font-weight: normal; 33 | display: inline-flex; 34 | align-items: center; 35 | margin-top: $spacing-6; 36 | 37 | &--private { 38 | display: block; 39 | } 40 | 41 | .entry-details__icon { 42 | width: 14px; 43 | height: 14px; 44 | margin-right: $spacing-6; 45 | 46 | path { 47 | fill: var(--color-contrast-opacity-86); 48 | } 49 | } 50 | } 51 | 52 | .entry-details__item { 53 | display: inline-flex; 54 | align-items: center; 55 | 56 | &:not(:last-child)::after { 57 | content: "·"; 58 | margin: 0 $spacing-6; 59 | } 60 | } 61 | } 62 | 63 | .guestbook-entries__action--make-public { 64 | @include Desktop() { 65 | display: none; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /client/app/components/authors/settings/MyPosts.scss: -------------------------------------------------------------------------------- 1 | .my-posts { 2 | padding: $spacing-16 0 0; 3 | margin: 0; 4 | list-style-type: none; 5 | } 6 | 7 | .my-posts__item { 8 | padding: $spacing-16 0; 9 | border-top: var(--border); 10 | display: flex; 11 | align-items: center; 12 | 13 | &:last-child { 14 | border-bottom: var(--border);; 15 | } 16 | 17 | a.my-posts__post { 18 | color: var(--color-contrast); 19 | flex-grow: 1; 20 | 21 | &::before { 22 | background-image: none; 23 | } 24 | 25 | &:hover::before { 26 | background-image: none; 27 | } 28 | 29 | .post__details { 30 | color: var(--color-contrast-opacity-86); 31 | font-weight: normal; 32 | display: inline-flex; 33 | align-items: center; 34 | margin-top: $spacing-6; 35 | 36 | .post-details__icon { 37 | width: 14px; 38 | height: 14px; 39 | margin-right: $spacing-6; 40 | 41 | path { 42 | fill: var(--color-contrast-opacity-86); 43 | } 44 | } 45 | } 46 | 47 | .post-details__item { 48 | display: inline-flex; 49 | align-items: center; 50 | 51 | &:not(:last-child)::after { 52 | content: "·"; 53 | margin: 0 $spacing-6; 54 | } 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /client/app/components/authors/settings/PaymentDetails.scss: -------------------------------------------------------------------------------- 1 | .payment-details { 2 | .payment-details__info { 3 | margin-top: $spacing-8; 4 | } 5 | 6 | .callout { 7 | margin-top: $spacing-12; 8 | width: 100%; 9 | 10 | &:first-child { 11 | margin-top: $spacing-24; 12 | } 13 | } 14 | 15 | .payment-details__credential { 16 | color: var(--color-contrast-opacity-86); 17 | font-weight: normal; 18 | display: flex; 19 | align-items: center; 20 | min-height: 30px; 21 | } 22 | 23 | .credential__details { 24 | display: flex; 25 | flex-wrap: wrap; 26 | align-items: center; 27 | flex-grow: 1; 28 | 29 | svg path { 30 | fill: var(--color-contrast); 31 | } 32 | } 33 | 34 | .credential__key { 35 | &:not(:last-child)::after { 36 | content: "·"; 37 | margin: 0 $spacing-6; 38 | } 39 | } 40 | 41 | .credential__icon { 42 | height: 20px; 43 | width: 20px; 44 | margin-right: $spacing-8; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /client/app/components/authors/settings/index.js: -------------------------------------------------------------------------------- 1 | import General from "./General"; 2 | import MyPosts from "./MyPosts"; 3 | import GuestbookEntries from "./GuestbookEntries"; 4 | import CustomDomain from "./CustomDomain"; 5 | import PaymentDetails from "./PaymentDetails"; 6 | import DeleteBlog from "./DeleteBlog"; 7 | import Appearance from "./Appearance"; 8 | 9 | export { 10 | General, 11 | MyPosts, 12 | GuestbookEntries, 13 | CustomDomain, 14 | PaymentDetails, 15 | DeleteBlog, 16 | Appearance, 17 | }; 18 | -------------------------------------------------------------------------------- /client/app/components/authors_mailer/DomainApproved.jsx: -------------------------------------------------------------------------------- 1 | import PropTypes from "prop-types"; 2 | import React from "react"; 3 | 4 | const DomainApproved = ({ url }) => ( 5 |
6 |

7 | Congratulations! 8 | {" "} 9 | 🎉 10 |

11 |

12 | Your custom domain has been approved, and your blog is now live at 13 | {" "} 14 | {url} 15 | . 16 |

17 |

18 | Any questions? Please feel free to reply directly to this email. 19 |

20 |
21 | ); 22 | 23 | DomainApproved.propTypes = { 24 | url: PropTypes.string.isRequired, 25 | }; 26 | 27 | export default DomainApproved; 28 | -------------------------------------------------------------------------------- /client/app/components/authors_mailer/DomainInvalid.jsx: -------------------------------------------------------------------------------- 1 | import PropTypes from "prop-types"; 2 | import React from "react"; 3 | 4 | const DomainInvalid = ({ customDomainIP }) => ( 5 |
6 |

7 | Hi there, your Listed domain settings were not configured properly. 8 | Please make sure you have an A record pointing to 9 | 10 | {" "} 11 | {customDomainIP} 12 | 13 | , 14 | then 15 | submit your domain request again via your author settings 16 | . 17 | You'll know you have it configured correctly if when you visit your custom domain, 18 | {" "} 19 | it shows an 20 | Invalid Certificate error 21 | . 22 |

23 |

24 | Any questions? Please feel free to reply directly to this email. 25 |

26 |
27 | ); 28 | 29 | DomainInvalid.propTypes = { 30 | customDomainIP: PropTypes.string.isRequired, 31 | }; 32 | 33 | export default DomainInvalid; 34 | -------------------------------------------------------------------------------- /client/app/components/authors_mailer/Featured.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const Featured = () => ( 4 |
5 |

Congratulations!

6 |

7 | Your excellent track record of high quality writing on Listed has earned you 8 | {" "} 9 | a spot as a featured writer. 10 | This means you'll have a permanent spot on our 11 | {" "} 12 | homepage 13 | {" "} 14 | (so long as you remain active), as well as a distinguished star next to your name. 15 |

16 |

17 | Keep up the good work! 18 |

19 |
20 | ); 21 | 22 | export default Featured; 23 | -------------------------------------------------------------------------------- /client/app/components/authors_mailer/NewReaction.jsx: -------------------------------------------------------------------------------- 1 | import PropTypes from "prop-types"; 2 | import React from "react"; 3 | 4 | const NewReaction = ({ author, post, reaction }) => ( 5 |
6 |

Hey {author.display_name},

7 |

8 | Someone reacted {reaction.reaction_string} to your post "{post.title}"! 9 |

10 |

Keep up the good work!

11 |
12 | ); 13 | 14 | NewReaction.propTypes = { 15 | reaction: PropTypes.shape({ 16 | reaction_string: PropTypes.string.isRequired, 17 | }).isRequired, 18 | author: PropTypes.shape({ 19 | display_name: PropTypes.string.isRequired, 20 | }).isRequired, 21 | post: PropTypes.shape({ 22 | title: PropTypes.string.isRequired, 23 | url: PropTypes.string.isRequired, 24 | }).isRequired, 25 | }; 26 | 27 | export default NewReaction; 28 | -------------------------------------------------------------------------------- /client/app/components/authors_mailer/VerifyEmail.jsx: -------------------------------------------------------------------------------- 1 | import PropTypes from "prop-types"; 2 | import React from "react"; 3 | 4 | const VerifyEmail = ({ displayName, verificationLink }) => ( 5 |
6 |

7 | Hey 8 | {" "} 9 | {displayName} 10 | , 11 |

12 |

13 | Please verify your email address by clicking the link below. 14 |

15 | {verificationLink} 16 |
17 | ); 18 | 19 | VerifyEmail.propTypes = { 20 | displayName: PropTypes.string.isRequired, 21 | verificationLink: PropTypes.string.isRequired, 22 | }; 23 | 24 | export default VerifyEmail; 25 | -------------------------------------------------------------------------------- /client/app/components/guestbook_entries/Guestbook.scss: -------------------------------------------------------------------------------- 1 | .guestbook__container .callout { 2 | margin: $spacing-36 0; 3 | } 4 | 5 | .guestbook__new-entry-button-container { 6 | display: flex; 7 | flex-direction: column-reverse; 8 | margin: $spacing-36 0; 9 | 10 | .p1 { 11 | margin-bottom: $spacing-16; 12 | } 13 | 14 | @include Desktop() { 15 | flex-direction: row; 16 | align-items: center; 17 | margin-top: $spacing-24; 18 | 19 | .button { 20 | margin-right: $spacing-16; 21 | } 22 | 23 | .p1 { 24 | margin-bottom: 0; 25 | } 26 | } 27 | } 28 | 29 | .guestbook__entry { 30 | border-left: 3px solid var(--guestbook-border-left-color); 31 | border-radius: $border-radius-4; 32 | padding-left: $spacing-16; 33 | 34 | &:not(:last-child) { 35 | margin-bottom: $spacing-36; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /client/app/components/guestbook_entries/New.scss: -------------------------------------------------------------------------------- 1 | .guestbook-entry-form__container { 2 | display: inline-block; 3 | margin: $spacing-24 0 $spacing-36; 4 | } 5 | 6 | .guestbook-entry-form__button-container { 7 | margin-top: $spacing-32; 8 | 9 | .button { 10 | width: 100%; 11 | margin-bottom: $spacing-16; 12 | } 13 | 14 | @include Desktop() { 15 | display: flex; 16 | align-items: center; 17 | 18 | .button { 19 | width: auto; 20 | margin: 0 $spacing-16 0 0; 21 | } 22 | } 23 | } 24 | 25 | .guestbook-entry-form__error-message { 26 | color: $color-error; 27 | margin-top: $spacing-6; 28 | } 29 | -------------------------------------------------------------------------------- /client/app/components/help/Help.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const Help = () => ( 4 |

5 | Questions? Get in touch any time: 6 | {" "} 7 | listed@standardnotes.org 8 | . 9 |

10 | ); 11 | 12 | export default Help; 13 | -------------------------------------------------------------------------------- /client/app/components/posts/Posts.jsx: -------------------------------------------------------------------------------- 1 | import PropTypes from "prop-types"; 2 | import React from "react"; 3 | 4 | const Posts = ({ days }) => ( 5 |
6 |
7 | {days.map((day) => ( 8 |
9 |

{day.day}

10 |
11 | {day.posts.map((post) => ( 12 |
13 | {post.title} 14 |
15 | ))} 16 |
17 |
18 | 19 | ))} 20 |
21 |
22 | ); 23 | 24 | Posts.propTypes = { 25 | days: PropTypes.arrayOf( 26 | PropTypes.shape({ 27 | day: PropTypes.string.isRequired, 28 | posts: PropTypes.arrayOf( 29 | PropTypes.shape({ 30 | id: PropTypes.number.isRequired, 31 | title: PropTypes.string.isRequired, 32 | tokenized_url: PropTypes.string.isRequired, 33 | }), 34 | ).isRequired, 35 | }), 36 | ).isRequired, 37 | }; 38 | 39 | export default Posts; 40 | -------------------------------------------------------------------------------- /client/app/components/reactions/New.scss: -------------------------------------------------------------------------------- 1 | .validate-page { 2 | .h1 { 3 | margin-bottom: $spacing-8; 4 | } 5 | 6 | #captcha-form { 7 | .button { 8 | width: 100%; 9 | margin-top: $spacing-16; 10 | 11 | @include Desktop() { 12 | width: auto; 13 | } 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /client/app/components/reactions/ReactionSuccess.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const ReactionSuccess = () => ( 4 |
5 |

6 | Your reaction has been sent to the author! Thanks for supporting their work and encouraging them to make 7 | more of it. 8 |

9 |
10 | ); 11 | 12 | export default ReactionSuccess; 13 | -------------------------------------------------------------------------------- /client/app/components/shared/Checkbox.jsx: -------------------------------------------------------------------------------- 1 | import PropTypes from "prop-types"; 2 | import React from "react"; 3 | import SVG from "react-inlinesvg"; 4 | import { IcCheckboxChecked, IcCheckboxEmpty } from "../../assets/icons"; 5 | import "./Checkbox.scss"; 6 | 7 | const Checkbox = ({ 8 | id, onClick, checked, label, 9 | }) => ( 10 |
11 | onClick(e.target.checked)} 17 | /> 18 |
19 | onClick(!checked)} 22 | /> 23 |
24 | 27 |
28 | ); 29 | 30 | Checkbox.propTypes = { 31 | checked: PropTypes.bool.isRequired, 32 | id: PropTypes.string.isRequired, 33 | label: PropTypes.string.isRequired, 34 | onClick: PropTypes.func.isRequired, 35 | }; 36 | 37 | export default Checkbox; 38 | -------------------------------------------------------------------------------- /client/app/components/shared/Checkbox.scss: -------------------------------------------------------------------------------- 1 | .form-section.checkbox__container { 2 | flex-direction: row; 3 | } 4 | 5 | .checkbox { 6 | display: none; 7 | } 8 | 9 | .checkbox__icon-container { 10 | margin-right: $spacing-12; 11 | height: 18px; 12 | width: 18px; 13 | 14 | svg path { 15 | fill: var(--color-primary); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /client/app/components/shared/Dropdown.scss: -------------------------------------------------------------------------------- 1 | .dropdown { 2 | position: relative; 3 | 4 | .button { 5 | margin: 0; 6 | padding: 0; 7 | font-weight: normal; 8 | } 9 | } 10 | 11 | .dropdown__list.card { 12 | z-index: 1000; 13 | opacity: 0; 14 | visibility: hidden; 15 | position: absolute; 16 | right: 0; 17 | margin: $spacing-2 0; 18 | padding: $spacing-8 0; 19 | list-style: none; 20 | width: 170px; 21 | background-image: none; 22 | transition: all 0.3s ease-in-out; 23 | 24 | &.dropdown__list--open { 25 | opacity: 1; 26 | visibility: visible; 27 | box-shadow: $spacing-8 $spacing-8 0 var(--color-primary); 28 | } 29 | } 30 | 31 | .dropdown__option { 32 | padding: $spacing-12; 33 | 34 | &:not(:last-child) { 35 | margin-bottom: $spacing-4; 36 | } 37 | 38 | &:hover { 39 | background-color: var(--color-primary-opacity-8); 40 | } 41 | 42 | .option__button { 43 | width: 100%; 44 | text-align: left; 45 | display: flex; 46 | align-items: center; 47 | } 48 | 49 | .option__icon { 50 | margin-right: $spacing-8; 51 | width: 20px; 52 | height: 20px; 53 | 54 | path { 55 | fill: var(--color-contrast-opacity-86); 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /client/app/components/shared/ErrorToast.scss: -------------------------------------------------------------------------------- 1 | .error-toast { 2 | &__container { 3 | position: fixed; 4 | top: -$spacing-86; 5 | left: 0; 6 | width: 100%; 7 | display: flex; 8 | justify-content: space-around; 9 | transition: top 0.3s ease-in-out; 10 | } 11 | 12 | &__toast { 13 | margin: auto; 14 | background-color: $color-error-light; 15 | padding: $spacing-16; 16 | display: flex; 17 | justify-content: space-between; 18 | align-items: center; 19 | border-radius: $border-radius-4; 20 | border: var(--border); 21 | } 22 | 23 | .error-toast__message { 24 | margin-right: $spacing-16; 25 | } 26 | 27 | .error-toast__dismiss-button { 28 | padding: 0; 29 | line-height: 0; 30 | 31 | path { 32 | fill: var(--color-contrast); 33 | } 34 | } 35 | 36 | &--visible .error-toast__container { 37 | top: $spacing-8; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /client/app/components/shared/Footer.jsx: -------------------------------------------------------------------------------- 1 | import PropTypes from "prop-types"; 2 | import React from "react"; 3 | import "./Footer.scss"; 4 | 5 | const Footer = ({ blogPage, author, privatePost }) => ( 6 | 20 | ); 21 | 22 | Footer.propTypes = { 23 | author: PropTypes.shape({ 24 | title: PropTypes.string.isRequired, 25 | }), 26 | blogPage: PropTypes.bool, 27 | privatePost: PropTypes.bool, 28 | }; 29 | 30 | Footer.defaultProps = { 31 | author: null, 32 | blogPage: false, 33 | privatePost: false, 34 | }; 35 | 36 | export default (props) =>