diff --git a/Procfile.dev b/Procfile.dev index 6d9e85a..81c6404 100644 --- a/Procfile.dev +++ b/Procfile.dev @@ -1,4 +1,5 @@ web: bundle exec hanami server -tailwind: bundle exec rake tailwind:watch +tailwind_main: bundle exec rake tailwind:watch +tailwind_admin: bundle exec rake tailwind:watch_admin assets: bundle exec hanami assets watch redis: redis-server diff --git a/Rakefile b/Rakefile index 4aca592..fa37fa8 100644 --- a/Rakefile +++ b/Rakefile @@ -41,7 +41,7 @@ namespace :blog do next if tweet["tweet"]["full_text"].start_with? "@" tweet["tweet"]["full_text"] = tweet["tweet"]["full_text"].gsub(/(#{URI::DEFAULT_PARSER.make_regexp})/, "#{$1}") - repo.create({slug: tweet["tweet"]["id"], content: tweet["tweet"]["full_text"], published_at: tweet["tweet"]["created_at"], category: [], post_type: "post", syndication_sources: {twitter: "https://twitter.com/nitza/status/#{tweet["tweet"]["id"]}"}}) + repo.create({slug: tweet["tweet"]["id"], content: tweet["tweet"]["full_text"], published_at: tweet["tweet"]["created_at"], category: ["tweet"], post_type: "post", syndication_sources: {twitter: "https://twitter.com/nitza/status/#{tweet["tweet"]["id"]}"}}) end end @@ -100,10 +100,15 @@ end namespace :tailwind do task :watch do - system("npx tailwindcss -i ./slices/main/assets/css/app.css -o ./public/assets/main/app.css --watch") + system("npx tailwindcss -i ./slices/main/assets/css/app.css -o ./public/assets/_main/app.css --watch") + end + + task :watch_admin do + system("npx tailwindcss -i ./slices/admin/assets/css/app.css -o ./public/assets/_admin/app.css --watch") end task :build do - system("npx tailwindcss -i ./slices/main/assets/css/app.css -o ./slices/main/assets/builds/app.css --minify") + system("npx tailwindcss -i ./slices/main/assets/css/app.css -o ./slices/_main/assets/builds/app.css --minify") + system("npx tailwindcss -i ./slices/admin/assets/css/app.css -o ./slices/_admin/assets/builds/app.css --minify") end end diff --git a/app/relations/highlights.rb b/app/relations/highlights.rb new file mode 100644 index 0000000..eb8f035 --- /dev/null +++ b/app/relations/highlights.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module Adamantium + module Relations + class Highlights < ROM::Relation[:sql] + schema :highlights, infer: true do + associations do + belongs_to :post + end + end + + auto_struct(true) + end + end +end diff --git a/app/relations/posts.rb b/app/relations/posts.rb index ceda518..73012c5 100644 --- a/app/relations/posts.rb +++ b/app/relations/posts.rb @@ -11,8 +11,8 @@ module Adamantium has_many :post_trips has_many :trips, through: :post_trips + has_many :highlights has_many :reactions - has_many :webmentions end end diff --git a/app/templates/layouts/app.html.slim b/app/templates/layouts/app.html.slim index 8ea8ba8..0c9d68e 100644 --- a/app/templates/layouts/app.html.slim +++ b/app/templates/layouts/app.html.slim @@ -31,9 +31,9 @@ html x-data="{darkMode: $persist(false)}" :class="{'dark' : darkMode === true}" script data-goatcounter="https://stats.dnitza.com/count" async="" src="//stats.dnitza.com/count.js" script src="https://unpkg.com/htmx.org@1.9.2/dist/htmx.min.js" integrity="sha384-L6OqL9pRWyyFU3+/bjdSri+iIphTN/bvYyM37tICVyOJkWZLpP2vGn6VUEXgzg6h" crossorigin="anonymous" - script src="https://cdn.jsdelivr.net/npm/@alpinejs/persist@3.13.3/dist/cdn.min.js" + script src="https://cdn.jsdelivr.net/npm/@alpinejs/persist@3.13.8/dist/cdn.min.js" = javascript_tag "app" - script src="https://cdn.jsdelivr.net/npm/alpinejs@3.13.3/dist/cdn.min.js" defer="" + script src="https://cdn.jsdelivr.net/npm/alpinejs@3.13.8/dist/cdn.min.js" defer="" link rel="stylesheet" href="https://unpkg.com/@highlightjs/cdn-assets@11.8.0/styles/github-dark.min.css" script src="https://unpkg.com/@highlightjs/cdn-assets@11.8.0/highlight.min.js" defer="" diff --git a/config/app.rb b/config/app.rb index 14dcd5e..61d66b9 100644 --- a/config/app.rb +++ b/config/app.rb @@ -9,8 +9,9 @@ module Adamantium config.actions.content_security_policy[:script_src] += " https://api.mapbox.com/mapbox-gl-js/v2.9.1/mapbox-gl.js" config.actions.content_security_policy[:media_src] += " https://dnitza.com" config.actions.content_security_policy[:script_src] += " https://unpkg.com/htmx.org@1.9.2/dist/htmx.min.js " - config.actions.content_security_policy[:script_src] += " https://cdn.jsdelivr.net/npm/alpinejs@3.13.3/dist/cdn.min.js" - config.actions.content_security_policy[:script_src] += " https://cdn.jsdelivr.net/npm/@alpinejs/persist@3.13.3/dist/cdn.min.js" + config.actions.content_security_policy[:script_src] += " https://cdn.jsdelivr.net/npm/alpinejs@3.13.8/dist/cdn.min.js" + config.actions.content_security_policy[:script_src] += " https://cdn.jsdelivr.net/npm/@alpinejs/persist@3.13.8/dist/cdn.min.js" + config.actions.content_security_policy[:script_src] += " https://cdn.jsdelivr.net/npm/@alpinejs/anchor@3.13.8/dist/cdn.min.js" config.actions.content_security_policy[:script_src] += " https://unpkg.com/@highlightjs/cdn-assets@11.8.0/highlight.min.js" config.actions.content_security_policy[:connect_src] += " https://stats.dnitza.com/count https://*.mapbox.com" diff --git a/db/migrate/20240405092210_create_highlights.rb b/db/migrate/20240405092210_create_highlights.rb new file mode 100644 index 0000000..74d7c22 --- /dev/null +++ b/db/migrate/20240405092210_create_highlights.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +ROM::SQL.migration do + change do + create_table :highlights do + primary_key :id + foreign_key :post_id, :posts, null: false + column :text, :text, null: false + + index :post_id + end + end +end diff --git a/slices/admin/actions/posts/create_highlight.rb b/slices/admin/actions/posts/create_highlight.rb new file mode 100644 index 0000000..44d8feb --- /dev/null +++ b/slices/admin/actions/posts/create_highlight.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module Admin + module Actions + module Posts + class CreateHighlight < Admin::Action + include Deps["commands.highlight.create"] + + def handle(req, res) + create.call(post_id: req.params[:id], text: req.params[:text]) + + res.redirect_to "/admin/posts/#{req.params[:id]}" + end + end + end + end +end diff --git a/slices/admin/actions/posts/delete_highlight.rb b/slices/admin/actions/posts/delete_highlight.rb new file mode 100644 index 0000000..3b94a85 --- /dev/null +++ b/slices/admin/actions/posts/delete_highlight.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module Admin + module Actions + module Posts + class DeleteHighlight < Admin::Action + include Deps["repos.highlight_repo"] + + def handle(req, res) + highlight_repo.delete(req.params[:highlight_id]) + + res.redirect_to "/admin/posts/#{req.params[:post_id]}" + end + end + end + end +end diff --git a/slices/admin/assets/css/app.css b/slices/admin/assets/css/app.css index b5ed0f7..8d6981e 100644 --- a/slices/admin/assets/css/app.css +++ b/slices/admin/assets/css/app.css @@ -1,5 +1,4 @@ -body { - background-color: #fff; - color: #000; - font-family: sans-serif; -} +@tailwind base; +@tailwind components; +@tailwind utilities; +@tailwind typography; diff --git a/slices/admin/assets/js/app.js b/slices/admin/assets/js/app.js index 2632e6f..650c851 100644 --- a/slices/admin/assets/js/app.js +++ b/slices/admin/assets/js/app.js @@ -6,5 +6,26 @@ import "../css/app.css"; Alpine.magic('clipboard', () => { return subject => navigator.clipboard.writeText(subject) }) + + Alpine.magic('textHighlighter', (el, {Alpine}) => { + return { + isOpen: false, + anchorX: "0px", + anchorY: "0px", + selection: null, + text: "", + highlightText() { + this.isOpen = false + // document. + this.selection = document.getSelection() + this.text = this.selection.toString() + const anchor = this.selection.focusNode.parentElement.getBoundingClientRect() + this.isOpen = el.contains(this.selection.focusNode) && this.selection.focusOffset != this.selection.anchorOffset; + + this.anchorX = `${anchor.left - 170}px` + this.anchorY = `${anchor.top + window.scrollY}px` + } + } + }) }) })(); \ No newline at end of file diff --git a/slices/admin/commands/highlight/create.rb b/slices/admin/commands/highlight/create.rb new file mode 100644 index 0000000..c0bfed4 --- /dev/null +++ b/slices/admin/commands/highlight/create.rb @@ -0,0 +1,16 @@ +module Admin + module Commands + module Highlight + class Create + include Dry::Monads[:result] + include Deps["repos.highlight_repo"] + + def call(post_id:, text:) + highlight_repo.create(post_id:, text:) + + Success() + end + end + end + end +end diff --git a/slices/admin/config/routes.rb b/slices/admin/config/routes.rb index 5e52174..8362d50 100644 --- a/slices/admin/config/routes.rb +++ b/slices/admin/config/routes.rb @@ -54,6 +54,8 @@ module Admin post "/posts/:id/de_syndicate/:target", to: Auth.call(action: "posts.de_syndicate") post "/posts/:id/syndicate/:target", to: Auth.call(action: "posts.syndicate") post "/post/:id/update", to: Auth.call(action: "posts.update") + post "/post/:id/highlight", to: Auth.call(action: "posts.create_highlight") + delete "/post/:post_id/highlight/:highlight_id", to: Auth.call(action: "posts.delete_highlight") get "/media", to: Auth.call(action: "photos.index") delete "/media/public/media/:year/:path", to: Auth.call(action: "photos.delete") diff --git a/slices/admin/repos/highlight_repo.rb b/slices/admin/repos/highlight_repo.rb new file mode 100644 index 0000000..1e13a47 --- /dev/null +++ b/slices/admin/repos/highlight_repo.rb @@ -0,0 +1,12 @@ +module Admin + module Repos + class HighlightRepo < Adamantium::Repo[:highlights] + commands :create, delete: :by_pk + + def list_all + highlights + .to_a + end + end + end +end diff --git a/slices/admin/repos/post_repo.rb b/slices/admin/repos/post_repo.rb index 414fa89..350ca11 100644 --- a/slices/admin/repos/post_repo.rb +++ b/slices/admin/repos/post_repo.rb @@ -71,7 +71,7 @@ module Admin def find(id:) posts - .combine(:tags) + .combine(:tags, :highlights) .where(id: id).one! end diff --git a/slices/admin/templates/bookmarks/index.html.slim b/slices/admin/templates/bookmarks/index.html.slim index de91058..93e8239 100644 --- a/slices/admin/templates/bookmarks/index.html.slim +++ b/slices/admin/templates/bookmarks/index.html.slim @@ -19,7 +19,7 @@ div class="max-w-prose mx-auto" x-data="{ activeTab: 0 }" tr id="bookmark-#{bookmark.id}" td div - a href="/bookmark/#{bookmark.slug}" + a href="/admin/posts/#{bookmark.id}" = bookmark.name a class="no-underline" href=bookmark.url small class="text-gray-400 dark:text-gray-600" = bookmark.url diff --git a/slices/admin/templates/layouts/app.html.slim b/slices/admin/templates/layouts/app.html.slim index 3fa073c..a0f4a5b 100644 --- a/slices/admin/templates/layouts/app.html.slim +++ b/slices/admin/templates/layouts/app.html.slim @@ -14,7 +14,8 @@ html = javascript_tag "app" script src="https://unpkg.com/htmx.org@1.9.2/dist/htmx.min.js" integrity="sha384-L6OqL9pRWyyFU3+/bjdSri+iIphTN/bvYyM37tICVyOJkWZLpP2vGn6VUEXgzg6h" crossorigin="anonymous" - script src="https://cdn.jsdelivr.net/npm/alpinejs@3.12.0/dist/cdn.min.js" defer="true" + script src="https://cdn.jsdelivr.net/npm/@alpinejs/anchor@3.13.8/dist/cdn.min.js" defer="true" + script src="https://cdn.jsdelivr.net/npm/alpinejs@3.13.8/dist/cdn.min.js" defer="true" - if Hanami.app.settings.micropub_pub_key link rel="pgpkey" href="/key" diff --git a/slices/admin/templates/posts/show.html.slim b/slices/admin/templates/posts/show.html.slim index bd1f16a..d2e7886 100644 --- a/slices/admin/templates/posts/show.html.slim +++ b/slices/admin/templates/posts/show.html.slim @@ -24,12 +24,18 @@ div class="mb-12 prose dark:prose-invert max-w-prose mx-auto text-gray-800 dark: button hx-post="/admin/posts/#{post.id}/syndicate/day_one" Send to Day One // TODO: Add preview, fix sending to DayOne -article class="mb-12 prose dark:prose-invert max-w-prose mx-auto text-gray-800 dark:text-gray-200 prose-a:text-blue-600 prose-a:no-underline hover:prose-a:underline prose-img:rounded" - a href="/post/#{post.slug}" - h1= post.name || "💬" +article x-data="$textHighlighter, {isOpen: false, anchorX: 0, anchorY: 0, text: ''}" class="mb-12 prose dark:prose-invert max-w-prose mx-auto text-gray-800 dark:text-gray-200 prose-a:text-blue-600 prose-a:no-underline hover:prose-a:underline prose-img:rounded" + - if post.post_type != "bookmark" + a href="/post/#{post.slug}" + h1= post.name || "💬" form action="/admin/post/#{post.id}/update" method="POST" - textarea name="body" class="text-gray-800 w-full border-blue-200 border-2 rounded p-2 mb-4" x-data="{ resize: () => { $el.style.height = '5px'; $el.style.height = $el.scrollHeight + 'px' } }" x-init="resize()" @input="resize()" - == markdown_body + - if post.post_type != "bookmark" + textarea name="body" class="text-gray-800 w-full border-blue-200 border-2 rounded p-2 mb-4" x-data="{ resize: () => { $el.style.height = '5px'; $el.style.height = $el.scrollHeight + 'px' } }" x-init="resize()" @input="resize()" + == markdown_body + - if post.post_type == "bookmark" + div x-ref="bookmarkText" @mouseup.capture="highlightText()" + == post.cached_content + fieldset class="mb-4 flex" label for="commentable" class="mr-2" Commentable? input class="mt-2" type="checkbox" value="true" id="commentable" name="commentable" switch="switch" checked=post.commentable @@ -38,3 +44,21 @@ article class="mb-12 prose dark:prose-invert max-w-prose mx-auto text-gray-800 d input type="text" name="tags" id="tags" class="w-full px-1 border rounded" value="#{post.tags.map(&:label).join(", ")}" button class="rounded bg-blue-100 hover:bg-blue-200 text-blue-600 px-2 hover:cursor-pointer" type="submit" = "Update" + + - if post.highlights.count > 0 + table class="prose dark:prose-invert table-auto" + - post.highlights.each_with_index do |highlight, idx| + tr class="#{idx.even? ? 'bg-amber-50' : ''}" + td class="p-2" + = highlight.text + td class="p-2" + form method="POST" action="/admin/post/#{post.id}/highlight/#{highlight.id}" + input type="hidden" name="_method" value="delete" + button + = "Delete" + + div @click.outside="isOpen = false" class="p-2 bg-indigo-900 hover:bg-indigo-800 rounded text-white shadow-md shadow-indigo-500" x-show="isOpen" x-anchor.no-style="$refs.bookmarkText" :style="{ position: 'absolute', hidden: isOpen, top: isOpen ? anchorY : '0px', left: isOpen ? anchorX : '0px' }" + form method="POST" class="p-0 m-0" action="/admin/post/#{post.id}/highlight" + input type="hidden" name="post_id" value="#{post.id}" + input class="text-gray-600" type="hidden" x-model="text" name="text" + button = "Save highlight" diff --git a/slices/main/repos/post_repo.rb b/slices/main/repos/post_repo.rb index 4263313..d24dbdf 100644 --- a/slices/main/repos/post_repo.rb +++ b/slices/main/repos/post_repo.rb @@ -184,7 +184,7 @@ module Main def fetch!(slug) posts .published - .combine(:tags, :trips, :webmentions, :reactions) + .combine(:tags, :trips, :webmentions, :reactions, :highlights) .node(:webmentions) { |webmention| webmention.published.where(type: "reply") } diff --git a/slices/main/templates/bookmarks/show.html.slim b/slices/main/templates/bookmarks/show.html.slim index 449d42e..df1683f 100644 --- a/slices/main/templates/bookmarks/show.html.slim +++ b/slices/main/templates/bookmarks/show.html.slim @@ -13,6 +13,13 @@ div class="mb-12 prose dark:prose-invert max-w-prose mx-auto text-gray-800 dark: == bookmark.content + - if bookmark.highlights.count > 0 + h2 Highlights + - bookmark.highlights.each do |highlight| + div class="rounded p-2 bg-amber-100 dark:bg-amber-800 mb-4" + = render "shared/quote", color: "fill-amber-200 dark:fill-amber-700" + = highlight.text + - unless bookmark.cached_content.nil? button class="hover:text-gray-400" @click="open = ! open" Toggle cached version diff --git a/slices/main/templates/layouts/app.html.slim b/slices/main/templates/layouts/app.html.slim index 1696638..5e72b07 100644 --- a/slices/main/templates/layouts/app.html.slim +++ b/slices/main/templates/layouts/app.html.slim @@ -31,9 +31,9 @@ html x-data="{darkMode: $persist(false)}" :class="{'dark' : darkMode === true}" script data-goatcounter="https://stats.dnitza.com/count" async="" src="//stats.dnitza.com/count.js" script src="https://unpkg.com/htmx.org@1.9.2/dist/htmx.min.js" integrity="sha384-L6OqL9pRWyyFU3+/bjdSri+iIphTN/bvYyM37tICVyOJkWZLpP2vGn6VUEXgzg6h" crossorigin="anonymous" - script src="https://cdn.jsdelivr.net/npm/@alpinejs/persist@3.13.3/dist/cdn.min.js" + script src="https://cdn.jsdelivr.net/npm/@alpinejs/persist@3.13.8/dist/cdn.min.js" = javascript_tag "app" - script src="https://cdn.jsdelivr.net/npm/alpinejs@3.13.3/dist/cdn.min.js" defer="" + script src="https://cdn.jsdelivr.net/npm/alpinejs@3.13.8/dist/cdn.min.js" defer="" link rel="stylesheet" href="https://unpkg.com/@highlightjs/cdn-assets@11.8.0/styles/github-dark.min.css" script src="https://unpkg.com/@highlightjs/cdn-assets@11.8.0/highlight.min.js" defer="" diff --git a/slices/main/templates/shared/_quote.html.slim b/slices/main/templates/shared/_quote.html.slim new file mode 100644 index 0000000..a95db6a --- /dev/null +++ b/slices/main/templates/shared/_quote.html.slim @@ -0,0 +1,12 @@ +- color_class = defined?(color) ? color : "fill-gray-900" +xml version="1.0" encoding="iso-8859-1" +svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 57 57" xml:space="preserve" class="w-8" + g + circle class="#{color_class}" cx="18.5" cy="31.5" r="5.5" + path class="#{color_class}" d="M18.5,38c-3.584,0-6.5-2.916-6.5-6.5s2.916-6.5,6.5-6.5s6.5,2.916,6.5,6.5S22.084,38,18.5,38z M18.5,27c-2.481,0-4.5,2.019-4.5,4.5s2.019,4.5,4.5,4.5s4.5-2.019,4.5-4.5S20.981,27,18.5,27z" + + g + circle class="#{color_class}" cx="35.5" cy="31.5" r="5.5" + path class="#{color_class}" d="M35.5,38c-3.584,0-6.5-2.916-6.5-6.5s2.916-6.5,6.5-6.5s6.5,2.916,6.5,6.5S39.084,38,35.5,38z M35.5,27c-2.481,0-4.5,2.019-4.5,4.5s2.019,4.5,4.5,4.5s4.5-2.019,4.5-4.5S37.981,27,35.5,27z" + path class="#{color_class}" d="M13,32c-0.553,0-1-0.447-1-1c0-7.72,6.28-14,14-14c0.553,0,1,0.447,1,1s-0.447,1-1,1 c-6.617,0-12,5.383-12,12C14,31.553,13.553,32,13,32z" + path class="#{color_class}" d="M30,32c-0.553,0-1-0.447-1-1c0-7.72,6.28-14,14-14c0.553,0,1,0.447,1,1s-0.447,1-1,1 c-6.617,0-12,5.383-12,12C31,31.553,30.553,32,30,32z" \ No newline at end of file