From 8db369b2c2d7b18c7520e920a741593a1719633d Mon Sep 17 00:00:00 2001 From: 5andu Date: Sun, 31 Dec 2023 00:08:57 +0100 Subject: [PATCH 01/13] added image_processing gem --- Gemfile | 1 + Gemfile.lock | 9 +++++++++ 2 files changed, 10 insertions(+) diff --git a/Gemfile b/Gemfile index 8858f231..99e6db21 100644 --- a/Gemfile +++ b/Gemfile @@ -25,6 +25,7 @@ gem 'stimulus-rails' gem 'stripe' gem 'tailwindcss-rails' gem 'turbo-rails' +gem "image_processing", ">= 1.2" # Windows does not include zoneinfo files, so bundle the tzinfo-data gem gem 'tzinfo-data', platforms: %i[mingw mswin x64_mingw jruby] diff --git a/Gemfile.lock b/Gemfile.lock index 72289d9a..de0b1984 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -138,6 +138,7 @@ GEM railties (>= 5.0.0) faker (3.2.1) i18n (>= 1.8.11, < 2) + ffi (1.16.3) globalid (1.2.1) activesupport (>= 6.1) groupdate (6.4.0) @@ -147,6 +148,9 @@ GEM multi_xml (>= 0.5.2) i18n (1.14.1) concurrent-ruby (~> 1.0) + image_processing (1.12.2) + mini_magick (>= 4.9.5, < 5) + ruby-vips (>= 2.0.17, < 3) importmap-rails (1.2.3) actionpack (>= 6.0.0) activesupport (>= 6.0.0) @@ -177,6 +181,7 @@ GEM metamagic (3.1.7) rails (>= 3.0.0) methodz (0.1.8) + mini_magick (4.12.0) mini_mime (1.1.5) minitest (5.20.0) msgpack (1.7.2) @@ -312,6 +317,8 @@ GEM rubocop-capybara (~> 2.17) rubocop-factory_bot (~> 2.22) ruby-progressbar (1.13.0) + ruby-vips (2.2.0) + ffi (~> 1.12) ruby2_keywords (0.0.5) rubyzip (2.3.2) selenium-webdriver (4.15.0) @@ -366,6 +373,7 @@ GEM PLATFORMS arm64-darwin-22 + arm64-darwin-23 x86_64-darwin-21 x86_64-linux @@ -381,6 +389,7 @@ DEPENDENCIES figaro! groupdate httparty + image_processing (>= 1.2) importmap-rails jbuilder letter_opener From 827276ff179315d7fd46402b784af022b3fc8564 Mon Sep 17 00:00:00 2001 From: 5andu Date: Sun, 31 Dec 2023 00:10:38 +0100 Subject: [PATCH 02/13] install trix, action text and active storage --- app/assets/config/manifest.js | 1 + app/assets/stylesheets/actiontext.css | 235 +++++++ .../stylesheets/application.tailwind.css | 7 +- app/assets/stylesheets/trix.css | 607 ++++++++++++++++++ app/javascript/application.js | 55 +- app/views/active_storage/blobs/_blob.html.erb | 15 + config/importmap.rb | 4 + .../20231230205139_create_blog_posts.rb | 12 + ...te_active_storage_tables.active_storage.rb | 57 ++ ...2_create_action_text_tables.action_text.rb | 26 + db/schema.rb | 51 +- spec/rails_helper.rb | 18 + 12 files changed, 1080 insertions(+), 8 deletions(-) create mode 100644 app/assets/stylesheets/actiontext.css create mode 100644 app/assets/stylesheets/trix.css create mode 100644 app/views/active_storage/blobs/_blob.html.erb create mode 100644 db/migrate/20231230205139_create_blog_posts.rb create mode 100644 db/migrate/20231230212421_create_active_storage_tables.active_storage.rb create mode 100644 db/migrate/20231230212422_create_action_text_tables.action_text.rb diff --git a/app/assets/config/manifest.js b/app/assets/config/manifest.js index ef2a396b..746fe66d 100644 --- a/app/assets/config/manifest.js +++ b/app/assets/config/manifest.js @@ -4,3 +4,4 @@ //= link_tree ../../javascript .js //= link_tree ../../../vendor/javascript .js //= link_tree ../builds +//= link actiontext.css diff --git a/app/assets/stylesheets/actiontext.css b/app/assets/stylesheets/actiontext.css new file mode 100644 index 00000000..a108fad3 --- /dev/null +++ b/app/assets/stylesheets/actiontext.css @@ -0,0 +1,235 @@ +/* + * Provides a drop-in pointer for the default Trix stylesheet that will format the toolbar and + * the trix-editor content (whether displayed or under editing). Feel free to incorporate this + * inclusion directly in any other asset bundle and remove this file. + * + *= require trix +*/ + +/* + * We need to override trix.css’s image gallery styles to accommodate the + * element we wrap around attachments. Otherwise, + * images in galleries will be squished by the max-width: 33%; rule. +*/ + +.link_to_embed { + white-space: normal; + margin-top: 1rem; + margin-left: 1rem; + + [data-behavior="embed_url"] { + display: inline-block; + margin-left: 0.75rem; + } +} + +trix-editor:empty:not(:focus)::before { + color: #9ca3af; +} + +.trix-content img { + display: inline-block; + max-width: 100%; + height: auto; + border-radius: 4px; +} + +.trix-content a { + color: #1b64f3; + text-decoration: underline; +} + +.trix-content a:hover { + color: #1c4ed8; +} + +.trix-content { + .attachment-gallery { + > action-text-attachment, + > .attachment { + flex: 1 0 33%; + padding: 0 0.5em; + max-width: 33%; + } + + &.attachment-gallery--2, + &.attachment-gallery--4 { + > action-text-attachment, + > .attachment { + flex-basis: 50%; + max-width: 50%; + } + } + } + + .embed { + display: inline-block; + line-height: 1; + margin: 1em 0 !important; + padding: 0 !important; + width: 100%; + } + + iframe, + twitter-widget { + display: inline-block !important; + } +} + +/* Trix attachment formatting */ +.attachment--preview { + margin: 0.6em 0; + text-align: center; + width: 100%; +} + +/* Tribute styles */ + +.tribute-container ul { + list-style-type: disc; + margin: 0; + padding: 0; +} + +.tribute-container { + border-radius: 4px; + border: 1px solid rgba(0, 0, 0, 0.1); + box-shadow: 0 0 4px rgba(0, 0, 0, 0.1), 0 5px 20px rgba(0, 0, 0, 0.05); + + ul { + list-style-type: disc; + margin: 0; + padding: 0; + } + + li { + background: #fff; + padding: 0.2em 1em; + min-width: 15em; + max-width: 100%; + } + + .highlight { + background: #f2f2f2; + color: #fff; + + span { + font-weight: bold; + } + } +} + +/* Tweet embeds */ +blockquote.twitter-tweet { + display: inline-block; + font-family: "Helvetica Neue", Roboto, "Segoe UI", Calibri, sans-serif; + font-size: 12px; + font-weight: bold; + line-height: 16px; + border-color: #eee #ddd #bbb; + border-radius: 5px; + border-style: solid; + border-width: 1px; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.15); + margin: 10px 5px; + padding: 0 16px 16px; + max-width: 468px; +} + +blockquote.twitter-tweet p { + font-size: 16px; + font-weight: normal; + line-height: 20px; +} + +blockquote.twitter-tweet a { + color: inherit; + font-weight: normal; + text-decoration: none; + outline: 0 none; +} + +blockquote.twitter-tweet a:hover, +blockquote.twitter-tweet a:focus { + text-decoration: underline; +} + +blockquote.twitter-tweet { + position: relative; + background: white; + padding: 72px 20px 28px !important; + box-shadow: none; + border: 1px solid #e1e8ed; + border-radius: 4px; + margin: 0; + font-style: normal; + text-align: left; + width: 500px; + max-width: 100%; +} + +@media (max-width: 369px) { + blockquote.twitter-tweet { + padding: 60px 17.5px 21.5px !important; + } +} + +blockquote.twitter-tweet:before { + content: "Follow"; + position: absolute; + top: 20px; + right: 20px; + padding: 5.5px 12px 6.5px 33px; + background: url("data:image/svg+xml,%3Csvg height='24' viewBox='0 0 24 24' width='24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='m18.89 7.012c.808-.496 1.343-1.173 1.605-2.034-.786.417-1.569.703-2.351.861-.703-.756-1.593-1.14-2.66-1.14-1.043 0-1.924.366-2.643 1.078-.715.717-1.076 1.588-1.076 2.605 0 .309.039.585.117.819-3.076-.105-5.622-1.381-7.628-3.837-.34.601-.51 1.213-.51 1.846 0 1.301.549 2.332 1.645 3.089-.625-.053-1.176-.211-1.645-.47 0 .929.273 1.705.82 2.388.549.676 1.254 1.107 2.115 1.291-.312.08-.641.118-.979.118-.312 0-.533-.026-.664-.083.23.757.664 1.371 1.291 1.841.625.472 1.344.721 2.152.743-1.332 1.045-2.855 1.562-4.578 1.562-.422 0-.721-.006-.902-.038 1.697 1.102 3.586 1.649 5.676 1.649 2.139 0 4.029-.542 5.674-1.626 1.645-1.078 2.859-2.408 3.639-3.974.784-1.564 1.172-3.192 1.172-4.892v-.468c.758-.57 1.371-1.212 1.84-1.921-.68.293-1.383.492-2.11.593z' fill='%23ccc'/%3E%3C/svg%3E") 9px center no-repeat; + background-size: 21px; + border: 1px solid #ccc; + border-radius: 4px; + color: #ccc; + font-size: 14px; +} + +@media (max-width: 369px) { + blockquote.twitter-tweet:before { + display: none; + } +} + +blockquote.twitter-tweet:after { + content: ""; + position: absolute; + top: 20px; + left: 20px; + width: 36px; + height: 36px; + background: #eee url("data:image/svg+xml,%3Csvg height='24' viewBox='0 0 24 24' width='24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='m12 12c2.21 0 4-1.795 4-4 0-2.21-1.79-4-4-4s-4 1.79-4 4c0 2.205 1.79 4 4 4zm0 2c-2.665 0-8 1.335-8 4v2h16v-2c0-2.665-5.335-4-8-4z' fill='%23444'/%3E%3C/svg%3E") center center no-repeat; + border-radius: 4px; +} + +@media (max-width: 369px) { + blockquote.twitter-tweet:after { + top: 17.5px; + left: 17.5px; + } +} + +blockquote.twitter-tweet p { + white-space: pre-wrap; + margin: 0 0 28px; +} + +@media (max-width: 369px) { + blockquote.twitter-tweet p { + font-size: 14px; + margin-bottom: 16px; + } +} + +blockquote.twitter-tweet a { + border: 0; + box-shadow: none; + color: #2b7bb9; +} + +blockquote.twitter-tweet > a { + color: #888; +} diff --git a/app/assets/stylesheets/application.tailwind.css b/app/assets/stylesheets/application.tailwind.css index 99cba095..1e867d52 100644 --- a/app/assets/stylesheets/application.tailwind.css +++ b/app/assets/stylesheets/application.tailwind.css @@ -2,6 +2,9 @@ @tailwind components; @tailwind utilities; +@import "actiontext.css"; +@import "trix.css"; + .speedrail-tooltip { display: none; position: absolute; @@ -14,9 +17,9 @@ .toggle-checkbox:checked { @apply right-0 border-green-400; right: 0; - border-color: #68D391; + border-color: #68d391; } .toggle-checkbox:checked + .toggle-label { @apply bg-green-400; - background-color: #68D391; + background-color: #68d391; } diff --git a/app/assets/stylesheets/trix.css b/app/assets/stylesheets/trix.css new file mode 100644 index 00000000..8216ba8c --- /dev/null +++ b/app/assets/stylesheets/trix.css @@ -0,0 +1,607 @@ +@charset "UTF-8"; +trix-editor { + border-radius: 3px; + margin-top: 4px; + padding: 0.2em 0.6em; + min-height: 5em; + outline: none; + border-top-left-radius: 0; + border-top-right-radius: 0; + border-color: #333; + font-size: 0.875rem; + line-height: 1.25rem; +} + +/* Hide attachement file, link button, code snippet, increase level, decrease level, and title */ + +.trix-button--icon-heading-1 { + display: none !important; +} + +/* Change text placeholder color to green */ +trix-editor:empty:not(:focus)::before { + color: #6c7280; +} + +trix-editor a { + color: #1b64f3; + text-decoration: underline; +} + +trix-editor a:hover { + color: #1c4ed8; +} + +trix-editor h1 { + font-size: 2em; + line-height: 1.6; + font-weight: 700; +} + +trix-editor h2 { + font-size: 1.8em; + line-height: 1.6; + font-weight: 700; +} +trix-editor h3 { + font-size: 1.5em; + line-height: 1.6; + font-weight: 700; +} + +trix-editor blockquote { + border: 0 solid #ccc; + border-left-width: 0.3em; + margin-left: 0.3em; + padding-left: 0.6em; +} +trix-editor [dir="rtl"] blockquote, +trix-editor blockquote[dir="rtl"] { + border-width: 0; + border-right-width: 0.3em; + margin-right: 0.3em; + padding-right: 0.6em; +} +trix-editor ol { + list-style-type: decimal; +} + +trix-editor ul { + list-style-type: disc; +} +trix-editor li { + margin-left: 1.25em; +} + +trix-editor [dir="rtl"] li { + margin-right: 1em; +} +trix-editor pre { + display: inline-block; + width: 100%; + vertical-align: top; + font-family: monospace; + font-size: 0.9em; + padding: 0.5em; + white-space: pre; + background-color: #eee; + border-radius: 4px; + overflow-x: auto; + text-wrap: wrap; +} +trix-editor img { + max-width: 100%; + height: auto; +} + +trix-toolbar { + border-bottom: none; + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; + padding: 0em 0.2em; +} +trix-toolbar * { + box-sizing: border-box; +} +trix-toolbar .trix-button-row { + display: flex; + flex-wrap: nowrap; + justify-content: space-between; + overflow-x: auto; +} +trix-toolbar .trix-button-group { + display: flex; +} +@media (max-device-width: 768px) { + trix-toolbar .trix-button-group:not(:first-child) { + margin-left: 0; + } +} +trix-toolbar .trix-button-group-spacer { + flex-grow: 1; +} +@media (max-device-width: 768px) { + trix-toolbar .trix-button-group-spacer { + display: none; + } +} +trix-toolbar .trix-button { + position: relative; + float: left; + color: rgba(0, 0, 0, 0.6); + font-size: 0.75em; + font-weight: 600; + white-space: nowrap; + padding: 0 0.3em; + margin: 0; + outline: none; + border: none; + border-radius: 4px; + background: transparent; + margin-right: 4px; +} +trix-toolbar .trix-button.trix-active { + background: #e6e5e5; + color: black; +} +trix-toolbar .trix-button:hover { + background: #f1f1f1; +} +trix-toolbar .trix-button:not(:disabled) { + cursor: pointer; +} +trix-toolbar .trix-button:disabled { + color: rgba(0, 0, 0, 0.125); +} +@media (max-device-width: 768px) { + trix-toolbar .trix-button { + letter-spacing: -0.01em; + padding: 0 0.3em; + } +} +trix-toolbar .trix-button--icon { + width: 2.2em; + height: 2em; + max-width: calc(0.8em + 4vw); + text-indent: -9999px; +} +@media (max-device-width: 768px) { + trix-toolbar .trix-button--icon { + height: 2em; + max-width: calc(0.8em + 3.5vw); + } +} +trix-toolbar .trix-button--icon::before { + display: inline-block; + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + opacity: 0.6; + content: ""; + background-position: center; + background-repeat: no-repeat; +} +@media (max-device-width: 768px) { + trix-toolbar .trix-button--icon::before { + right: 6%; + left: 6%; + } +} +trix-toolbar .trix-button--icon.trix-active::before { + opacity: 1; +} +trix-toolbar .trix-button--icon:disabled::before { + opacity: 0.125; +} +trix-toolbar .trix-button--icon-attach::before { + background-image: url(data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2224%22%20height%3D%2224%22%3E%3Cpath%20d%3D%22M16.5%206v11.5a4%204%200%201%201-8%200V5a2.5%202.5%200%200%201%205%200v10.5a1%201%200%201%201-2%200V6H10v9.5a2.5%202.5%200%200%200%205%200V5a4%204%200%201%200-8%200v12.5a5.5%205.5%200%200%200%2011%200V6h-1.5z%22%2F%3E%3C%2Fsvg%3E); + top: 8%; + bottom: 4%; +} +trix-toolbar .trix-button--icon-bold::before { + background-image: url(data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2224%22%20height%3D%2224%22%3E%3Cpath%20d%3D%22M15.6%2011.8c1-.7%201.6-1.8%201.6-2.8a4%204%200%200%200-4-4H7v14h7c2.1%200%203.7-1.7%203.7-3.8%200-1.5-.8-2.8-2.1-3.4zM10%207.5h3a1.5%201.5%200%201%201%200%203h-3v-3zm3.5%209H10v-3h3.5a1.5%201.5%200%201%201%200%203z%22%2F%3E%3C%2Fsvg%3E); +} +trix-toolbar .trix-button--icon-italic::before { + background-image: url(data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2224%22%20height%3D%2224%22%3E%3Cpath%20d%3D%22M10%205v3h2.2l-3.4%208H6v3h8v-3h-2.2l3.4-8H18V5h-8z%22%2F%3E%3C%2Fsvg%3E); +} +trix-toolbar .trix-button--icon-link::before { + background-image: url(data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2224%22%20height%3D%2224%22%3E%3Cpath%20d%3D%22M9.88%2013.7a4.3%204.3%200%200%201%200-6.07l3.37-3.37a4.26%204.26%200%200%201%206.07%200%204.3%204.3%200%200%201%200%206.06l-1.96%201.72a.91.91%200%201%201-1.3-1.3l1.97-1.71a2.46%202.46%200%200%200-3.48-3.48l-3.38%203.37a2.46%202.46%200%200%200%200%203.48.91.91%200%201%201-1.3%201.3z%22%2F%3E%3Cpath%20d%3D%22M4.25%2019.46a4.3%204.3%200%200%201%200-6.07l1.93-1.9a.91.91%200%201%201%201.3%201.3l-1.93%201.9a2.46%202.46%200%200%200%203.48%203.48l3.37-3.38c.96-.96.96-2.52%200-3.48a.91.91%200%201%201%201.3-1.3%204.3%204.3%200%200%201%200%206.07l-3.38%203.38a4.26%204.26%200%200%201-6.07%200z%22%2F%3E%3C%2Fsvg%3E); +} +trix-toolbar .trix-button--icon-strike::before { + background-image: url(data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2224%22%20height%3D%2224%22%3E%3Cpath%20d%3D%22M12.73%2014l.28.14c.26.15.45.3.57.44.12.14.18.3.18.5%200%20.3-.15.56-.44.75-.3.2-.76.3-1.39.3A13.52%2013.52%200%200%201%207%2014.95v3.37a10.64%2010.64%200%200%200%204.84.88c1.26%200%202.35-.19%203.28-.56.93-.37%201.64-.9%202.14-1.57s.74-1.45.74-2.32c0-.26-.02-.51-.06-.75h-5.21zm-5.5-4c-.08-.34-.12-.7-.12-1.1%200-1.29.52-2.3%201.58-3.02%201.05-.72%202.5-1.08%204.34-1.08%201.62%200%203.28.34%204.97%201l-1.3%202.93c-1.47-.6-2.73-.9-3.8-.9-.55%200-.96.08-1.2.26-.26.17-.38.38-.38.64%200%20.27.16.52.48.74.17.12.53.3%201.05.53H7.23zM3%2013h18v-2H3v2z%22%2F%3E%3C%2Fsvg%3E); +} +trix-toolbar .trix-button--icon-quote::before { + background-image: url(data:image/svg+xml,%3Csvg%20version%3D%221%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2224%22%20height%3D%2224%22%3E%3Cpath%20d%3D%22M6%2017h3l2-4V7H5v6h3zm8%200h3l2-4V7h-6v6h3z%22%2F%3E%3C%2Fsvg%3E); +} +trix-toolbar .trix-button--icon-heading-1::before { + background-image: url(data:image/svg+xml,%3Csvg%20version%3D%221%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2224%22%20height%3D%2224%22%3E%3Cpath%20d%3D%22M12%209v3H9v7H6v-7H3V9h9zM8%204h14v3h-6v12h-3V7H8V4z%22%2F%3E%3C%2Fsvg%3E); +} +trix-toolbar .trix-button--icon-code::before { + background-image: url(data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2224%22%20height%3D%2224%22%3E%3Cpath%20d%3D%22M18.2%2012L15%2015.2l1.4%201.4L21%2012l-4.6-4.6L15%208.8l3.2%203.2zM5.8%2012L9%208.8%207.6%207.4%203%2012l4.6%204.6L9%2015.2%205.8%2012z%22%2F%3E%3C%2Fsvg%3E); +} +trix-toolbar .trix-button--icon-bullet-list::before { + background-image: url(data:image/svg+xml,%3Csvg%20version%3D%221%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2224%22%20height%3D%2224%22%3E%3Cpath%20d%3D%22M4%204a2%202%200%201%200%200%204%202%202%200%200%200%200-4zm0%206a2%202%200%201%200%200%204%202%202%200%200%200%200-4zm0%206a2%202%200%201%200%200%204%202%202%200%200%200%200-4zm4%203h14v-2H8v2zm0-6h14v-2H8v2zm0-8v2h14V5H8z%22%2F%3E%3C%2Fsvg%3E); +} +trix-toolbar .trix-button--icon-number-list::before { + background-image: url(data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2224%22%20height%3D%2224%22%3E%3Cpath%20d%3D%22M2%2017h2v.5H3v1h1v.5H2v1h3v-4H2v1zm1-9h1V4H2v1h1v3zm-1%203h1.8L2%2013.1v.9h3v-1H3.2L5%2010.9V10H2v1zm5-6v2h14V5H7zm0%2014h14v-2H7v2zm0-6h14v-2H7v2z%22%2F%3E%3C%2Fsvg%3E); +} +trix-toolbar .trix-button--icon-undo::before { + background-image: url(data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2224%22%20height%3D%2224%22%3E%3Cpath%20d%3D%22M12.5%208c-2.6%200-5%201-6.9%202.6L2%207v9h9l-3.6-3.6A8%208%200%200%201%2020%2016l2.4-.8a10.5%2010.5%200%200%200-10-7.2z%22%2F%3E%3C%2Fsvg%3E); +} +trix-toolbar .trix-button--icon-redo::before { + background-image: url(data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2224%22%20height%3D%2224%22%3E%3Cpath%20d%3D%22M18.4%2010.6a10.5%2010.5%200%200%200-16.9%204.6L4%2016a8%208%200%200%201%2012.7-3.6L13%2016h9V7l-3.6%203.6z%22%2F%3E%3C%2Fsvg%3E); +} + +trix-toolbar .trix-button--icon-redo { + margin-right: 0px; +} + +trix-toolbar .trix-button--icon-decrease-nesting-level::before { + background-image: url(data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2224%22%20height%3D%2224%22%3E%3Cpath%20d%3D%22M3%2019h19v-2H3v2zm7-6h12v-2H10v2zm-8.3-.3l2.8%202.9L6%2014.2%204%2012l2-2-1.4-1.5L1%2012l.7.7zM3%205v2h19V5H3z%22%2F%3E%3C%2Fsvg%3E); +} +trix-toolbar .trix-button--icon-increase-nesting-level::before { + background-image: url(data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2224%22%20height%3D%2224%22%3E%3Cpath%20d%3D%22M3%2019h19v-2H3v2zm7-6h12v-2H10v2zm-6.9-1L1%2014.2l1.4%201.4L6%2012l-.7-.7-2.8-2.8L1%209.9%203.1%2012zM3%205v2h19V5H3z%22%2F%3E%3C%2Fsvg%3E); +} +trix-toolbar .trix-dialogs { + position: relative; +} +trix-toolbar .trix-dialog { + position: absolute; + top: 0; + left: 0; + right: 0; + font-size: 0.75em; + padding: 15px 10px; + background: #fff; + box-shadow: 0 0.3em 1em #ccc; + border-top: 2px solid #888; + border-radius: 5px; + z-index: 5; +} +trix-toolbar .trix-input--dialog { + font-size: inherit; + font-weight: normal; + padding: 0.5em 0.8em; + margin: 0 10px 0 0; + border-radius: 3px; + border: 1px solid #bbb; + background-color: #fff; + box-shadow: none; + outline: none; + -webkit-appearance: none; + -moz-appearance: none; +} +trix-toolbar .trix-input--dialog.validate:invalid { + box-shadow: #f00 0px 0px 1.5px 1px; +} + +trix-toolbar .trix-dialog--link { + max-width: 600px; + border: none; +} +trix-toolbar .trix-dialog__link-fields { + display: flex; + align-items: baseline; +} +trix-toolbar .trix-dialog__link-fields .trix-input { + flex: 1; + width: 100%; +} +trix-toolbar .trix-dialog__link-fields .trix-button-group { + flex: 0 0 content; + margin: 0; + + .trix-button--dialog:last-of-type { + margin-left: 0.25rem; + } +} +@media (max-device-width: 768px) { + trix-toolbar .trix-dialog__link-fields { + display: block; + + .trix-button-group { + margin-top: 0.5rem; + } + } +} + +trix-editor [data-trix-mutable]:not(.attachment__caption-editor) { + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} + +trix-editor [data-trix-mutable]::-moz-selection, +trix-editor [data-trix-cursor-target]::-moz-selection, +trix-editor [data-trix-mutable] ::-moz-selection { + background: none; +} +trix-editor [data-trix-mutable]::selection, +trix-editor [data-trix-cursor-target]::selection, +trix-editor [data-trix-mutable] ::selection { + background: none; +} + +trix-editor [data-trix-mutable].attachment__caption-editor:focus::-moz-selection { + background: highlight; +} +trix-editor [data-trix-mutable].attachment__caption-editor:focus::selection { + background: highlight; +} + +trix-editor [data-trix-mutable].attachment.attachment--file { + box-shadow: 0 0 0 2px highlight; + border-color: transparent; +} +trix-editor [data-trix-mutable].attachment img { + box-shadow: 0 0 0 2px highlight; +} +trix-editor .attachment { + position: relative; +} +trix-editor .attachment:hover { + cursor: default; +} +trix-editor .attachment--preview .attachment__caption:hover { + cursor: text; +} +trix-editor .attachment__progress { + position: absolute; + z-index: 1; + height: 20px; + top: calc(50% - 10px); + left: 5%; + width: 90%; + opacity: 0.9; + transition: opacity 200ms ease-in; +} +trix-editor .attachment__progress[value="100"] { + opacity: 0; +} +trix-editor .attachment__caption-editor { + display: inline-block; + width: 100%; + margin: 0; + padding: 0; + font-size: inherit; + font-family: inherit; + line-height: inherit; + color: inherit; + text-align: center; + vertical-align: top; + border: none; + outline: none; + -webkit-appearance: none; + -moz-appearance: none; +} +trix-editor .attachment__toolbar { + position: absolute; + z-index: 1; + top: -0.9em; + left: 0; + width: 100%; + text-align: center; +} +trix-editor .trix-button-group { + display: inline-flex; +} +trix-editor .trix-button { + position: relative; + float: left; + color: #666; + white-space: nowrap; + font-size: 80%; + padding: 0 0.8em; + margin: 0; + outline: none; + border: none; + border-radius: 0; + background: transparent; +} +trix-editor .trix-button:not(:first-child) { + border-left: 1px solid #ccc; +} +trix-editor .trix-button.trix-active { + background: #e0e0e0; +} +trix-editor .trix-button:not(:disabled) { + cursor: pointer; +} +trix-editor .trix-button--remove { + text-indent: -9999px; + display: inline-block; + padding: 0; + outline: none; + width: 1.8em; + height: 1.8em; + line-height: 1.8em; + border-radius: 50%; + background-color: #fff; + border: 2px solid highlight; + box-shadow: 1px 1px 6px rgba(0, 0, 0, 0.25); +} +trix-editor .trix-button--remove::before { + display: inline-block; + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + opacity: 0.7; + content: ""; + background-image: url(data:image/svg+xml,%3Csvg%20height%3D%2224%22%20width%3D%2224%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Cpath%20d%3D%22M19%206.4L17.6%205%2012%2010.6%206.4%205%205%206.4l5.6%205.6L5%2017.6%206.4%2019l5.6-5.6%205.6%205.6%201.4-1.4-5.6-5.6z%22%2F%3E%3Cpath%20d%3D%22M0%200h24v24H0z%22%20fill%3D%22none%22%2F%3E%3C%2Fsvg%3E); + background-position: center; + background-repeat: no-repeat; + background-size: 90%; +} +trix-editor .trix-button--remove:hover { + border-color: #333; +} +trix-editor .trix-button--remove:hover::before { + opacity: 1; +} +trix-editor .attachment__metadata-container { + position: relative; +} +trix-editor .attachment__metadata { + position: absolute; + left: 50%; + top: 2em; + transform: translate(-50%, 0); + max-width: 90%; + padding: 0.1em 0.6em; + font-size: 0.8em; + color: #fff; + background-color: rgba(0, 0, 0, 0.7); + border-radius: 3px; +} +trix-editor .attachment__metadata .attachment__name { + display: inline-block; + max-width: 100%; + vertical-align: bottom; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +trix-editor .attachment__metadata .attachment__size { + margin-left: 0.2em; + white-space: nowrap; +} + +.trix-content { + line-height: 1.5; +} +.trix-content * { + box-sizing: border-box; + margin: 0; + padding: 0; + margin-bottom: 10px; + margin-top: 5px; +} +.trix-content h1 { + font-size: 2em; + line-height: 1.6; + font-weight: 700; +} +.trix-content h2 { + font-size: 1.5em; + line-height: 1.6; + margin-top: 1.2em; + margin-bottom: 0.6em; + font-weight: 700; +} +.trix-content h3 { + font-size: 1.3em; + line-height: 1.6; + margin-top: 1.2em; + margin-bottom: 0.6em; + font-weight: 700; +} + +@media screen and (max-width: 768px) { + .trix-content h2 { + font-size: 1.3em; + } + .trix-content h3 { + font-size: 1.1em; + } +} + +.trix-content blockquote { + border: 0 solid #ccc; + border-left-width: 0.3em; + margin-left: 0.3em; + padding-left: 0.6em; +} +.trix-content [dir="rtl"] blockquote, +.trix-content blockquote[dir="rtl"] { + border-width: 0; + border-right-width: 0.3em; + margin-right: 0.3em; + padding-right: 0.6em; +} +.trix-content ol { + list-style-type: decimal; +} + +.trix-content ul { + list-style-type: disc; +} +.trix-content li { + margin-left: 1em; +} + +.trix-content [dir="rtl"] li { + margin-right: 1em; +} +.trix-content pre { + display: inline-block; + width: 100%; + vertical-align: top; + font-family: monospace; + font-size: 0.9em; + padding: 0.5em; + white-space: pre; + background-color: #eee; + border-radius: 4px; + overflow-x: auto; + text-wrap: wrap; +} +.trix-content img { + max-width: 100%; + height: auto; +} +.trix-content .attachment { + display: inline-block; + position: relative; + max-width: 100%; +} +.trix-content .attachment a { + color: #1b64f3; + text-decoration: underline; +} +.trix-content .attachment a:hover, +.trix-content .attachment a:visited:hover { + color: inherit; +} +.trix-content .attachment__caption { + text-align: center; +} +.trix-content .attachment__caption .attachment__name + .attachment__size::before { + content: " · "; +} +.trix-content .attachment--preview { + width: 100%; + text-align: center; +} +.trix-content .attachment--preview .attachment__caption { + color: #666; + font-size: 0.9em; + line-height: 1.2; +} +.trix-content .attachment--file { + color: #333; + line-height: 1; + margin: 0 2px 2px 2px; + padding: 0.4em 1em; + border: 1px solid #bbb; + border-radius: 5px; +} +.trix-content .attachment-gallery { + display: flex; + flex-wrap: wrap; + position: relative; +} +.trix-content .attachment-gallery .attachment { + flex: 1 0 33%; + padding: 0 0.5em; + max-width: 33%; +} +.trix-content .attachment-gallery.attachment-gallery--2 .attachment, +.trix-content .attachment-gallery.attachment-gallery--4 .attachment { + flex-basis: 50%; + max-width: 50%; +} diff --git a/app/javascript/application.js b/app/javascript/application.js index 9cf6d7ba..09c30d39 100644 --- a/app/javascript/application.js +++ b/app/javascript/application.js @@ -1,6 +1,51 @@ // Configure your import map in config/importmap.rb. Read more: https://github.com/rails/importmap-rails -import "@hotwired/turbo-rails" -import "controllers" -import "channels" -import "chartkick" -import "Chart.bundle" +import "@hotwired/turbo-rails"; +import "controllers"; +import "channels"; +import "chartkick"; +import "Chart.bundle"; +import * as ActiveStorage from "@rails/activestorage"; +ActiveStorage.start(); +import "trix"; +import "@rails/actiontext"; + +Trix.config.blockAttributes.heading2 = { + tagName: "h2", + terminal: true, + breakOnReturn: true, + group: false, +}; + +addEventListener("trix-initialize", (event) => { + const { toolbarElement } = event.target; + const h1Button = toolbarElement.querySelector("[data-trix-attribute=heading1]"); + h1Button.insertAdjacentHTML( + "afterend", + ` + + ` + ); +}); + +Trix.config.blockAttributes.heading3 = { + tagName: "h3", + terminal: true, + breakOnReturn: true, + group: false, +}; + +addEventListener("trix-initialize", (event) => { + const { toolbarElement } = event.target; + const h2Button = toolbarElement.querySelector("[data-trix-attribute=heading2]"); + h2Button.insertAdjacentHTML( + "afterend", + ` + + ` + ); +}); + +import "trix" +import "@rails/actiontext" diff --git a/app/views/active_storage/blobs/_blob.html.erb b/app/views/active_storage/blobs/_blob.html.erb new file mode 100644 index 00000000..d309a8b4 --- /dev/null +++ b/app/views/active_storage/blobs/_blob.html.erb @@ -0,0 +1,15 @@ +
attachment--<%= blob.filename.extension %>"> + <% if blob.representable? %> + <%= image_tag blob.representation(resize_to_limit: local_assigns[:in_gallery] ? [ 800, 600 ] : [ 1024, 768 ]) %> + <% end %> + +
+ <% if caption = blob.try(:caption) %> + <%= caption %> + <% else %> + <%= blob.filename %> + <%= number_to_human_size blob.byte_size %> + <% end %> +
+
+ diff --git a/config/importmap.rb b/config/importmap.rb index 2ce14756..3c9c75df 100644 --- a/config/importmap.rb +++ b/config/importmap.rb @@ -9,3 +9,7 @@ pin_all_from "app/javascript/channels", under: "channels" pin "chartkick", to: "chartkick.js" pin "Chart.bundle", to: "Chart.bundle.js" +pin "actiontext", to: "actiontext.js" +pin "trix", to: "trix.js" +pin "@rails/activestorage", to: "activestorage.esm.js" +pin "@rails/actiontext", to: "actiontext.esm.js" diff --git a/db/migrate/20231230205139_create_blog_posts.rb b/db/migrate/20231230205139_create_blog_posts.rb new file mode 100644 index 00000000..e703dada --- /dev/null +++ b/db/migrate/20231230205139_create_blog_posts.rb @@ -0,0 +1,12 @@ +class CreateBlogPosts < ActiveRecord::Migration[7.1] + def change + create_table :blog_posts do |t| + t.string :title + t.string :slug + t.string :description + t.boolean :draft + + t.timestamps + end + end +end diff --git a/db/migrate/20231230212421_create_active_storage_tables.active_storage.rb b/db/migrate/20231230212421_create_active_storage_tables.active_storage.rb new file mode 100644 index 00000000..e4706aa2 --- /dev/null +++ b/db/migrate/20231230212421_create_active_storage_tables.active_storage.rb @@ -0,0 +1,57 @@ +# This migration comes from active_storage (originally 20170806125915) +class CreateActiveStorageTables < ActiveRecord::Migration[7.0] + def change + # Use Active Record's configured type for primary and foreign keys + primary_key_type, foreign_key_type = primary_and_foreign_key_types + + create_table :active_storage_blobs, id: primary_key_type do |t| + t.string :key, null: false + t.string :filename, null: false + t.string :content_type + t.text :metadata + t.string :service_name, null: false + t.bigint :byte_size, null: false + t.string :checksum + + if connection.supports_datetime_with_precision? + t.datetime :created_at, precision: 6, null: false + else + t.datetime :created_at, null: false + end + + t.index [ :key ], unique: true + end + + create_table :active_storage_attachments, id: primary_key_type do |t| + t.string :name, null: false + t.references :record, null: false, polymorphic: true, index: false, type: foreign_key_type + t.references :blob, null: false, type: foreign_key_type + + if connection.supports_datetime_with_precision? + t.datetime :created_at, precision: 6, null: false + else + t.datetime :created_at, null: false + end + + t.index [ :record_type, :record_id, :name, :blob_id ], name: :index_active_storage_attachments_uniqueness, unique: true + t.foreign_key :active_storage_blobs, column: :blob_id + end + + create_table :active_storage_variant_records, id: primary_key_type do |t| + t.belongs_to :blob, null: false, index: false, type: foreign_key_type + t.string :variation_digest, null: false + + t.index [ :blob_id, :variation_digest ], name: :index_active_storage_variant_records_uniqueness, unique: true + t.foreign_key :active_storage_blobs, column: :blob_id + end + end + + private + def primary_and_foreign_key_types + config = Rails.configuration.generators + setting = config.options[config.orm][:primary_key_type] + primary_key_type = setting || :primary_key + foreign_key_type = setting || :bigint + [primary_key_type, foreign_key_type] + end +end diff --git a/db/migrate/20231230212422_create_action_text_tables.action_text.rb b/db/migrate/20231230212422_create_action_text_tables.action_text.rb new file mode 100644 index 00000000..1be48d70 --- /dev/null +++ b/db/migrate/20231230212422_create_action_text_tables.action_text.rb @@ -0,0 +1,26 @@ +# This migration comes from action_text (originally 20180528164100) +class CreateActionTextTables < ActiveRecord::Migration[6.0] + def change + # Use Active Record's configured type for primary and foreign keys + primary_key_type, foreign_key_type = primary_and_foreign_key_types + + create_table :action_text_rich_texts, id: primary_key_type do |t| + t.string :name, null: false + t.text :body, size: :long + t.references :record, null: false, polymorphic: true, index: false, type: foreign_key_type + + t.timestamps + + t.index [ :record_type, :record_id, :name ], name: "index_action_text_rich_texts_uniqueness", unique: true + end + end + + private + def primary_and_foreign_key_types + config = Rails.configuration.generators + setting = config.options[config.orm][:primary_key_type] + primary_key_type = setting || :primary_key + foreign_key_type = setting || :bigint + [primary_key_type, foreign_key_type] + end +end diff --git a/db/schema.rb b/db/schema.rb index 6f759db8..35c0c005 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,10 +10,57 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.1].define(version: 2023_10_09_160058) do +ActiveRecord::Schema[7.1].define(version: 2023_12_30_212422) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" + create_table "action_text_rich_texts", force: :cascade do |t| + t.string "name", null: false + t.text "body" + t.string "record_type", null: false + t.bigint "record_id", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["record_type", "record_id", "name"], name: "index_action_text_rich_texts_uniqueness", unique: true + end + + create_table "active_storage_attachments", force: :cascade do |t| + t.string "name", null: false + t.string "record_type", null: false + t.bigint "record_id", null: false + t.bigint "blob_id", null: false + t.datetime "created_at", null: false + t.index ["blob_id"], name: "index_active_storage_attachments_on_blob_id" + t.index ["record_type", "record_id", "name", "blob_id"], name: "index_active_storage_attachments_uniqueness", unique: true + end + + create_table "active_storage_blobs", force: :cascade do |t| + t.string "key", null: false + t.string "filename", null: false + t.string "content_type" + t.text "metadata" + t.string "service_name", null: false + t.bigint "byte_size", null: false + t.string "checksum" + t.datetime "created_at", null: false + t.index ["key"], name: "index_active_storage_blobs_on_key", unique: true + end + + create_table "active_storage_variant_records", force: :cascade do |t| + t.bigint "blob_id", null: false + t.string "variation_digest", null: false + t.index ["blob_id", "variation_digest"], name: "index_active_storage_variant_records_uniqueness", unique: true + end + + create_table "blog_posts", force: :cascade do |t| + t.string "title" + t.string "slug" + t.string "description" + t.boolean "draft" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + create_table "delayed_jobs", force: :cascade do |t| t.integer "priority", default: 0, null: false t.integer "attempts", default: 0, null: false @@ -45,4 +92,6 @@ t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true end + add_foreign_key "active_storage_attachments", "active_storage_blobs", column: "blob_id" + add_foreign_key "active_storage_variant_records", "active_storage_blobs", column: "blob_id" end diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb index 5362225e..c03473e9 100644 --- a/spec/rails_helper.rb +++ b/spec/rails_helper.rb @@ -48,6 +48,9 @@ # Remove this line if you're not using ActiveRecord or ActiveRecord fixtures config.fixture_path = "#{::Rails.root}/spec/fixtures" + # Remove this line if you're not using ActiveRecord or ActiveRecord fixtures + config.fixture_path = "#{::Rails.root}/spec/fixtures" + # TODO: update fixture_path to below (not yet supported by Rspec) # config.fixture_paths << "#{::Rails.root}/spec/fixtures" @@ -56,6 +59,21 @@ # instead of true. config.use_transactional_fixtures = true + # You can uncomment this line to turn off ActiveRecord support entirely. + # config.use_active_record = false + + # RSpec Rails can automatically mix in different behaviours to your tests + # based on their file location, for example enabling you to call `get` and + # `post` in specs under `spec/controllers`. + # + # You can disable this behaviour by removing the line below, and instead + # explicitly tag your specs with their type, e.g.: + # + # RSpec.describe UsersController, type: :controller do + # # ... + # end + # + # The different available types are documented in the features, such as in # https://relishapp.com/rspec/rspec-rails/docs config.infer_spec_type_from_file_location! From 9fbfdd35138670e6ffc2aa40950c7c020bccf9ac Mon Sep 17 00:00:00 2001 From: 5andu Date: Sun, 31 Dec 2023 00:10:56 +0100 Subject: [PATCH 03/13] added blog post MVC --- app/assets/images/founder-avatar.png | Bin 0 -> 10601 bytes app/controllers/blog_posts_controller.rb | 71 +++++++++ app/models/blog_post.rb | 25 ++++ app/views/blog_posts/_blog_post.html.erb | 23 +++ app/views/blog_posts/_form.html.erb | 137 ++++++++++++++++++ app/views/blog_posts/edit.html.erb | 17 +++ app/views/blog_posts/index.html.erb | 60 ++++++++ app/views/blog_posts/new.html.erb | 15 ++ app/views/blog_posts/show.html.erb | 50 +++++++ .../devise/shared/_error_messages.html.erb | 2 +- .../action_text/contents/_content.html.erb | 3 + app/views/shared/_header.html.erb | 5 +- config/routes.rb | 1 + 13 files changed, 406 insertions(+), 3 deletions(-) create mode 100644 app/assets/images/founder-avatar.png create mode 100644 app/controllers/blog_posts_controller.rb create mode 100644 app/models/blog_post.rb create mode 100644 app/views/blog_posts/_blog_post.html.erb create mode 100644 app/views/blog_posts/_form.html.erb create mode 100644 app/views/blog_posts/edit.html.erb create mode 100644 app/views/blog_posts/index.html.erb create mode 100644 app/views/blog_posts/new.html.erb create mode 100644 app/views/blog_posts/show.html.erb create mode 100644 app/views/layouts/action_text/contents/_content.html.erb diff --git a/app/assets/images/founder-avatar.png b/app/assets/images/founder-avatar.png new file mode 100644 index 0000000000000000000000000000000000000000..cd65041cbb6652f5383f7d1935fabb89c9af134e GIT binary patch literal 10601 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∈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 literal 0 HcmV?d00001 diff --git a/app/controllers/blog_posts_controller.rb b/app/controllers/blog_posts_controller.rb new file mode 100644 index 00000000..52c8197c --- /dev/null +++ b/app/controllers/blog_posts_controller.rb @@ -0,0 +1,71 @@ +class BlogPostsController < ApplicationController + before_action :set_blog_post, only: %i[show edit update destroy] + before_action :require_admin!, only: %i[new edit create update destroy] + # before_action :authenticate_user!, only: %i[new create edit update destroy] + + # GET /blog_posts + def index + @blog_posts = BlogPost.where(draft: false).order(created_at: :asc) + @drafts = BlogPost.where(draft: true).order(created_at: :desc) + end + + # GET /blog_posts/slug + def show + end + + # GET /blog_posts/new + def new + @blog_post = BlogPost.new + + end + + # GET /blog_posts/slug/edit + def edit + end + + # POST /blog_posts + def create + @blog_post = BlogPost.new(blog_post_params) + + if @blog_post.save + redirect_to blog_post_path(@blog_post.slug), notice: "Blog post was successfully created." + else + render :new, status: :unprocessable_entity + end + end + + # PATCH/PUT /blog_posts/slug + def update + @blog_post.slug = params[:blog_post][:slug] + if @blog_post.save && @blog_post.update(blog_post_params) + redirect_to blog_post_path(@blog_post.slug), notice: "Blog post was successfully updated." + else + render :edit, status: :unprocessable_entity + end + end + + # DELETE /blog_posts/1 + def destroy + @blog_post.destroy + redirect_to blog_posts_url, notice: "Blog post was successfully destroyed." + end + + private + + # Use callbacks to share common setup or constraints between actions. + def set_blog_post + slug = params[:blog_post].present? ? params[:blog_post][:slug] : params[:slug] + @blog_post = BlogPost.find_by!(slug: slug) + end + + # Only allow a list of trusted parameters through, but add :body, and use slug instead of id in the URL. + def blog_post_params + params.require(:blog_post).permit(:title, :slug, :description, :body, :cover_image, :draft) + end + + def require_admin! + unless current_user&.admin? + redirect_to root_path, alert: "You are not authorized to view this page." + end + end +end diff --git a/app/models/blog_post.rb b/app/models/blog_post.rb new file mode 100644 index 00000000..117c44df --- /dev/null +++ b/app/models/blog_post.rb @@ -0,0 +1,25 @@ +class BlogPost < ApplicationRecord + has_one_attached :cover_image + has_rich_text :body + validates :slug, uniqueness: true + before_validation :generate_unique_slug + validates_presence_of :title, :slug, :body, :description + + def to_param + slug + end + + private + + def generate_unique_slug + if new_record? || slug_changed? + base_slug = slug.blank? ? title.parameterize : slug + other = self.class.where("slug LIKE ?", "#{base_slug}%") + self.slug = if other.exists? + "#{base_slug}-#{other.count + 1}" + else + base_slug + end + end + end +end diff --git a/app/views/blog_posts/_blog_post.html.erb b/app/views/blog_posts/_blog_post.html.erb new file mode 100644 index 00000000..701455c2 --- /dev/null +++ b/app/views/blog_posts/_blog_post.html.erb @@ -0,0 +1,23 @@ +
+
+ + <%= link_to blog_post_path(blog_post) do %> + +
+ <%= image_tag blog_post.cover_image, class:"aspect-[16/9] w-full rounded-2xl bg-gray-100 object-cover" if blog_post.cover_image.attached? %> +
+
+
+
+ +
+
+

+ <%= blog_post.title %> +

+

<%= blog_post.description %>

+
+ + <% end %> +
+
diff --git a/app/views/blog_posts/_form.html.erb b/app/views/blog_posts/_form.html.erb new file mode 100644 index 00000000..41144359 --- /dev/null +++ b/app/views/blog_posts/_form.html.erb @@ -0,0 +1,137 @@ +<%= form_with(model: blog_post) do |form| %> + <% if blog_post.errors.any? %> +
+

<%= pluralize(blog_post.errors.count, "error") %> prohibited this blog_post from being saved:

+ +
    + <% blog_post.errors.each do |error| %> +
  • <%= error.full_message %>
  • + <% end %> +
+
+ <% end %> +
+ <% unless turbo_native_app? %> + <% end %> +
+
+ +
+
+
+ <%= form.label :slug, class: "block text-sm font-medium text-gray-700" %> +
+ + https://yt.careers/blog/ + + <%= form.text_field :slug, required: true, class: "flex-1 block w-full min-w-0 rounded-none rounded-r-md sm:text-sm border-gray-300 focus:border-primary-500 focus:ring focus:ring-primary-500 focus:ring-opacity-50", placeholder: "how-to-grow-a-saas" %> +
+
+ + <%# checkbox to consider this post a draft %> +
+
+
+ <%= form.check_box :draft, class: "focus:ring-primary-500 h-4 w-4 text-primary-600 border-gray-300 rounded" %> +
+
+ <%= form.label :draft, class: "font-medium text-gray-700" %> +

Drafts are not visible to the public

+
+
+
+ +
+ <%= form.label :title, class: "block text-sm font-medium text-gray-700" %> + <%= form.text_field :title, required: true, class: "mt-1 focus:border-primary-500 focus:ring focus:ring-primary-500 focus:ring-opacity-50 block w-full shadow-sm sm:text-sm border-gray-300 rounded-md", placeholder: "Title of your blog post. (65 characters max)", maxlength: 65 %> +
+ +
+ <%= form.label :description, class: "block text-sm font-medium text-gray-700" %> + <%= form.text_area :description, required: true, rows: 2 , class: "mt-1 focus:border-primary-500 focus:ring focus:ring-primary-500 focus:ring-opacity-50 block w-full shadow-sm sm:text-sm border-gray-300 rounded-md", placeholder: "A snippet of text designed to summarise a page. Though not critically important, an ideal length would be 70-155 characters.", maxlength: 155 %> +
+ +
+ +
+
+ <%= image_tag @blog_post.cover_image, id: 'cover_image_preview', data: { url: url_for(@blog_post.cover_image) } if @blog_post.cover_image.attached? %> + +
+ +

or drag and drop

+
+

PNG, JPG, WEBP up to 1MB (1200 × 630 px recommended)

+
+
+
+ +
+ <%= form.rich_text_area :body, placeholder: "Write your blog post...", required: true, class:"group-focus:ring-gray-500 group-focus:border-gray-500" %> +
+ +
+ <%= form.submit "Save", class:"group relative w-full flex justify-center py-2.5 px-4 border border-transparent text-sm leading-5 font-medium rounded-md text-white bg-primary-600 hover:bg-primary-700 focus:outline-none cursor-pointer"%> +
+
+
+
+
+
+<% end %> + + diff --git a/app/views/blog_posts/edit.html.erb b/app/views/blog_posts/edit.html.erb new file mode 100644 index 00000000..6dad2d9c --- /dev/null +++ b/app/views/blog_posts/edit.html.erb @@ -0,0 +1,17 @@ +<% meta title: "Edit blog post", description: "Edit blog post." %> + +

Edit blog post

+ +
+ <%= link_to "Show blog post", blog_post_path(@blog_post.slug), class:"mx-auto w-fit shrink-0 mt-4 md:mt-0 inline-flex items-center justify-center sm:justify-start px-4 py-2 border border-gray-300 shadow-sm text-base font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500" %> + <%= link_to "Back to blog posts", blog_posts_path, class:"mx-auto w-fit shrink-0 mt-4 md:mt-0 inline-flex items-center justify-center sm:justify-start px-4 py-2 border border-gray-300 shadow-sm text-base font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500" %> +
+ +
+ <%= render "form", blog_post: @blog_post %> +
+ +
+ <%= link_to "Show blog post", blog_post_path(@blog_post.slug), class:"mx-auto w-fit shrink-0 mt-4 md:mt-0 inline-flex items-center justify-center sm:justify-start px-4 py-2 border border-gray-300 shadow-sm text-base font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500" %> + <%= link_to "Back to blog posts", blog_posts_path, class:"mx-auto w-fit shrink-0 mt-4 md:mt-0 inline-flex items-center justify-center sm:justify-start px-4 py-2 border border-gray-300 shadow-sm text-base font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500" %> +
diff --git a/app/views/blog_posts/index.html.erb b/app/views/blog_posts/index.html.erb new file mode 100644 index 00000000..a7705051 --- /dev/null +++ b/app/views/blog_posts/index.html.erb @@ -0,0 +1,60 @@ +<% meta title: "Blog posts", description: "All blog posts from #{ENV['COMPANY_NAME']}." %> + +<%# Title %> +
+
+

+ Blog posts +

+

+ All <%= ENV['COMPANY_NAME'] %> blog posts. +

+
+
+ +<% if current_user&.admin? %> + <%# New blog post button, only accessibly by admins %> +
+
+
+ + <%= link_to new_blog_post_path, class: "focus:outline-none" do %> + + New blog post + <% end %> +
+
+
+<% end %> + +
+
+
+
+
+ <%# Blog post list %> + <% @blog_posts.each do |blog_post| %> + <%= render blog_post %> + <% end %> +
+
+ + <% if current_user && current_user.admin? && @drafts.any? %> +
+

+ Drafts +

+
+ <%# Drafts %> + <% @drafts.each do |blog_post| %> + <%= render blog_post %> + <% end %> +
+
+ <% end %> +
+
+ +
diff --git a/app/views/blog_posts/new.html.erb b/app/views/blog_posts/new.html.erb new file mode 100644 index 00000000..e608c74b --- /dev/null +++ b/app/views/blog_posts/new.html.erb @@ -0,0 +1,15 @@ +<% meta title: "Create a new blog post", description: "New blog post for #{ENV['COMPANY_NAME']}." %> + +

New blog post

+ +
+ <%= link_to "Back to blog posts", blog_posts_path, class:"mx-auto w-fit shrink-0 mt-4 md:mt-0 inline-flex items-center justify-center sm:justify-start px-4 py-2 border border-gray-300 shadow-sm text-base font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500" %> +
+ +
+ <%= render "form", blog_post: @blog_post %> +
+ +
+ <%= link_to "Back to blog posts", blog_posts_path, class:"mx-auto w-fit shrink-0 mt-4 md:mt-0 inline-flex items-center justify-center sm:justify-start px-4 py-2 border border-gray-300 shadow-sm text-base font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500" %> +
diff --git a/app/views/blog_posts/show.html.erb b/app/views/blog_posts/show.html.erb new file mode 100644 index 00000000..a28e1168 --- /dev/null +++ b/app/views/blog_posts/show.html.erb @@ -0,0 +1,50 @@ +<% meta title: "#{@blog_post.title}", description: "#{@blog_post.description}" %> + +
+
+
+
+ <%= link_to "← Back to all articles", blog_posts_path, class:"hover:opacity-75 mr-1 w-fit font-semibold" %> +
+
+ <%= image_tag @blog_post.cover_image, class:"aspect-[16/9] w-full rounded-2xl bg-gray-100 object-cover" if @blog_post.cover_image.attached? %> +
+
+ <% if current_user&.admin? %> +
+
+ <%= link_to edit_blog_post_path(@blog_post.slug), class:"shrink-0 mt-4 md:mt-0 inline-flex items-center justify-center px-4 py-2 border border-gray-300 shadow-sm text-base font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500" do %> + + Edit + <% end %> + <%= button_to @blog_post.slug, method: :delete, class:"shrink-0 mt-4 md:mt-0 inline-flex items-center justify-center px-4 py-2 border border-gray-300 shadow-sm text-base font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500" do %> + + Delete + <% end %> +
+
+ <% end %> +
+
+
+ +
+

+ <%= @blog_post.title %> +

+

<%= @blog_post.body %>

+
+
+
+ <%= image_tag "founder-avatar.png", class:"mx-auto h-24 w-24 rounded-full" %> +

Your name

+

Founder of <%= ENV['COMPANY_NAME'] %>

+
+
+
+
+
diff --git a/app/views/devise/shared/_error_messages.html.erb b/app/views/devise/shared/_error_messages.html.erb index 94946da9..e2450bfa 100644 --- a/app/views/devise/shared/_error_messages.html.erb +++ b/app/views/devise/shared/_error_messages.html.erb @@ -1,7 +1,7 @@ <% if resource.errors.any? %>

- <%= I18n.t("errors.messages.not_saved", + <%= I18n."Errors prohibited this #{resource_name} from being saved", count: resource.errors.count, resource: resource.class.model_name.human.downcase) %> diff --git a/app/views/layouts/action_text/contents/_content.html.erb b/app/views/layouts/action_text/contents/_content.html.erb new file mode 100644 index 00000000..9e3c0d0d --- /dev/null +++ b/app/views/layouts/action_text/contents/_content.html.erb @@ -0,0 +1,3 @@ +
+ <%= yield -%> +
diff --git a/app/views/shared/_header.html.erb b/app/views/shared/_header.html.erb index f21e7ef3..e843a91a 100644 --- a/app/views/shared/_header.html.erb +++ b/app/views/shared/_header.html.erb @@ -2,10 +2,10 @@