commit 833f3ea8b23a7ac302f242e38eca29963f36de73 Author: Daniel Nitsikopoulos Date: Fri Jan 27 22:55:09 2023 +1100 Initial commit diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..0bf0575 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,12 @@ +# flyctl launch added from .gitignore +**/.env +**/public/key.pub +**/.idea/* +**/log/* + +# flyctl launch added from .idea/.gitignore +# Default ignored files +.idea/shelf +.idea/workspace.xml +# Editor-based HTTP Client requests +.idea/httpRequests diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0dcf17f --- /dev/null +++ b/.gitignore @@ -0,0 +1,15 @@ +.env +public/key.pub +public/media/**/* +.idea/* +log/* +fly.toml +*.sock +publish.rb +publish_bookmark.rb +node_modules/* +.DS_Store +.bundle/ +.env.* +.vimrc.local + diff --git a/.rspec b/.rspec new file mode 100644 index 0000000..c99d2e7 --- /dev/null +++ b/.rspec @@ -0,0 +1 @@ +--require spec_helper diff --git a/.tool-versions b/.tool-versions new file mode 100644 index 0000000..7dd48f6 --- /dev/null +++ b/.tool-versions @@ -0,0 +1,2 @@ +ruby 3.2.0 +postgres 15.1 diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..62e18b2 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,11 @@ +FROM ruby:3.2-alpine + +RUN apk update && apk add build-base git sqlite-dev postgresql-dev + +WORKDIR /app + +COPY Gemfile* ./ + +RUN gem install bundler:2.2.16 && bundle install --jobs 4 --retry 5 + +COPY . . diff --git a/Gemfile b/Gemfile new file mode 100644 index 0000000..6ab2177 --- /dev/null +++ b/Gemfile @@ -0,0 +1,51 @@ +# frozen_string_literal: true + +source "https://rubygems.org" + +gem "hanami", "~> 2.0.0" +gem "hanami-router", "~> 2.0.0" +gem "hanami-controller", "~> 2.0.0" +gem "hanami-validations", "~> 2.0.0" +# gem "hanami-assets", github: "hanami/view", branch: "main" +gem "hanami-view", github: "hanami/view", branch: "main" + +gem "rom-sql" +gem "pg" + +gem "dry-types" +gem "dry-matcher" +gem "dry-monads" +gem "puma" +gem "rake" +gem "slim" +gem "builder" + +gem "httparty" +gem "redcarpet" +gem "rexml" +gem "babosa" + +group :development, :test do + gem "dotenv" +end + +group :cli, :development do + gem "hanami-reloader" +end + +group :cli, :development, :test do + gem "hanami-rspec" +end + +group :development do + gem "guard-puma", "~> 0.8" + gem "standardrb" + gem "capistrano", "~> 3.17", require: false +end + +group :test do + gem "rack-test" + gem "rom-factory" + gem "database_cleaner-sequel" + gem "timecop" +end diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 0000000..1c4bc8b --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,326 @@ +GIT + remote: https://github.com/hanami/view.git + revision: c1a6f60a989f1face809a6d8d61652748aee19a7 + branch: main + specs: + hanami-view (2.0.0.alpha8) + concurrent-ruby (~> 1.0) + dry-configurable (~> 1.0) + dry-core (~> 1.0) + dry-inflector (~> 1.0, < 2) + tilt (~> 2.0, >= 2.0.6) + +GEM + remote: https://rubygems.org/ + specs: + airbrussh (1.4.1) + sshkit (>= 1.6.1, != 1.7.0) + ast (2.4.2) + babosa (2.0.0) + builder (3.2.4) + capistrano (3.17.1) + airbrussh (>= 1.0.0) + i18n + rake (>= 10.0.0) + sshkit (>= 1.9.0) + coderay (1.1.3) + concurrent-ruby (1.1.10) + database_cleaner-core (2.0.1) + database_cleaner-sequel (2.0.2) + database_cleaner-core (~> 2.0.0) + sequel + diff-lcs (1.5.0) + dotenv (2.8.1) + dry-auto_inject (1.0.0) + dry-core (~> 1.0, < 2) + zeitwerk (~> 2.6) + dry-cli (1.0.0) + dry-configurable (1.0.1) + dry-core (~> 1.0, < 2) + zeitwerk (~> 2.6) + dry-core (1.0.0) + concurrent-ruby (~> 1.0) + zeitwerk (~> 2.6) + dry-events (1.0.1) + concurrent-ruby (~> 1.0) + dry-core (~> 1.0, < 2) + dry-files (1.0.1) + dry-inflector (1.0.0) + dry-initializer (3.1.1) + dry-logger (1.0.3) + dry-logic (1.5.0) + concurrent-ruby (~> 1.0) + dry-core (~> 1.0, < 2) + zeitwerk (~> 2.6) + dry-matcher (1.0.0) + dry-core (~> 1.0, < 2) + dry-monads (1.6.0) + concurrent-ruby (~> 1.0) + dry-core (~> 1.0, < 2) + zeitwerk (~> 2.6) + dry-monitor (1.0.1) + dry-configurable (~> 1.0, < 2) + dry-core (~> 1.0, < 2) + dry-events (~> 1.0, < 2) + dry-schema (1.13.0) + concurrent-ruby (~> 1.0) + dry-configurable (~> 1.0, >= 1.0.1) + dry-core (~> 1.0, < 2) + dry-initializer (~> 3.0) + dry-logic (>= 1.5, < 2) + dry-types (>= 1.7, < 2) + zeitwerk (~> 2.6) + dry-struct (1.6.0) + dry-core (~> 1.0, < 2) + dry-types (>= 1.7, < 2) + ice_nine (~> 0.11) + zeitwerk (~> 2.6) + dry-system (1.0.1) + dry-auto_inject (~> 1.0, < 2) + dry-configurable (~> 1.0, < 2) + dry-core (~> 1.0, < 2) + dry-inflector (~> 1.0, < 2) + dry-transformer (1.0.1) + zeitwerk (~> 2.6) + dry-types (1.7.0) + concurrent-ruby (~> 1.0) + dry-core (~> 1.0, < 2) + dry-inflector (~> 1.0, < 2) + dry-logic (>= 1.4, < 2) + zeitwerk (~> 2.6) + dry-validation (1.10.0) + concurrent-ruby (~> 1.0) + dry-core (~> 1.0, < 2) + dry-initializer (~> 3.0) + dry-schema (>= 1.12, < 2) + zeitwerk (~> 2.6) + faker (2.23.0) + i18n (>= 1.8.11, < 2) + ffi (1.15.5) + formatador (1.1.0) + guard (2.18.0) + formatador (>= 0.2.4) + listen (>= 2.7, < 4.0) + lumberjack (>= 1.0.12, < 2.0) + nenv (~> 0.1) + notiffany (~> 0.0) + pry (>= 0.13.0) + shellany (~> 0.0) + thor (>= 0.18.1) + guard-compat (1.2.1) + guard-puma (0.8.0) + guard (~> 2.14) + guard-compat (~> 1.2) + puma (>= 4.0, < 7) + hanami (2.0.2) + bundler (>= 1.16, < 3) + dry-configurable (~> 1.0, < 2) + dry-core (~> 1.0, < 2) + dry-inflector (~> 1.0, < 2) + dry-logger (~> 1.0, < 2) + dry-monitor (~> 1.0, >= 1.0.1, < 2) + dry-system (~> 1.0, < 2) + hanami-cli (~> 2.0) + hanami-utils (~> 2.0) + zeitwerk (~> 2.6) + hanami-cli (2.0.2) + bundler (~> 2.1) + dry-cli (~> 1.0, < 2) + dry-files (~> 1.0, >= 1.0.1, < 2) + dry-inflector (~> 1.0, < 2) + rake (~> 13.0) + zeitwerk (~> 2.6) + hanami-controller (2.0.1) + dry-configurable (~> 1.0, < 2) + dry-core (~> 1.0) + hanami-utils (~> 2.0) + rack (~> 2.0) + zeitwerk (~> 2.6) + hanami-reloader (2.0.2) + hanami-cli (~> 2.0) + zeitwerk (~> 2.6) + hanami-router (2.0.2) + mustermann (~> 3.0) + mustermann-contrib (~> 3.0) + rack (~> 2.0) + hanami-rspec (2.0.1) + hanami-cli (~> 2.0) + rake (~> 13.0) + rspec (~> 3.12) + zeitwerk (~> 2.6) + hanami-utils (2.0.2) + concurrent-ruby (~> 1.0) + dry-core (~> 1.0, < 2) + dry-transformer (~> 1.0, < 2) + hanami-validations (2.0.1) + dry-validation (>= 1.10, < 2) + zeitwerk (~> 2.6.0) + hansi (0.2.1) + httparty (0.21.0) + mini_mime (>= 1.0.0) + multi_xml (>= 0.5.2) + i18n (1.12.0) + concurrent-ruby (~> 1.0) + ice_nine (0.11.2) + json (2.6.3) + language_server-protocol (3.17.0.2) + listen (3.7.1) + rb-fsevent (~> 0.10, >= 0.10.3) + rb-inotify (~> 0.9, >= 0.9.10) + lumberjack (1.2.8) + method_source (1.0.0) + mini_mime (1.1.2) + multi_xml (0.6.0) + mustermann (3.0.0) + ruby2_keywords (~> 0.0.1) + mustermann-contrib (3.0.0) + hansi (~> 0.2.0) + mustermann (= 3.0.0) + nenv (0.3.0) + net-scp (4.0.0) + net-ssh (>= 2.6.5, < 8.0.0) + net-ssh (7.0.1) + nio4r (2.5.8) + notiffany (0.1.3) + nenv (~> 0.1) + shellany (~> 0.0) + parallel (1.22.1) + parser (3.2.0.0) + ast (~> 2.4.1) + pg (1.4.5) + pry (0.14.1) + coderay (~> 1.1) + method_source (~> 1.0) + puma (6.0.1) + nio4r (~> 2.0) + rack (2.2.5) + rack-test (2.0.2) + rack (>= 1.3) + rainbow (3.1.1) + rake (13.0.6) + rb-fsevent (0.11.2) + rb-inotify (0.10.1) + ffi (~> 1.0) + redcarpet (3.5.1) + regexp_parser (2.6.1) + rexml (3.2.5) + rom (5.3.0) + rom-changeset (~> 5.3, >= 5.3.0) + rom-core (~> 5.3, >= 5.3.0) + rom-repository (~> 5.3, >= 5.3.0) + rom-changeset (5.3.0) + dry-core (~> 1.0) + rom-core (~> 5.3) + transproc (~> 1.0, >= 1.1.0) + rom-core (5.3.0) + concurrent-ruby (~> 1.1) + dry-configurable (~> 1.0) + dry-core (~> 1.0) + dry-inflector (~> 1.0) + dry-initializer (~> 3.0, >= 3.0.1) + dry-struct (~> 1.0) + dry-types (~> 1.6) + transproc (~> 1.0, >= 1.1.0) + rom-factory (0.11.0) + dry-configurable (~> 1.0) + dry-core (~> 1.0) + dry-struct (~> 1.6) + faker (>= 2.0, < 3.0) + rom-core (~> 5.3) + rom-repository (5.3.0) + dry-core (~> 1.0) + dry-initializer (~> 3.0, >= 3.0.1) + rom-core (~> 5.3, >= 5.3.0) + rom-sql (3.6.1) + dry-core (~> 1.0) + dry-types (~> 1.0) + rom (~> 5.2, >= 5.2.1) + sequel (>= 4.49) + rspec (3.12.0) + rspec-core (~> 3.12.0) + rspec-expectations (~> 3.12.0) + rspec-mocks (~> 3.12.0) + rspec-core (3.12.0) + rspec-support (~> 3.12.0) + rspec-expectations (3.12.1) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.12.0) + rspec-mocks (3.12.1) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.12.0) + rspec-support (3.12.0) + rubocop (1.42.0) + json (~> 2.3) + parallel (~> 1.10) + parser (>= 3.1.2.1) + rainbow (>= 2.2.2, < 4.0) + regexp_parser (>= 1.8, < 3.0) + rexml (>= 3.2.5, < 4.0) + rubocop-ast (>= 1.24.1, < 2.0) + ruby-progressbar (~> 1.7) + unicode-display_width (>= 1.4.0, < 3.0) + rubocop-ast (1.24.1) + parser (>= 3.1.1.0) + rubocop-performance (1.15.2) + rubocop (>= 1.7.0, < 2.0) + rubocop-ast (>= 0.4.0) + ruby-progressbar (1.11.0) + ruby2_keywords (0.0.5) + sequel (5.63.0) + shellany (0.0.1) + slim (4.1.0) + temple (>= 0.7.6, < 0.9) + tilt (>= 2.0.6, < 2.1) + sshkit (1.21.3) + net-scp (>= 1.1.2) + net-ssh (>= 2.8.0) + standard (1.21.1) + language_server-protocol (~> 3.17.0.2) + rubocop (= 1.42.0) + rubocop-performance (= 1.15.2) + standardrb (1.0.1) + standard + temple (0.8.2) + thor (1.2.1) + tilt (2.0.11) + timecop (0.9.6) + transproc (1.1.1) + unicode-display_width (2.4.2) + zeitwerk (2.6.6) + +PLATFORMS + x86_64-darwin-20 + x86_64-darwin-22 + +DEPENDENCIES + babosa + builder + capistrano (~> 3.17) + database_cleaner-sequel + dotenv + dry-matcher + dry-monads + dry-types + guard-puma (~> 0.8) + hanami (~> 2.0.0) + hanami-controller (~> 2.0.0) + hanami-reloader + hanami-router (~> 2.0.0) + hanami-rspec + hanami-validations (~> 2.0.0) + hanami-view! + httparty + pg + puma + rack-test + rake + redcarpet + rexml + rom-factory + rom-sql + slim + standardrb + timecop + +BUNDLED WITH + 2.3.22 diff --git a/Guardfile b/Guardfile new file mode 100644 index 0000000..7c4504a --- /dev/null +++ b/Guardfile @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +group :server do + guard "puma", port: ENV.fetch("HANAMI_PORT", 2300) do + watch(%r{config/*}) + watch(%r{lib/*}) + watch(%r{app/*}) + watch(%r{slices/*}) + end +end diff --git a/Procfile.dev b/Procfile.dev new file mode 100644 index 0000000..8648a09 --- /dev/null +++ b/Procfile.dev @@ -0,0 +1,2 @@ +web: bundle exec hanami server +css: npx tailwindcss -i ./app/assets/index.css -o ./public/assets/index.css --watch \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..6df81d7 --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +# Adamantium diff --git a/Rakefile b/Rakefile new file mode 100644 index 0000000..c526f38 --- /dev/null +++ b/Rakefile @@ -0,0 +1,3 @@ +# frozen_string_literal: true + +require "hanami/rake_tasks" diff --git a/app/action.rb b/app/action.rb new file mode 100644 index 0000000..2c7ab02 --- /dev/null +++ b/app/action.rb @@ -0,0 +1,62 @@ +# auto_register: false +# frozen_string_literal: true + +require "hanami/action" +require "httparty" + +module Adamantium + class Action < Hanami::Action + include Deps["logger", "settings", not_found_view: "views.not_found"] + + handle_exception ROM::TupleCountMismatchError => :not_found + + def authenticate!(req, res) + if Hanami.env == :development || Hanami.env == :test + req.env[:scopes] = verify_token(nil) + return true + end + + # Pull out and verify the authorization header or access_token + if req.env["HTTP_AUTHORIZATION"] + header = req.env["HTTP_AUTHORIZATION"].match(/Bearer (.*)$/) + access_token = header[1] unless header.nil? + elsif req.params["access_token"] + access_token = req.params["access_token"] + else + logger.error "Received request without a token" + halt 401 + end + + req.env[:access_token] = access_token + + # Verify the token and extract scopes + req.env[:scopes] = verify_token(access_token) + end + + def not_found(_req, res, _exception) + res.render not_found_view + end + + def verify_scope(req:, scope:) + req.env[:scopes].include? scope + end + + private + + def verify_token(access_token) + return %i[create update delete undelete media] if Hanami.env == :development || Hanami.env == :test + + resp = HTTParty.get(settings.micropub_token_endpoint, { + headers: { + "Accept" => "application/x-www-form-urlencoded", + "Authorization" => "Bearer #{access_token}" + } + }) + decoded_response = URI.decode_www_form(resp.body).to_h.transform_keys(&:to_sym) + + halt 401 unless (decoded_response.include? :scope) && (decoded_response.include? :me) + + decoded_response[:scope].gsub(/post/, "create").split.map(&:to_sym) + end + end +end diff --git a/app/actions/.keep b/app/actions/.keep new file mode 100644 index 0000000..e69de29 diff --git a/app/actions/bookmarks/index.rb b/app/actions/bookmarks/index.rb new file mode 100644 index 0000000..0c3c059 --- /dev/null +++ b/app/actions/bookmarks/index.rb @@ -0,0 +1,13 @@ +module Adamantium + module Actions + module Bookmarks + class Index < Action + include Deps["views.bookmarks.index"] + + def handle(req, res) + res.render index, query: req.params[:q] + end + end + end + end +end diff --git a/app/actions/bookmarks/show.rb b/app/actions/bookmarks/show.rb new file mode 100644 index 0000000..e9e8ce0 --- /dev/null +++ b/app/actions/bookmarks/show.rb @@ -0,0 +1,12 @@ +module Adamantium + module Actions + module Bookmarks + class Show < Action + include Deps["views.bookmarks.show"] + def handle(req, res) + res.render show, slug: req.params[:slug] + end + end + end + end +end diff --git a/app/actions/feeds/rss.rb b/app/actions/feeds/rss.rb new file mode 100644 index 0000000..79fbe95 --- /dev/null +++ b/app/actions/feeds/rss.rb @@ -0,0 +1,14 @@ +module Adamantium + module Actions + module Feeds + class Rss < Action + include Deps["views.feeds.rss"] + + def handle(req, res) + res.content_type = "application/rss+xml" + res.render rss, format: :xml + end + end + end + end +end diff --git a/app/actions/key/show.rb b/app/actions/key/show.rb new file mode 100644 index 0000000..973690e --- /dev/null +++ b/app/actions/key/show.rb @@ -0,0 +1,13 @@ +module Adamantium + module Actions + module Key + class Show < Action + include Deps["settings"] + def handle(req, res) + res.content_type = "text/plain" + res.body = settings.micropub_pub_key + end + end + end + end +end diff --git a/app/actions/media/create.rb b/app/actions/media/create.rb new file mode 100644 index 0000000..2fc9dff --- /dev/null +++ b/app/actions/media/create.rb @@ -0,0 +1,28 @@ +module Adamantium + module Actions + module Media + class Create < Action + before :authenticate + + include Deps["commands.media.upload"] + + def handle(req, res) + data = req.params[:file] + + halt 401 if verify_scope(req: req, scope: :media) + + upload(file: data) do |m| + m.failure do |v| + res.status = 422 + end + + m.success do |v| + res.status = 201 + res.body = v + end + end + end + end + end + end +end diff --git a/app/actions/pages/show.rb b/app/actions/pages/show.rb new file mode 100644 index 0000000..77e73fe --- /dev/null +++ b/app/actions/pages/show.rb @@ -0,0 +1,15 @@ +module Adamantium + module Actions + module Pages + class Show < Action + include Deps["views.pages.show"] + + def handle(req, res) + slug = req.params[:slug] + + res.render show, slug: slug + end + end + end + end +end diff --git a/app/actions/posts/handle.rb b/app/actions/posts/handle.rb new file mode 100644 index 0000000..499df30 --- /dev/null +++ b/app/actions/posts/handle.rb @@ -0,0 +1,66 @@ +require "pry" + +module Adamantium + module Actions + module Posts + class Handle < Action + before :authenticate! + + include Deps[ + "settings", + post_param_parser: "param_parser.micropub_post", + create_resolver: "commands.posts.creation_resolver", + delete_post: "commands.posts.delete", + undelete_post: "commands.posts.undelete", + update_post: "commands.posts.update" + ] + + def handle(req, res) + req_entity = post_param_parser.call(params: req.params.to_h) + action = req.params[:action] + + if action + operation, permission_check = resolve_operation(action) + + if permission_check.call(req) + operation.call(params: req.params.to_h) + res.status = 200 + else + res.status = 401 + end + elsif req_entity + halt 401 unless verify_scope(req: req, scope: :create) + + command, contract = create_resolver.call(entry_type: req_entity).values_at(:command, :validation) + + validation = contract.call(req_entity.to_h) + if validation.success? + post = command.call(validation.to_h) + + res.status = 201 + res.headers.merge!( + "Location" => "#{settings.micropub_site_url}/#{post.post_type}/#{post.slug}" + ) + else + res.body = {error: validation.errors.to_h}.to_json + res.status = 422 + end + end + end + + private + + def resolve_operation(action) + case action + when "delete" + [delete_post, ->(req) { verify_scope(req: req, scope: :delete) }] + when "undelete" + [undelete_post, ->(req) { verify_scope(req: req, scope: :undelete) }] + when "update" + [update_post, ->(req) { verify_scope(req: req, scope: :update) }] + end + end + end + end + end +end diff --git a/app/actions/posts/index.rb b/app/actions/posts/index.rb new file mode 100644 index 0000000..c83d4eb --- /dev/null +++ b/app/actions/posts/index.rb @@ -0,0 +1,12 @@ +module Adamantium + module Actions + module Posts + class Index < Action + include Deps["views.posts.index"] + def handle(req, res) + res.render index + end + end + end + end +end diff --git a/app/actions/posts/show.rb b/app/actions/posts/show.rb new file mode 100644 index 0000000..cb66e58 --- /dev/null +++ b/app/actions/posts/show.rb @@ -0,0 +1,15 @@ +module Adamantium + module Actions + module Posts + class Show < Action + include Deps["views.posts.show"] + + def handle(req, res) + slug = req.params[:slug] + + res.render show, slug: slug + end + end + end + end +end diff --git a/app/actions/site/config.rb b/app/actions/site/config.rb new file mode 100644 index 0000000..0b1e68c --- /dev/null +++ b/app/actions/site/config.rb @@ -0,0 +1,31 @@ +module Adamantium + module Actions + module Site + class Config < Action + include Deps["settings", "views.site.home"] + + def handle(req, res) + if req.params[:q] == "config" + res.status = 200 + res.content_type = "Application/JSON" + res.body = { + "media-endpoint" => settings.micropub_media_endpoint, + "destination" => [ + {uid: settings.micropub_site_id, name: settings.micropub_site_name} + ], + "post-types" => [ + {type: "note", name: "Note", properties: %w[content category]}, + {type: "article", name: "Article", properties: %w[name content category]}, + {type: "photo", name: "Photo", properties: %w[name content category]}, + {type: "bookmark", name: "Bookmark", properties: %w[name content category]} + ], + "syndicate-to" => [] + }.to_json + else + res.render home + end + end + end + end + end +end diff --git a/app/actions/site/home.rb b/app/actions/site/home.rb new file mode 100644 index 0000000..e37690e --- /dev/null +++ b/app/actions/site/home.rb @@ -0,0 +1,12 @@ +module Adamantium + module Actions + module Site + class Home < Action + include Deps["views.site.home"] + def handle(req, res) + res.render home + end + end + end + end +end diff --git a/app/actions/tags/show.rb b/app/actions/tags/show.rb new file mode 100644 index 0000000..5dfbe08 --- /dev/null +++ b/app/actions/tags/show.rb @@ -0,0 +1,15 @@ +module Adamantium + module Actions + module Tags + class Show < Action + include Deps["views.tags.show"] + + def handle(req, res) + slug = req.params[:slug] + + res.render show, slug: slug + end + end + end + end +end diff --git a/app/assets/index.css b/app/assets/index.css new file mode 100644 index 0000000..f18b239 --- /dev/null +++ b/app/assets/index.css @@ -0,0 +1,27 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; +@tailwind typography; + +@font-face { + font-family: "Rubik"; + src: url("/assets/Rubik-VariableFont_wght.ttf") format("truetype"); +} + +@font-face { + font-family: "JetBrainsMono"; + src: url("/assets/JetBrainsMono-VariableFont_wght.ttf") format("truetype"); +} + +* { + font-family: "Rubik", Helvetica, Arial, sans-serif; +} + +.gist tr { + border-bottom: none; +} + +.gist span { + font-family: "JetBrainsMono", Monaco, monospace; +} + diff --git a/app/commands/media/upload.rb b/app/commands/media/upload.rb new file mode 100644 index 0000000..2bf63b8 --- /dev/null +++ b/app/commands/media/upload.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +module Adamantium + module Commands + module Media + class Upload < Command + include Deps["settings"] + + def call(file:) + pathname = Time.now.strftime("%m-%Y") + + filename = file[:filename].split("/").last + + dirname = File.join("public", "media", pathname) + + unless File.directory?(dirname) + FileUtils.mkdir_p(dirname) + end + + begin + File.write(File.join(dirname, filename), file[:tempfile].read) + rescue Errno::ENOENT, NoMethodError => e + return Failure(e.message) + end + + upload_path = File.join(settings.micropub_site_url, "/media/", "/#{pathname}/", filename).to_s + Success(upload_path) + end + end + end + end +end diff --git a/app/commands/posts/create_bookmark.rb b/app/commands/posts/create_bookmark.rb new file mode 100644 index 0000000..04fef73 --- /dev/null +++ b/app/commands/posts/create_bookmark.rb @@ -0,0 +1,12 @@ +module Adamantium + module Commands + module Posts + class CreateBookmark < Command + include Deps["repos.post_repo"] + def call(bookmark) + post_repo.create(bookmark) + end + end + end + end +end diff --git a/app/commands/posts/create_entry.rb b/app/commands/posts/create_entry.rb new file mode 100644 index 0000000..c3b3314 --- /dev/null +++ b/app/commands/posts/create_entry.rb @@ -0,0 +1,18 @@ +module Adamantium + module Commands + module Posts + class CreateEntry < Command + include Deps["repos.post_repo", + "post_utilities.slugify", + renderer: "renderers.markdown" + ] + def call(post) + attrs = post.to_h + attrs[:content] = renderer.call(content: attrs[:content]) + + post_repo.create(attrs) + end + end + end + end +end diff --git a/app/commands/posts/creation_resolver.rb b/app/commands/posts/creation_resolver.rb new file mode 100644 index 0000000..36a1de7 --- /dev/null +++ b/app/commands/posts/creation_resolver.rb @@ -0,0 +1,23 @@ +module Adamantium + module Commands + module Posts + class CreationResolver + include Deps[ + "validation.posts.post_contract", + "validation.posts.bookmark_contract", + "commands.posts.create_entry", + "commands.posts.create_bookmark" + ] + + def call(entry_type:) + case entry_type + in Entities::BookmarkRequest + {command: create_bookmark, validation: bookmark_contract} + else + {command: create_entry, validation: post_contract} + end + end + end + end + end +end diff --git a/app/commands/posts/delete.rb b/app/commands/posts/delete.rb new file mode 100644 index 0000000..6bfff30 --- /dev/null +++ b/app/commands/posts/delete.rb @@ -0,0 +1,14 @@ +module Adamantium + module Commands + module Posts + class Delete < Command + include Deps["repos.post_repo"] + def call(params:) + slug = URI(params[:url]).path.split("/").last + + post_repo.delete!(slug) + end + end + end + end +end diff --git a/app/commands/posts/undelete.rb b/app/commands/posts/undelete.rb new file mode 100644 index 0000000..40086a7 --- /dev/null +++ b/app/commands/posts/undelete.rb @@ -0,0 +1,14 @@ +module Adamantium + module Commands + module Posts + class Undelete < Command + include Deps["repos.post_repo"] + def call(params:) + slug = URI(params[:url]).path.split("/").last + + post_repo.restore!(slug) + end + end + end + end +end diff --git a/app/commands/posts/update.rb b/app/commands/posts/update.rb new file mode 100644 index 0000000..0cc099e --- /dev/null +++ b/app/commands/posts/update.rb @@ -0,0 +1,13 @@ +module Adamantium + module Commands + module Posts + class Update < Command + def call(params) + slug = URI(params[:url]).path.split("/").last + + post_repo.update(slug, params) + end + end + end + end +end diff --git a/app/content/home.md b/app/content/home.md new file mode 100644 index 0000000..dda65f6 --- /dev/null +++ b/app/content/home.md @@ -0,0 +1,2 @@ +Hi! đź‘‹ I'm Daniel, a software engineer living in Canberra, Australia. + diff --git a/app/content/pages/about.md b/app/content/pages/about.md new file mode 100644 index 0000000..cb71538 --- /dev/null +++ b/app/content/pages/about.md @@ -0,0 +1,17 @@ +# About + +Hi, I'm _Daniel_. + +I've been in the software / web industry for around 10 years. I'm currently a technical lead at [Culture Amp](https://cultureamp.com), where I lead a small team working in a Rails monolith and across many microservices. Previously I worked with the wonderful humans of [Icelab](https://icelab.com.au) on a wide range of interesting and valuable projects. + +I currently live in Canberra with my partner and [our dogs](https://instagram.com/barkly_and_crumpet). + +In my spare time I like to tinker on various Ruby projects (including the software that powers this blog), make things with [Processing](https://processing.org), explore the various walking tracks around our house and potter around in the garden. + +### Contact me + +- [Email](mailto:hello@dnitza.com) +- [Mastodon](https://social.dnitza.com/@daniel) +- [Github](https://github.com/dnitza) + + diff --git a/app/content/pages/art-things.md b/app/content/pages/art-things.md new file mode 100644 index 0000000..ffeb345 --- /dev/null +++ b/app/content/pages/art-things.md @@ -0,0 +1 @@ +# Art things diff --git a/app/decorators/bookmarks/decorator.rb b/app/decorators/bookmarks/decorator.rb new file mode 100644 index 0000000..312dc55 --- /dev/null +++ b/app/decorators/bookmarks/decorator.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: false + +# auto_register: false + +module Adamantium + module Decorators + module Bookmarks + class Decorator < SimpleDelegator + def display_published_at + published_at.strftime("%e %B, %Y") + end + end + end + end +end diff --git a/app/decorators/posts/decorator.rb b/app/decorators/posts/decorator.rb new file mode 100644 index 0000000..43033a3 --- /dev/null +++ b/app/decorators/posts/decorator.rb @@ -0,0 +1,73 @@ +# frozen_string_literal: false + +# auto_register: false + +require "rexml/parsers/pullparser" + +module Adamantium + module Decorators + module Posts + class Decorator < SimpleDelegator + def prefix_emoji + name ? "📝" : "📯" + end + + def display_title + title = name || published_at.strftime("%D") + "#{prefix_emoji} #{title}" + end + + def display_published_at + published_at.strftime("%e %B, %Y") + end + + def machine_published_at + published_at.rfc2822 + end + + def excerpt + truncate_html(content, 140, true) + end + + private + + def truncate_html(content, len = 30, at_end = nil) + p = REXML::Parsers::PullParser.new(content) + tags = [] + new_len = len + results = "" + while p.has_next? && new_len > 0 + p_e = p.pull + case p_e.event_type + when :start_element + tags.push p_e[0] + results << "<#{tags.last}#{attrs_to_s(p_e[1])}>" + when :end_element + results << "" + when :text + results << p_e[0][0..new_len] + new_len -= p_e[0].length + else + results << "" + end + end + if at_end + results << "..." + end + tags.reverse_each do |tag| + results << "" + end + results + end + + def attrs_to_s(attrs) + if attrs.empty? + "" + else + " " + attrs.to_a.map { |attr| %(#{attr[0]}="#{attr[1]}") }.join(" ") + end + end + end + end + end +end diff --git a/app/entities/bookmark_request.rb b/app/entities/bookmark_request.rb new file mode 100644 index 0000000..ff926c6 --- /dev/null +++ b/app/entities/bookmark_request.rb @@ -0,0 +1,15 @@ +module Adamantium + module Entities + class BookmarkRequest < Dry::Struct + attribute :h, Types::Coercible::String + attribute :action, Types::Coercible::String.optional + attribute :name, Types::Coercible::String + attribute :content, Types::Coercible::String.optional + attribute :url, Types::Coercible::String + attribute :slug, Types::Coercible::String + attribute :category, Types::Array.of(Types::Coercible::String) + attribute :published_at, Types::Nominal::DateTime.optional + attribute :post_type, Types::Coercible::String + end + end +end diff --git a/app/entities/post_request.rb b/app/entities/post_request.rb new file mode 100644 index 0000000..fba64a8 --- /dev/null +++ b/app/entities/post_request.rb @@ -0,0 +1,14 @@ +module Adamantium + module Entities + class PostRequest < Dry::Struct + attribute :h, Types::Coercible::String + attribute :action, Types::Coercible::String.optional + attribute :name, Types::Coercible::String.optional + attribute :content, Types::Coercible::String + attribute :slug, Types::Coercible::String + attribute :category, Types::Array.of(Types::Coercible::String) + attribute :published_at, Types::Nominal::DateTime.optional + attribute :post_type, Types::Coercible::String + end + end +end diff --git a/app/repos/post_repo.rb b/app/repos/post_repo.rb new file mode 100644 index 0000000..43f8f3e --- /dev/null +++ b/app/repos/post_repo.rb @@ -0,0 +1,84 @@ +module Adamantium + module Repos + class PostRepo < Adamantium::Repo[:posts] + commands :update + + def create(post_attrs) + posts.transaction do + new_post = posts.changeset(:create, post_attrs).commit + + post_attrs[:category].each do |tag_name| + next if tag_name == "" + + tag = posts.tags.where(label: tag_name).one || + posts + .tags + .changeset(:create, {label: tag_name, slug: tag_name.downcase.strip.tr(" ", "-").gsub(/[^\w-]/, "")}) + .commit + + posts.post_tags.changeset(:create, { + post_id: new_post.id, + tag_id: tag[:id] + }) + .commit + end + + new_post + end + end + + def post_listing(limit: nil) + posts + .where(post_type: "post") + .published + .combine(:tags) + .order(Sequel.desc(:published_at)) + .limit(limit) + .to_a + end + + def bookmark_listing(query: nil) + base = posts + .where(post_type: "bookmark") + .published + .combine(:tags) + .order(Sequel.desc(:published_at)) + + query ? base.where(Sequel.ilike(:name, "%#{query}%")).to_a : base.to_a + end + + def for_rss + posts + .where(post_type: "post") + .published + .combine(:tags) + .order(Sequel.desc(:published_at)) + .to_a + end + + def fetch!(slug) + posts + .published + .combine(:tags) + .where(slug: slug) + .one! + end + + def slug_exists?(slug) + !!posts + .where(slug: slug) + .one + end + + def delete!(slug) + delete_post = posts.where(slug: slug).command(:update) + delete_post.call(published_at: nil) + end + + def restore!(slug) + delete_post = posts.where(slug: slug).command(:update) + delete_post.call(published_at: Time.now) + end + end + end +end diff --git a/app/repos/post_tag_repo.rb b/app/repos/post_tag_repo.rb new file mode 100644 index 0000000..3d79030 --- /dev/null +++ b/app/repos/post_tag_repo.rb @@ -0,0 +1,26 @@ +module Adamantium + module Repos + class PostTagRepo < Adamantium::Repo[:post_tags] + def posts_tagged(tag:) + tag_id = post_tags + .tags + .where(slug: tag) + .one! + .id + + post_ids = post_tags + .where(tag_id: tag_id) + .to_a + .map(&:post_id) + + post_tags + .posts + .where(id: post_ids) + .published + .combine(:tags) + .order(Sequel.desc(:published_at)) + .to_a + end + end + end +end diff --git a/app/repos/tag_repo.rb b/app/repos/tag_repo.rb new file mode 100644 index 0000000..9e9f338 --- /dev/null +++ b/app/repos/tag_repo.rb @@ -0,0 +1,9 @@ +module Adamantium + module Repos + class TagRepo < Adamantium::Repo[:tags] + def fetch!(slug) + tags.where(slug: slug).one! + end + end + end +end diff --git a/app/templates/bookmarks/index.html.slim b/app/templates/bookmarks/index.html.slim new file mode 100644 index 0000000..753c3ab --- /dev/null +++ b/app/templates/bookmarks/index.html.slim @@ -0,0 +1,13 @@ +div class="flex justify-between mb-12 prose dark:prose-invert max-w-prose mx-auto text-gray-800 dark:text-gray-200" + div class="basis-1/5" + h1 Bookmarks + + form class="basis-2/5" method="GET" action="/bookmarks" + input class="w-48 border-blue-400 border-2 rounded mr-2 px-2" id="seach" type="text" name="q" value=q + button class="w-16 border-blue-400 border-2 rounded bg-blue-400 hover:bg-blue-800 hover:border-blue-800 hover:text-blue-100 px-1 text-gray-200" type="submit" Search + +div class="mb-12 max-w-prose mx-auto" + - bookmarks.each do |bookmark| + == render :bookmark, bookmark: bookmark + +div class="max-w-screen-md mx-auto border-t-4 border-solid border-gray-400 dark:border-gray-600" diff --git a/app/templates/bookmarks/show.html.slim b/app/templates/bookmarks/show.html.slim new file mode 100644 index 0000000..b2124fa --- /dev/null +++ b/app/templates/bookmarks/show.html.slim @@ -0,0 +1,17 @@ +div class="mb-12 prose dark:prose-invert max-w-prose mx-auto text-gray-800 dark:text-gray-200" + h1 = bookmark.name + +div class="mb-12 prose max-w-prose mx-auto text-gray-800 dark:text-gray-200" + a class="text-blue-600 no-underline hover:underline" href=bookmark.url + p class="text-xl" + = bookmark.url + + == bookmark.content + +div class="mb-8 max-w-screen-md mx-auto border-t-4 border-solid border-gray-400 dark:border-gray-600" + +div class="mb-2 max-w-prose mx-auto text-gray-800 dark:text-gray-200 flex" + = "Published #{bookmark.display_published_at}" + span class="text-right flex-1" + == render :tags, tags: bookmark.tags + diff --git a/app/templates/feeds/rss.xml.builder b/app/templates/feeds/rss.xml.builder new file mode 100644 index 0000000..e2f024b --- /dev/null +++ b/app/templates/feeds/rss.xml.builder @@ -0,0 +1,20 @@ +xml.instruct!(:xml, version: "2.0", encoding: "utf-8") + +xml.channel do |channel| + channel.title "Daniel Nitsikopoulos" + channel.description "The RSS feed for https://dnitza.com" + channel.lastBuildDate Time.now.rfc2822 + channel.pubDate Time.now.rfc2822 + channel.ttl 1800 + + posts.each do |post| + channel.item do |item| + item.title post.display_title + item.description do |desc| + desc.cdata! post.content + end + item.guid(post.slug, isPermaLink: true) + item.pubDate post.machine_published_at + end + end +end diff --git a/app/templates/layouts/app.html.slim b/app/templates/layouts/app.html.slim new file mode 100644 index 0000000..09b3516 --- /dev/null +++ b/app/templates/layouts/app.html.slim @@ -0,0 +1,46 @@ +html + head + meta charest="utf-8" + + meta name="viewport" content="width=device-width, initial-scale=1.0" + + title Daniel Nitsikopoulos + + link rel="authorization_endpoint" href=Hanami.app.settings.micropub_authorization_endpoint + link rel="token_endpoint" href=Hanami.app.settings.micropub_token_endpoint + link rel="micropub" href="#{URI.join(Hanami.app.settings.micropub_site_url, "micropub")}" + + link rel="stylesheet" href="/assets/index.css" + + script data-domain="dnitza.com" src="https://stats.dnitza.com/js/script.js" defer="true" + + / script src="https://unpkg.com/htmx.org@1.8.4" integrity="sha384-wg5Y/JwF7VxGk4zLsJEcAojRtlVp1FKKdGy1qN+OMtdq72WRvX/EdRdqg/LOhYeV" crossorigin="anonymous" + + - if Hanami.app.settings.micropub_pub_key + link rel="pgpkey" href="/key" + body class="bg-white dark:bg-black selection:bg-blue-100 selection:text-blue-900 dark:selection:bg-blue-600 dark:selection:text-blue-100" + main class="pb-8 px-4 pt-4 md:pt-8" + header class="mb-12 max-w-screen-md mx-auto" + div class="flex items-center mb-8 md:mb-12 text-lg md:text-xl text-gray-400 dark:text-gray-600" + div class="flex-none mx-auto md:flex-auto md:mx-0" + div class="h-card flex items-center" + img class="u-photo w-6 h6 md:w-10 md:h-10 rounded-full mr-1.5" src="/assets/memoji.png" + a href="/" + h1 class="p-bane uppercase text-sm md:text-sm text-gray-400 dark:text-gray-400" = Hanami.app.settings.site_name + nav class="space-x-1 text-sm md:text-sm uppercase md:block" + a class="p-1 rounded text-gray-400 hover:bg-red-100 hover:text-red-400 dark:hover:bg-red-200 #{link_active?('about') ? 'text-red-600 dark:text-red-400' : ''}" href="/about" About + span class="text-gray-400 dark:text-gray-600" + = "/" + a class="p-1 rounded text-gray-400 hover:bg-blue-100 hover:text-blue-400 dark:hover:bg-blue-200" href="/posts" Writing + span class="text-gray-400 dark:text-gray-600" + = "/" + a class="p-1 rounded text-gray-400 hover:bg-yellow-100 hover:text-yellow-600 dark:hover:bg-yellow-200 #{link_active?('bookmarks') ? 'text-yellow-600 dark:text-yellow-600' : ''}" href="/bookmarks" Bookmarks + span class="text-gray-400 dark:text-gray-600" + = "/" + a class="p-1 rounded text-gray-400 hover:bg-orange-100 hover:text-orange-400 dark:hover:bg-orange-200" href="#{Hanami.app.settings.micropub_site_url}/feeds/rss" RSS + // span class="text-gray-400 dark:text-gray-600" + = "/" + // a class="text-green-600 hover:text-gray-800 dark:hover:text-gray-200" href="/art-things" Art things + == yield + div class="px-4 max-w-screen-md mx-auto pb-10" + p class="text-gray-200 dark:text-gray-600" © 2023 Daniel Nitsikopoulos. All rights reserved. diff --git a/app/templates/layouts/app.xml.builder b/app/templates/layouts/app.xml.builder new file mode 100644 index 0000000..e69de29 diff --git a/app/templates/not_found.html.slim b/app/templates/not_found.html.slim new file mode 100644 index 0000000..e87e6cc --- /dev/null +++ b/app/templates/not_found.html.slim @@ -0,0 +1,3 @@ +div class="mb-12 prose dark:prose-invert max-w-prose mx-auto text-gray-800 dark:text-gray-200" + h1 Not Found! + diff --git a/app/templates/pages/show.html.slim b/app/templates/pages/show.html.slim new file mode 100644 index 0000000..deba018 --- /dev/null +++ b/app/templates/pages/show.html.slim @@ -0,0 +1,4 @@ +article class="mb-12 prose dark:prose-invert max-w-prose mx-auto text-gray-800 dark:text-gray-200 prose-em:font-bold prose-em:not-italic prose-em:bg-blue-600 prose-em:px-1 prose-a:text-blue-600 prose-a:p-0.5 prose-a:rounded-sm prose-a:no-underline hover:prose-a:underline prose-em:text-blue-100" + == page_content + +div class="max-w-screen-md mx-auto border-t-4 border-solid border-gray-400 dark:border-gray-600" diff --git a/app/templates/posts/index.html.slim b/app/templates/posts/index.html.slim new file mode 100644 index 0000000..7f9c26f --- /dev/null +++ b/app/templates/posts/index.html.slim @@ -0,0 +1,8 @@ +div class="mb-12 prose dark:prose-invert max-w-prose mx-auto text-gray-800 dark:text-gray-200" + h1 Writing + +div class="mb-12 max-w-prose mx-auto" + - posts.each do |post| + == render :post, post: post + +div class="max-w-screen-md mx-auto border-t-4 border-solid border-gray-400 dark:border-gray-600" diff --git a/app/templates/posts/show.html.slim b/app/templates/posts/show.html.slim new file mode 100644 index 0000000..11c3f99 --- /dev/null +++ b/app/templates/posts/show.html.slim @@ -0,0 +1,12 @@ +div class="mb-12 prose dark:prose-invert max-w-prose mx-auto text-gray-800 dark:text-gray-200" + h1 = post.display_title + +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" + == post.content + +div class="mb-4 max-w-screen-md mx-auto border-t-4 border-solid border-gray-400 dark:border-gray-600" + +div class="mb-2 max-w-prose mx-auto text-gray-600 dark:text-gray-200 flex" + = "Published #{post.display_published_at}" + span class="text-right flex-1" + == render :tags, tags: post.tags diff --git a/app/templates/shared/_bookmark.html.slim b/app/templates/shared/_bookmark.html.slim new file mode 100644 index 0000000..2d3f7b6 --- /dev/null +++ b/app/templates/shared/_bookmark.html.slim @@ -0,0 +1,12 @@ +div class="mb-8" + h3 class="text-xl font-bold text-blue-600 hover:underline" + a href="#{bookmark.url}" + = "đź”– #{bookmark.name} " + == render("link_arrow") + p class="e-content leading-relaxed md:text-lg text-gray-800 dark:text-gray-200" + = bookmark.content + == render :tags, tags: bookmark.tags + + p class="text-sm u-url text-blue-400 hover:text-blue-600 dark:hover:text-blue-200" + a href="/bookmark/#{bookmark.slug}" + = bookmark.display_published_at diff --git a/app/templates/shared/_link_arrow.html.slim b/app/templates/shared/_link_arrow.html.slim new file mode 100644 index 0000000..2f5f18c --- /dev/null +++ b/app/templates/shared/_link_arrow.html.slim @@ -0,0 +1,4 @@ +svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="inline w-2 h2 md:w-4 md:h-4 mt-0.5" + g + rect height="14.4434" opacity="0" width="14.4238" x="0" y="0" + path d="M14.4238 10.8008L14.4141 0.976562C14.4141 0.419922 14.0527 0.0292969 13.4668 0.0292969L3.64258 0.0292969C3.0957 0.0292969 2.72461 0.449219 2.72461 0.917969C2.72461 1.38672 3.14453 1.78711 3.60352 1.78711L7.00195 1.78711L11.7676 1.63086L9.95117 3.22266L0.273438 12.9199C0.0976562 13.0957 0 13.3203 0 13.5352C0 14.0039 0.419922 14.4434 0.908203 14.4434C1.13281 14.4434 1.34766 14.3652 1.52344 14.1797L11.2207 4.49219L12.832 2.66602L12.6562 7.22656L12.6562 10.8398C12.6562 11.2988 13.0566 11.7285 13.5352 11.7285C14.0039 11.7285 14.4238 11.3281 14.4238 10.8008Z" \ No newline at end of file diff --git a/app/templates/shared/_post.html.slim b/app/templates/shared/_post.html.slim new file mode 100644 index 0000000..88d7f85 --- /dev/null +++ b/app/templates/shared/_post.html.slim @@ -0,0 +1,10 @@ +div class="mb-8" + h3 class="text-xl font-bold text-blue-600 hover:underline" + a href="/post/#{post.slug}" + = post.display_title + div class="text-base text-gray-800 dark:text-gray-200" + == post.excerpt + == render :tags, tags: post.tags + + p class="text-sm text-blue-400" + = post.display_published_at diff --git a/app/templates/shared/_tags.html.slim b/app/templates/shared/_tags.html.slim new file mode 100644 index 0000000..07787e6 --- /dev/null +++ b/app/templates/shared/_tags.html.slim @@ -0,0 +1,8 @@ +- if tags.count > 0 + div class="mb-2" + span class="text-sm text-gray-600 dark:text-gray-200" + = "Tagged: " + - tags.each do |tag| + span + a class="rounded p-1 mr-1 text-xsm u-url bg-yellow-200/60 hover:bg-yellow-200 dark:bg-yellow-400 dark:hover:bg-yellow-400/80 dark:text-yellow-800 dark:hover:text-yellow-100 text-gray-600" href="/tagged/#{tag.slug}" + = tag.label diff --git a/app/templates/site/home.html.slim b/app/templates/site/home.html.slim new file mode 100644 index 0000000..2975768 --- /dev/null +++ b/app/templates/site/home.html.slim @@ -0,0 +1,14 @@ +div class="mb-12 max-w-prose mx-auto text-gray-800 dark:text-gray-200" + == home_content + +div class="mb-8 max-w-screen-md mx-auto border-t-4 border-solid border-gray-400 dark:border-gray-600" + +div class="mb-4 flex max-w-prose mx-auto" + h2 class="text-xl text-gray-600 dark:text-gray-200" Recent + a class="text-right flex-1 text-blue-400" href="/posts" See all → + +div class="mb-12 max-w-prose mx-auto" + - posts.each do |post| + == render :post, post: post + +div class="max-w-screen-md mx-auto border-t-4 border-solid border-gray-400 dark:border-gray-600" diff --git a/app/templates/tags/show.html.slim b/app/templates/tags/show.html.slim new file mode 100644 index 0000000..6302783 --- /dev/null +++ b/app/templates/tags/show.html.slim @@ -0,0 +1,8 @@ +div class="mb-12 prose dark:prose-invert max-w-prose mx-auto text-gray-800 dark:text-gray-200" + h1 = "Tagged: \"#{tag.label}\"" + +div class="mb-12 max-w-prose mx-auto" + - posts.each do |post| + == render post.post_type.to_sym, post.post_type.to_sym => post + +div class="max-w-screen-md mx-auto border-t-4 border-solid border-gray-400 dark:border-gray-600" diff --git a/app/validation/posts/bookmark_contract.rb b/app/validation/posts/bookmark_contract.rb new file mode 100644 index 0000000..d5c8b84 --- /dev/null +++ b/app/validation/posts/bookmark_contract.rb @@ -0,0 +1,17 @@ +module Adamantium + module Validation + module Posts + class BookmarkContract < Dry::Validation::Contract + params do + required(:name).filled(:string) + required(:content).maybe(:string) + required(:category).array(:string) + required(:published_at).maybe(:time) + required(:url).filled(:string) + required(:slug).filled(:string) + required(:post_type).value(included_in?: %w[bookmark]) + end + end + end + end +end diff --git a/app/validation/posts/post_contract.rb b/app/validation/posts/post_contract.rb new file mode 100644 index 0000000..547de1b --- /dev/null +++ b/app/validation/posts/post_contract.rb @@ -0,0 +1,16 @@ +module Adamantium + module Validation + module Posts + class PostContract < Dry::Validation::Contract + params do + required(:name).maybe(:string) + required(:content).filled(:string) + required(:category).array(:string) + required(:published_at).maybe(:time) + required(:slug).filled(:string) + required(:post_type).value(included_in?: %w[post]) + end + end + end + end +end diff --git a/app/views/bookmarks/index.rb b/app/views/bookmarks/index.rb new file mode 100644 index 0000000..76edff1 --- /dev/null +++ b/app/views/bookmarks/index.rb @@ -0,0 +1,19 @@ +module Adamantium + module Views + module Bookmarks + class Index < Adamantium::View + include Deps["repos.post_repo"] + + expose :bookmarks do |query:| + post_repo.bookmark_listing(query: query).map do |bookmark| + Decorators::Bookmarks::Decorator.new bookmark + end + end + + expose :q do |query:| + query + end + end + end + end +end diff --git a/app/views/bookmarks/show.rb b/app/views/bookmarks/show.rb new file mode 100644 index 0000000..7bdd4e6 --- /dev/null +++ b/app/views/bookmarks/show.rb @@ -0,0 +1,13 @@ +module Adamantium + module Views + module Bookmarks + class Show < Adamantium::View + include Deps["repos.post_repo"] + + expose :bookmark do |slug:| + Decorators::Bookmarks::Decorator.new(post_repo.fetch!(slug)) + end + end + end + end +end diff --git a/app/views/feeds/rss.rb b/app/views/feeds/rss.rb new file mode 100644 index 0000000..5f010b5 --- /dev/null +++ b/app/views/feeds/rss.rb @@ -0,0 +1,23 @@ +require "builder" + +module Adamantium + module Views + module Feeds + class Rss < Adamantium::View + include Deps["repos.post_repo"] + + expose :posts do + post_repo.for_rss.map do |post| + Decorators::Posts::Decorator.new post + end + end + + expose :xml, decorate: false, layout: true + + def xml + Builder::XmlMarkup.new(indent: 2) + end + end + end + end +end diff --git a/app/views/not_found.rb b/app/views/not_found.rb new file mode 100644 index 0000000..9142fc7 --- /dev/null +++ b/app/views/not_found.rb @@ -0,0 +1,6 @@ +module Adamantium + module Views + class NotFound < View + end + end +end diff --git a/app/views/pages/show.rb b/app/views/pages/show.rb new file mode 100644 index 0000000..97af299 --- /dev/null +++ b/app/views/pages/show.rb @@ -0,0 +1,17 @@ +module Adamantium + module Views + module Pages + class Show < Adamantium::View + include Deps[renderer: "renderers.markdown"] + + expose :page_content do |slug:| + markdown_content = File.read("app/content/pages/#{slug}.md") + + renderer.call(content: markdown_content) + rescue Errno::ENOENT + renderer.call(content: "## Page not found") + end + end + end + end +end diff --git a/app/views/posts/index.rb b/app/views/posts/index.rb new file mode 100644 index 0000000..5c20828 --- /dev/null +++ b/app/views/posts/index.rb @@ -0,0 +1,15 @@ +module Adamantium + module Views + module Posts + class Index < Adamantium::View + include Deps["repos.post_repo"] + + expose :posts do + post_repo.post_listing.map do |post| + Decorators::Posts::Decorator.new(post) + end + end + end + end + end +end diff --git a/app/views/posts/show.rb b/app/views/posts/show.rb new file mode 100644 index 0000000..4398cf8 --- /dev/null +++ b/app/views/posts/show.rb @@ -0,0 +1,13 @@ +module Adamantium + module Views + module Posts + class Show < Adamantium::View + include Deps["repos.post_repo"] + + expose :post do |slug:| + Decorators::Posts::Decorator.new(post_repo.fetch!(slug)) + end + end + end + end +end diff --git a/app/views/site/home.rb b/app/views/site/home.rb new file mode 100644 index 0000000..0504721 --- /dev/null +++ b/app/views/site/home.rb @@ -0,0 +1,21 @@ +module Adamantium + module Views + module Site + class Home < Adamantium::View + include Deps["repos.post_repo", renderer: "renderers.markdown"] + + expose :home_content do + markdown_content = File.read("app/content/home.md") + + renderer.call(content: markdown_content) + end + + expose :posts do + post_repo.post_listing(limit: 10).map do |post| + Decorators::Posts::Decorator.new(post) + end + end + end + end + end +end diff --git a/app/views/tags/show.rb b/app/views/tags/show.rb new file mode 100644 index 0000000..f311438 --- /dev/null +++ b/app/views/tags/show.rb @@ -0,0 +1,22 @@ +module Adamantium + module Views + module Tags + class Show < Adamantium::View + include Deps[ + "repos.post_tag_repo", + "repos.tag_repo" + ] + + expose :posts do |slug:| + post_tag_repo.posts_tagged(tag: slug).map do |post| + Decorators::Posts::Decorator.new(post) + end + end + + expose :tag do |slug:| + tag_repo.fetch!(slug) + end + end + end + end +end diff --git a/bin/hanami b/bin/hanami new file mode 100755 index 0000000..b890cb6 --- /dev/null +++ b/bin/hanami @@ -0,0 +1,29 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +require "bundler/setup" +require "hanami/cli" + +# Hanami 2.0 does does not officially include database integration. However, much of the required +# work is already done and included in the gem. +# +# This CLI shim activates the database commands so we can manage our app database. + +Hanami::CLI.tap do |cli| + cli.register "db create", Hanami::CLI::Commands::App::DB::Create + cli.register "db create_migration", Hanami::CLI::Commands::App::DB::CreateMigration + cli.register "db drop", Hanami::CLI::Commands::App::DB::Drop + cli.register "db migrate", Hanami::CLI::Commands::App::DB::Migrate + cli.register "db setup", Hanami::CLI::Commands::App::DB::Setup + cli.register "db reset", Hanami::CLI::Commands::App::DB::Reset + cli.register "db rollback", Hanami::CLI::Commands::App::DB::Rollback + cli.register "db sample_data", Hanami::CLI::Commands::App::DB::SampleData + cli.register "db seed", Hanami::CLI::Commands::App::DB::Seed + cli.register "db structure dump", Hanami::CLI::Commands::App::DB::Structure::Dump + cli.register "db version", Hanami::CLI::Commands::App::DB::Version +end + +Hanami::CLI::Bundler.require(:cli) + +cli = Dry::CLI.new(Hanami::CLI) +cli.call \ No newline at end of file diff --git a/config.ru b/config.ru new file mode 100644 index 0000000..4e620c3 --- /dev/null +++ b/config.ru @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +require "hanami/boot" + +use Rack::Static, urls: ["/assets", "/media"], root: "public" + +run Hanami.app diff --git a/config/app.rb b/config/app.rb new file mode 100644 index 0000000..4585a0d --- /dev/null +++ b/config/app.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +require "hanami" + +module Adamantium + class App < Hanami::App + config.actions.content_security_policy[:script_src] += " https://gist.github.com" + config.actions.content_security_policy[:script_src] += " *.dnitza.com" + config.actions.content_security_policy[:connect_src] += " https://stats.dnitza.com/api/event" + end +end diff --git a/config/nginx.conf b/config/nginx.conf new file mode 100644 index 0000000..e69de29 diff --git a/config/providers/param_parser.rb b/config/providers/param_parser.rb new file mode 100644 index 0000000..271bd7a --- /dev/null +++ b/config/providers/param_parser.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +Hanami.app.register_provider :param_parser, namespace: true do + start do + register "micropub_post", Adamantium::MicropubRequestParser.new + end +end diff --git a/config/providers/persistence.rb b/config/providers/persistence.rb new file mode 100644 index 0000000..0ffd8c7 --- /dev/null +++ b/config/providers/persistence.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +Hanami.app.register_provider :persistence, namespace: true do + prepare do + require "rom-changeset" + require "rom/core" + require "rom/sql" + + # TODO(Hanami): As part of built-in rom setup, configure ROM with app inflector + silence_warnings { ROM::Inflector = Hanami.app["inflector"] } + + rom_config = ROM::Configuration.new(:sql, target["settings"].database_url) + + rom_config.plugin(:sql, relations: :instrumentation) do |plugin_config| + plugin_config.notifications = target["notifications"] + end + + rom_config.plugin(:sql, relations: :auto_restrictions) + + register "config", rom_config + register "db", rom_config.gateways[:default].connection + end + + start do + rom_config = target["persistence.config"] + rom_config.auto_registration( + target.root.join("lib/adamantium/persistence"), + namespace: "Adamantium::Persistence" + ) + + register "rom", ROM.container(rom_config) + end + + define_method(:silence_warnings) do |&block| + orig_verbose = $VERBOSE + $VERBOSE = nil + result = block.call + $VERBOSE = orig_verbose + result + end +end diff --git a/config/providers/post_utilities.rb b/config/providers/post_utilities.rb new file mode 100644 index 0000000..da44ad1 --- /dev/null +++ b/config/providers/post_utilities.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +Hanami.app.register_provider :post_utilities, namespace: true do + start do + register "slugify", Adamantium::SlugCreator.new + end +end diff --git a/config/providers/renderers.rb b/config/providers/renderers.rb new file mode 100644 index 0000000..58d84cf --- /dev/null +++ b/config/providers/renderers.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +Hanami.app.register_provider :renderers, namespace: true do + start do + register "markdown", Adamantium::Renderer::Markdown.new + end +end diff --git a/config/puma.rb b/config/puma.rb new file mode 100644 index 0000000..7e498c7 --- /dev/null +++ b/config/puma.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +max_threads_count = ENV.fetch("HANAMI_MAX_THREADS", 5) +min_threads_count = ENV.fetch("HANAMI_MIN_THREADS") { max_threads_count } +threads min_threads_count, max_threads_count + +port ENV.fetch("HANAMI_PORT", 2300) +environment ENV.fetch("HANAMI_ENV", "development") +workers ENV.fetch("HANAMI_WEB_CONCURRENCY", 2) + +on_worker_boot do + Hanami.shutdown +end + +preload_app! diff --git a/config/routes.rb b/config/routes.rb new file mode 100644 index 0000000..4fce10e --- /dev/null +++ b/config/routes.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +require "hanami/middleware/body_parser" +require "adamantium/middleware/process_params" + +module Adamantium + class Routes < Hanami::Routes + use Hanami::Middleware::BodyParser, [:form, :json] + use Adamantium::Middleware::ProcessParams + + scope "micropub" do + get "/", to: "site.config" + post "/", to: "posts.handle" + post "/media", to: "media.create" + end + + get "/", to: "site.home" + get "/post/:slug", to: "posts.show" + get "/posts", to: "posts.index" + + get "/bookmarks", to: "bookmarks.index" + get "/bookmark/:slug", to: "bookmarks.show" + + get "/tagged/:slug", to: "tags.show" + + get "/key", to: "key.show" if Hanami.app.settings.micropub_pub_key + + get "/feeds/rss", to: "feeds.rss" + + get "/:slug", to: "pages.show" + + redirect "deploying-a-hanami-app-to-fly-io", to: "/post/deploying-a-hanami-20-app-to-flyio" + redirect "deploying-a-hanami-app-to-fly-io/", to: "/post/deploying-a-hanami-20-app-to-flyio" + end +end diff --git a/config/settings.rb b/config/settings.rb new file mode 100644 index 0000000..326a98a --- /dev/null +++ b/config/settings.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +require "adamantium/types" + +module Adamantium + class Settings < Hanami::Settings + # Infrastructure + setting :database_url + + # Site details + setting :site_name + + ## ---- Micropub ---- + + # Site details + setting :micropub_site_id + setting :micropub_site_name + setting :micropub_site_url + # Auth + setting :micropub_pub_key, default: nil + # TODO: add other auth methods here + + # Micropub endpoints + setting :micropub_media_endpoint, default: "", constructor: Types::Params::String + + setting :micropub_authorization_endpoint + setting :micropub_token_endpoint + end +end diff --git a/db/migrate/20230101035642_create_posts.rb b/db/migrate/20230101035642_create_posts.rb new file mode 100644 index 0000000..aad71a9 --- /dev/null +++ b/db/migrate/20230101035642_create_posts.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +ROM::SQL.migration do + change do + create_table :posts do + primary_key :id + column :name, :text, null: false, default: "" + column :content, :text, null: false + column :published_at, :timestamp + end + end +end diff --git a/db/migrate/20230101113513_add_slug_to_posts.rb b/db/migrate/20230101113513_add_slug_to_posts.rb new file mode 100644 index 0000000..834be6f --- /dev/null +++ b/db/migrate/20230101113513_add_slug_to_posts.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +ROM::SQL.migration do + change do + alter_table :posts do + add_column :slug, :text, null: false + end + end +end diff --git a/db/migrate/20230101122416_add_unique_constraint_to_post_slug.rb b/db/migrate/20230101122416_add_unique_constraint_to_post_slug.rb new file mode 100644 index 0000000..ea8f339 --- /dev/null +++ b/db/migrate/20230101122416_add_unique_constraint_to_post_slug.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +ROM::SQL.migration do + change do + alter_table :posts do + add_unique_constraint :slug + end + end +end diff --git a/db/migrate/20230102075558_create_bookmarks.rb b/db/migrate/20230102075558_create_bookmarks.rb new file mode 100644 index 0000000..f344c1e --- /dev/null +++ b/db/migrate/20230102075558_create_bookmarks.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +ROM::SQL.migration do + change do + create_table :bookmarks do + primary_key :id + column :url, :text, null: false + column :name, :text, null: false + column :content, :text, null: false, default: "" + column :published_at, :timestamp + end + end +end diff --git a/db/migrate/20230103085913_add_slug_to_bookmarks.rb b/db/migrate/20230103085913_add_slug_to_bookmarks.rb new file mode 100644 index 0000000..44b4d22 --- /dev/null +++ b/db/migrate/20230103085913_add_slug_to_bookmarks.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +ROM::SQL.migration do + change do + alter_table :bookmarks do + add_column :slug, :text, null: false, unique: true + end + end +end diff --git a/db/migrate/20230103215123_create_tags.rb b/db/migrate/20230103215123_create_tags.rb new file mode 100644 index 0000000..e61049c --- /dev/null +++ b/db/migrate/20230103215123_create_tags.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +ROM::SQL.migration do + change do + create_table :tags do + primary_key :id + column :label, :text, null: false, unique: true + column :slug, :text, null: false, unique: true + end + + create_table :post_tags do + foreign_key :post_id, :posts, null: false + foreign_key :tag_id, :tags, null: false + end + end +end diff --git a/db/migrate/20230103215311_combine_posts_and_bookmarks.rb b/db/migrate/20230103215311_combine_posts_and_bookmarks.rb new file mode 100644 index 0000000..4edfbfb --- /dev/null +++ b/db/migrate/20230103215311_combine_posts_and_bookmarks.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +ROM::SQL.migration do + change do + drop_table :bookmarks + alter_table :posts do + add_column :url, :text + add_column :post_type, :text + end + end +end diff --git a/db/migrate/20230104085227_remove_not_null_constraint_on_post_content.rb b/db/migrate/20230104085227_remove_not_null_constraint_on_post_content.rb new file mode 100644 index 0000000..ffd038f --- /dev/null +++ b/db/migrate/20230104085227_remove_not_null_constraint_on_post_content.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +ROM::SQL.migration do + change do + alter_table :posts do + set_column_allow_null :content + end + end +end diff --git a/db/migrate/20230114093354_remove_not_null_constraint_on_post_name.rb b/db/migrate/20230114093354_remove_not_null_constraint_on_post_name.rb new file mode 100644 index 0000000..0c3c03a --- /dev/null +++ b/db/migrate/20230114093354_remove_not_null_constraint_on_post_name.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +ROM::SQL.migration do + change do + alter_table :posts do + set_column_allow_null :name + end + end +end diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..bc68b60 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,17 @@ +version: "3" + +services: + app: + build: + context: . + dockerfile: Dockerfile + volumes: + - .:/app + - bundle_path:/usr/local/bundle + command: script/puma + env_file: ".env" + ports: + - 2300:2300 +volumes: + bundle_path: + node_modules: diff --git a/lib/adamantium/command.rb b/lib/adamantium/command.rb new file mode 100644 index 0000000..8a01ae7 --- /dev/null +++ b/lib/adamantium/command.rb @@ -0,0 +1,13 @@ +# auto_register: false +# frozen_string_literal: true + +require "dry-matcher" +require "dry/matcher/result_matcher" +require "dry-monads" + +module Adamantium + class Command + include Dry::Matcher.for(:call, with: Dry::Matcher::ResultMatcher) + include Dry::Monads[:result] + end +end diff --git a/lib/adamantium/context.rb b/lib/adamantium/context.rb new file mode 100644 index 0000000..82f8082 --- /dev/null +++ b/lib/adamantium/context.rb @@ -0,0 +1,14 @@ +module Adamantium + class Context < Hanami::View::Context + def initialize(**options) + @options = options + super(**options) + end + + def link_active? path + # TODO: waiting for Hanami View to be released + # to access current_path + false + end + end +end diff --git a/lib/adamantium/micropub_request_parser.rb b/lib/adamantium/micropub_request_parser.rb new file mode 100644 index 0000000..cb7b360 --- /dev/null +++ b/lib/adamantium/micropub_request_parser.rb @@ -0,0 +1,85 @@ +require "securerandom" + +module Adamantium + class MicropubRequestParser + include Deps["post_utilities.slugify", "repos.post_repo"] + + def call(params:) + return nil if params.key?(:action) + + cont_type = content_type(params) + req_type = request_type(params) + req_params = parse_params(req_type, cont_type, params) + + if cont_type == :bookmark + return Entities::BookmarkRequest.new(req_params) + end + + Entities::PostRequest.new(req_params) + end + + private + + def slug(name:, default_slug:) + return default_slug if default_slug + + slugify.call( + text: name, + checker: post_repo.method(:slug_exists?) + ) + end + + def content_type(params) + return :bookmark if params[:"bookmark-of"] + :post + end + + def request_type(params) + if params[:h] == "entry" + return :form + end + + if params[:type]&.include?("h-entry") + return :json + end + + nil + end + + def parse_params(req_type, post_type, params) + new_params = {} + new_params[:h] = "entry" + new_params[:post_type] = post_type + new_params[:action] = params[:action] + + publish_time = params[:published_at] || Time.now + + if req_type == :json + new_params[:published_at] = (params[:"post-status"] == "draft") ? nil : publish_time + new_params[:category] = params[:properties][:category] || [] + new_params[:name] = params[:properties][:name] && params[:properties][:name].first + new_params[:content] = params[:properties][:content]&.first&.tr("\n", " ") + new_params[:slug] = params[:slug] + + else + new_params[:name] = params[:name] + new_params[:published_at] = (params[:"post-status"] == "draft") ? nil : publish_time + new_params[:category] = params[:category] || [] + + content = if params[:content] + if params[:content].is_a?(Hash) && params[:content][:html] + params[:content][:html] + else + params[:content] + end + end + + new_params[:content] = content + end + new_params[:url] = params[:"bookmark-of"] + new_params[:slug] = slug(name: new_params[:name], default_slug: params[:slug]) + + new_params + end + end +end diff --git a/lib/adamantium/middleware/process_params.rb b/lib/adamantium/middleware/process_params.rb new file mode 100644 index 0000000..89884c1 --- /dev/null +++ b/lib/adamantium/middleware/process_params.rb @@ -0,0 +1,14 @@ +module Adamantium + module Middleware + class ProcessParams + def initialize(app) + @app = app + end + + def call(env) + # NOOP for now. + @app.call(env) + end + end + end +end diff --git a/lib/adamantium/persistence/relations/post_tags.rb b/lib/adamantium/persistence/relations/post_tags.rb new file mode 100644 index 0000000..30d7736 --- /dev/null +++ b/lib/adamantium/persistence/relations/post_tags.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +module Adamantium + module Persistence + module Relations + class PostTags < ROM::Relation[:sql] + schema :post_tags, infer: true do + associations do + belongs_to :post + belongs_to :tag + end + end + + auto_struct(true) + end + end + end +end diff --git a/lib/adamantium/persistence/relations/posts.rb b/lib/adamantium/persistence/relations/posts.rb new file mode 100644 index 0000000..8e08ef4 --- /dev/null +++ b/lib/adamantium/persistence/relations/posts.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +module Adamantium + module Persistence + module Relations + class Posts < ROM::Relation[:sql] + schema :posts, infer: true do + associations do + has_many :post_tags + has_many :tags, through: :post_tags + end + end + + auto_struct(true) + + def published + where(self[:published_at] <= Time.now) + end + end + end + end +end diff --git a/lib/adamantium/persistence/relations/tags.rb b/lib/adamantium/persistence/relations/tags.rb new file mode 100644 index 0000000..3b84a1d --- /dev/null +++ b/lib/adamantium/persistence/relations/tags.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +module Adamantium + module Persistence + module Relations + class Tags < ROM::Relation[:sql] + schema :tags, infer: true do + associations do + belongs_to :post_tag + belongs_to :post, through: :post_tag + end + end + + auto_struct(true) + end + end + end +end diff --git a/lib/adamantium/renderer/markdown.rb b/lib/adamantium/renderer/markdown.rb new file mode 100644 index 0000000..877a209 --- /dev/null +++ b/lib/adamantium/renderer/markdown.rb @@ -0,0 +1,21 @@ +require "redcarpet" + +module Adamantium + module Renderer + class Markdown + attr_accessor :markdown + + def initialize + renderer = Redcarpet::Render::HTML.new({}) + extensions = { + fenced_code_blocks: true + } + @markdown = Redcarpet::Markdown.new(renderer, extensions) + end + + def call(content:) + markdown.render(content) + end + end + end +end diff --git a/lib/adamantium/repo.rb b/lib/adamantium/repo.rb new file mode 100644 index 0000000..d77c095 --- /dev/null +++ b/lib/adamantium/repo.rb @@ -0,0 +1,10 @@ +# auto_register: false +# frozen_string_literal: true + +require "rom-repository" + +module Adamantium + class Repo < ROM::Repository::Root + include Deps[container: "persistence.rom"] + end +end diff --git a/lib/adamantium/slug_creator.rb b/lib/adamantium/slug_creator.rb new file mode 100644 index 0000000..22d8b4e --- /dev/null +++ b/lib/adamantium/slug_creator.rb @@ -0,0 +1,20 @@ +require "babosa" +require "securerandom" + +module Adamantium + class SlugCreator + def call(text:, checker:) + input_slug = (text != "" && !text.nil?) ? text.to_slug.normalize.to_s : SecureRandom.uuid + slug = input_slug + + suffix = 1 + + while checker.call(slug) + slug = "#{input_slug}-#{suffix}" + suffix += 1 + end + + slug + end + end +end diff --git a/lib/adamantium/types.rb b/lib/adamantium/types.rb new file mode 100644 index 0000000..5ecff41 --- /dev/null +++ b/lib/adamantium/types.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +require "dry/types" + +module Adamantium + Types = Dry.Types + + module Types + # Define your custom types here + end +end diff --git a/lib/adamantium/view.rb b/lib/adamantium/view.rb new file mode 100644 index 0000000..807caa3 --- /dev/null +++ b/lib/adamantium/view.rb @@ -0,0 +1,10 @@ +# auto_register: false +# frozen_string_literal: true + +require "hanami/view" + +module Adamantium + class View < Hanami::View + config.default_context = Adamantium::Context.new + end +end diff --git a/lib/tasks/.keep b/lib/tasks/.keep new file mode 100644 index 0000000..e69de29 diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..a52881f --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1420 @@ +{ + "name": "adamantium", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "devDependencies": { + "@tailwindcss/typography": "^0.5.8", + "tailwindcss": "^3.2.4" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@tailwindcss/typography": { + "version": "0.5.8", + "resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.8.tgz", + "integrity": "sha512-xGQEp8KXN8Sd8m6R4xYmwxghmswrd0cPnNI2Lc6fmrC3OojysTBJJGSIVwPV56q4t6THFUK3HJ0EaWwpglSxWw==", + "dev": true, + "dependencies": { + "lodash.castarray": "^4.4.0", + "lodash.isplainobject": "^4.0.6", + "lodash.merge": "^4.6.2", + "postcss-selector-parser": "6.0.10" + }, + "peerDependencies": { + "tailwindcss": ">=3.0.0 || insiders" + } + }, + "node_modules/@tailwindcss/typography/node_modules/postcss-selector-parser": { + "version": "6.0.10", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz", + "integrity": "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==", + "dev": true, + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/acorn": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", + "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-node": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/acorn-node/-/acorn-node-1.8.2.tgz", + "integrity": "sha512-8mt+fslDufLYntIoPAaIMUe/lrbrehIiwmR3t2k9LljIzoigEPF27eLk2hy8zSGzmR/ogr7zbRKINMo1u0yh5A==", + "dev": true, + "dependencies": { + "acorn": "^7.0.0", + "acorn-walk": "^7.0.0", + "xtend": "^4.0.2" + } + }, + "node_modules/acorn-walk": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-7.2.0.tgz", + "integrity": "sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "dev": true + }, + "node_modules/binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/chokidar": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/defined": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/defined/-/defined-1.0.1.tgz", + "integrity": "sha512-hsBd2qSVCRE+5PmNdHt1uzyrFu5d3RwmFDKzyNZMFq/EwDNJF7Ee5+D5oEKF0hU6LhtoUF1macFvOe4AskQC1Q==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/detective": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/detective/-/detective-5.2.1.tgz", + "integrity": "sha512-v9XE1zRnz1wRtgurGu0Bs8uHKFSTdteYZNbIPFVhUZ39L/S79ppMpdmVOZAnoz1jfEFodc48n6MX483Xo3t1yw==", + "dev": true, + "dependencies": { + "acorn-node": "^1.8.2", + "defined": "^1.0.0", + "minimist": "^1.2.6" + }, + "bin": { + "detective": "bin/detective.js" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "dev": true + }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "dev": true + }, + "node_modules/fast-glob": { + "version": "3.2.12", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz", + "integrity": "sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fastq": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", + "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==", + "dev": true, + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.1" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.11.0.tgz", + "integrity": "sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw==", + "dev": true, + "dependencies": { + "has": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/lilconfig": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.0.6.tgz", + "integrity": "sha512-9JROoBW7pobfsx+Sq2JsASvCo6Pfo6WWoUW79HuB1BCoBXD4PLWJPqDF6fNj67pqBYTbAHkE57M1kS/+L1neOg==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/lodash.castarray": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.castarray/-/lodash.castarray-4.4.0.tgz", + "integrity": "sha512-aVx8ztPv7/2ULbArGJ2Y42bG1mEQ5mGjpdvrbJcJFU3TbYybe+QlLS4pst9zV52ymy2in1KpFPiZnAOATxD4+Q==", + "dev": true + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "dev": true + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dev": true, + "dependencies": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/minimist": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.7.tgz", + "integrity": "sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/nanoid": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz", + "integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==", + "dev": true, + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "node_modules/picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "dev": true + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postcss": { + "version": "8.4.20", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.20.tgz", + "integrity": "sha512-6Q04AXR1212bXr5fh03u8aAwbLxAQNGQ/Q1LNa0VfOI06ZAlhPHtQvE4OIdpj4kLThXilalPnmDSOD65DcHt+g==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + } + ], + "dependencies": { + "nanoid": "^3.3.4", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-import": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-14.1.0.tgz", + "integrity": "sha512-flwI+Vgm4SElObFVPpTIT7SU7R3qk2L7PyduMcokiaVKuWv9d/U+Gm/QAd8NDLuykTWTkcrjOeD2Pp1rMeBTGw==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-js": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.0.tgz", + "integrity": "sha512-77QESFBwgX4irogGVPgQ5s07vLvFqWr228qZY+w6lW599cRlK/HmnlivnnVUxkjHnCu4J16PDMHcH+e+2HbvTQ==", + "dev": true, + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": "^8.3.3" + } + }, + "node_modules/postcss-load-config": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-3.1.4.tgz", + "integrity": "sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==", + "dev": true, + "dependencies": { + "lilconfig": "^2.0.5", + "yaml": "^1.10.2" + }, + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": ">=8.0.9", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "postcss": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/postcss-nested": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.0.0.tgz", + "integrity": "sha512-0DkamqrPcmkBDsLn+vQDIrtkSbNkv5AD/M322ySo9kqFkCIYklym2xEmWkwo+Y3/qZo34tzEPNUw4y7yMCdv5w==", + "dev": true, + "dependencies": { + "postcss-selector-parser": "^6.0.10" + }, + "engines": { + "node": ">=12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.0.11", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.11.tgz", + "integrity": "sha512-zbARubNdogI9j7WY4nQJBiNqQf3sLS3wCP4WfOidu+p28LofJqDH1tcXypGrcmMHhDk2t9wGhCsYe/+szLTy1g==", + "dev": true, + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/quick-lru": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", + "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "dev": true, + "dependencies": { + "pify": "^2.3.0" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.1", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz", + "integrity": "sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==", + "dev": true, + "dependencies": { + "is-core-module": "^2.9.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/source-map-js": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", + "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tailwindcss": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.2.4.tgz", + "integrity": "sha512-AhwtHCKMtR71JgeYDaswmZXhPcW9iuI9Sp2LvZPo9upDZ7231ZJ7eA9RaURbhpXGVlrjX4cFNlB4ieTetEb7hQ==", + "dev": true, + "dependencies": { + "arg": "^5.0.2", + "chokidar": "^3.5.3", + "color-name": "^1.1.4", + "detective": "^5.2.1", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.2.12", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "lilconfig": "^2.0.6", + "micromatch": "^4.0.5", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.0.0", + "postcss": "^8.4.18", + "postcss-import": "^14.1.0", + "postcss-js": "^4.0.0", + "postcss-load-config": "^3.1.4", + "postcss-nested": "6.0.0", + "postcss-selector-parser": "^6.0.10", + "postcss-value-parser": "^4.2.0", + "quick-lru": "^5.1.1", + "resolve": "^1.22.1" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, + "engines": { + "node": ">=12.13.0" + }, + "peerDependencies": { + "postcss": "^8.0.9" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "dev": true, + "engines": { + "node": ">=0.4" + } + }, + "node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "dev": true, + "engines": { + "node": ">= 6" + } + } + }, + "dependencies": { + "@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "requires": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + } + }, + "@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true + }, + "@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "requires": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + } + }, + "@tailwindcss/typography": { + "version": "0.5.8", + "resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.8.tgz", + "integrity": "sha512-xGQEp8KXN8Sd8m6R4xYmwxghmswrd0cPnNI2Lc6fmrC3OojysTBJJGSIVwPV56q4t6THFUK3HJ0EaWwpglSxWw==", + "dev": true, + "requires": { + "lodash.castarray": "^4.4.0", + "lodash.isplainobject": "^4.0.6", + "lodash.merge": "^4.6.2", + "postcss-selector-parser": "6.0.10" + }, + "dependencies": { + "postcss-selector-parser": { + "version": "6.0.10", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz", + "integrity": "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==", + "dev": true, + "requires": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + } + } + } + }, + "acorn": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", + "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", + "dev": true + }, + "acorn-node": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/acorn-node/-/acorn-node-1.8.2.tgz", + "integrity": "sha512-8mt+fslDufLYntIoPAaIMUe/lrbrehIiwmR3t2k9LljIzoigEPF27eLk2hy8zSGzmR/ogr7zbRKINMo1u0yh5A==", + "dev": true, + "requires": { + "acorn": "^7.0.0", + "acorn-walk": "^7.0.0", + "xtend": "^4.0.2" + } + }, + "acorn-walk": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-7.2.0.tgz", + "integrity": "sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==", + "dev": true + }, + "anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "requires": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + } + }, + "arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "dev": true + }, + "binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "dev": true + }, + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "requires": { + "fill-range": "^7.0.1" + } + }, + "camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "dev": true + }, + "chokidar": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "dev": true, + "requires": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "fsevents": "~2.3.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "dependencies": { + "glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "requires": { + "is-glob": "^4.0.1" + } + } + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true + }, + "defined": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/defined/-/defined-1.0.1.tgz", + "integrity": "sha512-hsBd2qSVCRE+5PmNdHt1uzyrFu5d3RwmFDKzyNZMFq/EwDNJF7Ee5+D5oEKF0hU6LhtoUF1macFvOe4AskQC1Q==", + "dev": true + }, + "detective": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/detective/-/detective-5.2.1.tgz", + "integrity": "sha512-v9XE1zRnz1wRtgurGu0Bs8uHKFSTdteYZNbIPFVhUZ39L/S79ppMpdmVOZAnoz1jfEFodc48n6MX483Xo3t1yw==", + "dev": true, + "requires": { + "acorn-node": "^1.8.2", + "defined": "^1.0.0", + "minimist": "^1.2.6" + } + }, + "didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "dev": true + }, + "dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "dev": true + }, + "fast-glob": { + "version": "3.2.12", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz", + "integrity": "sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==", + "dev": true, + "requires": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "dependencies": { + "glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "requires": { + "is-glob": "^4.0.1" + } + } + } + }, + "fastq": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", + "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==", + "dev": true, + "requires": { + "reusify": "^1.0.4" + } + }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "optional": true + }, + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true + }, + "glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "requires": { + "is-glob": "^4.0.3" + } + }, + "has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, + "requires": { + "function-bind": "^1.1.1" + } + }, + "is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "requires": { + "binary-extensions": "^2.0.0" + } + }, + "is-core-module": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.11.0.tgz", + "integrity": "sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw==", + "dev": true, + "requires": { + "has": "^1.0.3" + } + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true + }, + "is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "requires": { + "is-extglob": "^2.1.1" + } + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true + }, + "lilconfig": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.0.6.tgz", + "integrity": "sha512-9JROoBW7pobfsx+Sq2JsASvCo6Pfo6WWoUW79HuB1BCoBXD4PLWJPqDF6fNj67pqBYTbAHkE57M1kS/+L1neOg==", + "dev": true + }, + "lodash.castarray": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.castarray/-/lodash.castarray-4.4.0.tgz", + "integrity": "sha512-aVx8ztPv7/2ULbArGJ2Y42bG1mEQ5mGjpdvrbJcJFU3TbYybe+QlLS4pst9zV52ymy2in1KpFPiZnAOATxD4+Q==", + "dev": true + }, + "lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "dev": true + }, + "lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, + "merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true + }, + "micromatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dev": true, + "requires": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + } + }, + "minimist": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.7.tgz", + "integrity": "sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g==", + "dev": true + }, + "nanoid": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz", + "integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==", + "dev": true + }, + "normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true + }, + "object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "dev": true + }, + "path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "dev": true + }, + "picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true + }, + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true + }, + "postcss": { + "version": "8.4.20", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.20.tgz", + "integrity": "sha512-6Q04AXR1212bXr5fh03u8aAwbLxAQNGQ/Q1LNa0VfOI06ZAlhPHtQvE4OIdpj4kLThXilalPnmDSOD65DcHt+g==", + "dev": true, + "requires": { + "nanoid": "^3.3.4", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + } + }, + "postcss-import": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-14.1.0.tgz", + "integrity": "sha512-flwI+Vgm4SElObFVPpTIT7SU7R3qk2L7PyduMcokiaVKuWv9d/U+Gm/QAd8NDLuykTWTkcrjOeD2Pp1rMeBTGw==", + "dev": true, + "requires": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + } + }, + "postcss-js": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.0.tgz", + "integrity": "sha512-77QESFBwgX4irogGVPgQ5s07vLvFqWr228qZY+w6lW599cRlK/HmnlivnnVUxkjHnCu4J16PDMHcH+e+2HbvTQ==", + "dev": true, + "requires": { + "camelcase-css": "^2.0.1" + } + }, + "postcss-load-config": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-3.1.4.tgz", + "integrity": "sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==", + "dev": true, + "requires": { + "lilconfig": "^2.0.5", + "yaml": "^1.10.2" + } + }, + "postcss-nested": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.0.0.tgz", + "integrity": "sha512-0DkamqrPcmkBDsLn+vQDIrtkSbNkv5AD/M322ySo9kqFkCIYklym2xEmWkwo+Y3/qZo34tzEPNUw4y7yMCdv5w==", + "dev": true, + "requires": { + "postcss-selector-parser": "^6.0.10" + } + }, + "postcss-selector-parser": { + "version": "6.0.11", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.11.tgz", + "integrity": "sha512-zbARubNdogI9j7WY4nQJBiNqQf3sLS3wCP4WfOidu+p28LofJqDH1tcXypGrcmMHhDk2t9wGhCsYe/+szLTy1g==", + "dev": true, + "requires": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + } + }, + "postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true + }, + "queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true + }, + "quick-lru": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", + "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", + "dev": true + }, + "read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "dev": true, + "requires": { + "pify": "^2.3.0" + } + }, + "readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "requires": { + "picomatch": "^2.2.1" + } + }, + "resolve": { + "version": "1.22.1", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz", + "integrity": "sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==", + "dev": true, + "requires": { + "is-core-module": "^2.9.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + } + }, + "reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true + }, + "run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "requires": { + "queue-microtask": "^1.2.2" + } + }, + "source-map-js": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", + "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", + "dev": true + }, + "supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true + }, + "tailwindcss": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.2.4.tgz", + "integrity": "sha512-AhwtHCKMtR71JgeYDaswmZXhPcW9iuI9Sp2LvZPo9upDZ7231ZJ7eA9RaURbhpXGVlrjX4cFNlB4ieTetEb7hQ==", + "dev": true, + "requires": { + "arg": "^5.0.2", + "chokidar": "^3.5.3", + "color-name": "^1.1.4", + "detective": "^5.2.1", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.2.12", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "lilconfig": "^2.0.6", + "micromatch": "^4.0.5", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.0.0", + "postcss": "^8.4.18", + "postcss-import": "^14.1.0", + "postcss-js": "^4.0.0", + "postcss-load-config": "^3.1.4", + "postcss-nested": "6.0.0", + "postcss-selector-parser": "^6.0.10", + "postcss-value-parser": "^4.2.0", + "quick-lru": "^5.1.1", + "resolve": "^1.22.1" + } + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "requires": { + "is-number": "^7.0.0" + } + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true + }, + "xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "dev": true + }, + "yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "dev": true + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..52ea97d --- /dev/null +++ b/package.json @@ -0,0 +1,6 @@ +{ + "devDependencies": { + "@tailwindcss/typography": "^0.5.8", + "tailwindcss": "^3.2.4" + } +} diff --git a/public/01/2023/IMG_0147.jpg b/public/01/2023/IMG_0147.jpg new file mode 100644 index 0000000..c4c0342 Binary files /dev/null and b/public/01/2023/IMG_0147.jpg differ diff --git a/public/assets/JetBrainsMono-VariableFont_wght.ttf b/public/assets/JetBrainsMono-VariableFont_wght.ttf new file mode 100644 index 0000000..0b06e2c Binary files /dev/null and b/public/assets/JetBrainsMono-VariableFont_wght.ttf differ diff --git a/public/assets/Rubik-VariableFont_wght.ttf b/public/assets/Rubik-VariableFont_wght.ttf new file mode 100644 index 0000000..fa161b9 Binary files /dev/null and b/public/assets/Rubik-VariableFont_wght.ttf differ diff --git a/public/assets/index.css b/public/assets/index.css new file mode 100644 index 0000000..af170a5 --- /dev/null +++ b/public/assets/index.css @@ -0,0 +1,1694 @@ +/* +! tailwindcss v3.2.4 | MIT License | https://tailwindcss.com +*/ + +/* +1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4) +2. Allow adding a border to an element by just adding a border-width. (https://github.com/tailwindcss/tailwindcss/pull/116) +*/ + +*, +::before, +::after { + box-sizing: border-box; + /* 1 */ + border-width: 0; + /* 2 */ + border-style: solid; + /* 2 */ + border-color: #e5e7eb; + /* 2 */ +} + +::before, +::after { + --tw-content: ''; +} + +/* +1. Use a consistent sensible line-height in all browsers. +2. Prevent adjustments of font size after orientation changes in iOS. +3. Use a more readable tab size. +4. Use the user's configured `sans` font-family by default. +5. Use the user's configured `sans` font-feature-settings by default. +*/ + +html { + line-height: 1.5; + /* 1 */ + -webkit-text-size-adjust: 100%; + /* 2 */ + -moz-tab-size: 4; + /* 3 */ + -o-tab-size: 4; + tab-size: 4; + /* 3 */ + font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; + /* 4 */ + font-feature-settings: normal; + /* 5 */ +} + +/* +1. Remove the margin in all browsers. +2. Inherit line-height from `html` so users can set them as a class directly on the `html` element. +*/ + +body { + margin: 0; + /* 1 */ + line-height: inherit; + /* 2 */ +} + +/* +1. Add the correct height in Firefox. +2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655) +3. Ensure horizontal rules are visible by default. +*/ + +hr { + height: 0; + /* 1 */ + color: inherit; + /* 2 */ + border-top-width: 1px; + /* 3 */ +} + +/* +Add the correct text decoration in Chrome, Edge, and Safari. +*/ + +abbr:where([title]) { + -webkit-text-decoration: underline dotted; + text-decoration: underline dotted; +} + +/* +Remove the default font size and weight for headings. +*/ + +h1, +h2, +h3, +h4, +h5, +h6 { + font-size: inherit; + font-weight: inherit; +} + +/* +Reset links to optimize for opt-in styling instead of opt-out. +*/ + +a { + color: inherit; + text-decoration: inherit; +} + +/* +Add the correct font weight in Edge and Safari. +*/ + +b, +strong { + font-weight: bolder; +} + +/* +1. Use the user's configured `mono` font family by default. +2. Correct the odd `em` font sizing in all browsers. +*/ + +code, +kbd, +samp, +pre { + font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; + /* 1 */ + font-size: 1em; + /* 2 */ +} + +/* +Add the correct font size in all browsers. +*/ + +small { + font-size: 80%; +} + +/* +Prevent `sub` and `sup` elements from affecting the line height in all browsers. +*/ + +sub, +sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; +} + +sub { + bottom: -0.25em; +} + +sup { + top: -0.5em; +} + +/* +1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297) +2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016) +3. Remove gaps between table borders by default. +*/ + +table { + text-indent: 0; + /* 1 */ + border-color: inherit; + /* 2 */ + border-collapse: collapse; + /* 3 */ +} + +/* +1. Change the font styles in all browsers. +2. Remove the margin in Firefox and Safari. +3. Remove default padding in all browsers. +*/ + +button, +input, +optgroup, +select, +textarea { + font-family: inherit; + /* 1 */ + font-size: 100%; + /* 1 */ + font-weight: inherit; + /* 1 */ + line-height: inherit; + /* 1 */ + color: inherit; + /* 1 */ + margin: 0; + /* 2 */ + padding: 0; + /* 3 */ +} + +/* +Remove the inheritance of text transform in Edge and Firefox. +*/ + +button, +select { + text-transform: none; +} + +/* +1. Correct the inability to style clickable types in iOS and Safari. +2. Remove default button styles. +*/ + +button, +[type='button'], +[type='reset'], +[type='submit'] { + -webkit-appearance: button; + /* 1 */ + background-color: transparent; + /* 2 */ + background-image: none; + /* 2 */ +} + +/* +Use the modern Firefox focus style for all focusable elements. +*/ + +:-moz-focusring { + outline: auto; +} + +/* +Remove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737) +*/ + +:-moz-ui-invalid { + box-shadow: none; +} + +/* +Add the correct vertical alignment in Chrome and Firefox. +*/ + +progress { + vertical-align: baseline; +} + +/* +Correct the cursor style of increment and decrement buttons in Safari. +*/ + +::-webkit-inner-spin-button, +::-webkit-outer-spin-button { + height: auto; +} + +/* +1. Correct the odd appearance in Chrome and Safari. +2. Correct the outline style in Safari. +*/ + +[type='search'] { + -webkit-appearance: textfield; + /* 1 */ + outline-offset: -2px; + /* 2 */ +} + +/* +Remove the inner padding in Chrome and Safari on macOS. +*/ + +::-webkit-search-decoration { + -webkit-appearance: none; +} + +/* +1. Correct the inability to style clickable types in iOS and Safari. +2. Change font properties to `inherit` in Safari. +*/ + +::-webkit-file-upload-button { + -webkit-appearance: button; + /* 1 */ + font: inherit; + /* 2 */ +} + +/* +Add the correct display in Chrome and Safari. +*/ + +summary { + display: list-item; +} + +/* +Removes the default spacing and border for appropriate elements. +*/ + +blockquote, +dl, +dd, +h1, +h2, +h3, +h4, +h5, +h6, +hr, +figure, +p, +pre { + margin: 0; +} + +fieldset { + margin: 0; + padding: 0; +} + +legend { + padding: 0; +} + +ol, +ul, +menu { + list-style: none; + margin: 0; + padding: 0; +} + +/* +Prevent resizing textareas horizontally by default. +*/ + +textarea { + resize: vertical; +} + +/* +1. Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300) +2. Set the default placeholder color to the user's configured gray 400 color. +*/ + +input::-moz-placeholder, textarea::-moz-placeholder { + opacity: 1; + /* 1 */ + color: #9ca3af; + /* 2 */ +} + +input::placeholder, +textarea::placeholder { + opacity: 1; + /* 1 */ + color: #9ca3af; + /* 2 */ +} + +/* +Set the default cursor for buttons. +*/ + +button, +[role="button"] { + cursor: pointer; +} + +/* +Make sure disabled buttons don't get the pointer cursor. +*/ + +:disabled { + cursor: default; +} + +/* +1. Make replaced elements `display: block` by default. (https://github.com/mozdevs/cssremedy/issues/14) +2. Add `vertical-align: middle` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210) + This can trigger a poorly considered lint error in some tools but is included by design. +*/ + +img, +svg, +video, +canvas, +audio, +iframe, +embed, +object { + display: block; + /* 1 */ + vertical-align: middle; + /* 2 */ +} + +/* +Constrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14) +*/ + +img, +video { + max-width: 100%; + height: auto; +} + +/* Make elements with the HTML hidden attribute stay hidden by default */ + +[hidden] { + display: none; +} + +*, ::before, ::after { + --tw-border-spacing-x: 0; + --tw-border-spacing-y: 0; + --tw-translate-x: 0; + --tw-translate-y: 0; + --tw-rotate: 0; + --tw-skew-x: 0; + --tw-skew-y: 0; + --tw-scale-x: 1; + --tw-scale-y: 1; + --tw-pan-x: ; + --tw-pan-y: ; + --tw-pinch-zoom: ; + --tw-scroll-snap-strictness: proximity; + --tw-ordinal: ; + --tw-slashed-zero: ; + --tw-numeric-figure: ; + --tw-numeric-spacing: ; + --tw-numeric-fraction: ; + --tw-ring-inset: ; + --tw-ring-offset-width: 0px; + --tw-ring-offset-color: #fff; + --tw-ring-color: rgb(59 130 246 / 0.5); + --tw-ring-offset-shadow: 0 0 #0000; + --tw-ring-shadow: 0 0 #0000; + --tw-shadow: 0 0 #0000; + --tw-shadow-colored: 0 0 #0000; + --tw-blur: ; + --tw-brightness: ; + --tw-contrast: ; + --tw-grayscale: ; + --tw-hue-rotate: ; + --tw-invert: ; + --tw-saturate: ; + --tw-sepia: ; + --tw-drop-shadow: ; + --tw-backdrop-blur: ; + --tw-backdrop-brightness: ; + --tw-backdrop-contrast: ; + --tw-backdrop-grayscale: ; + --tw-backdrop-hue-rotate: ; + --tw-backdrop-invert: ; + --tw-backdrop-opacity: ; + --tw-backdrop-saturate: ; + --tw-backdrop-sepia: ; +} + +::backdrop { + --tw-border-spacing-x: 0; + --tw-border-spacing-y: 0; + --tw-translate-x: 0; + --tw-translate-y: 0; + --tw-rotate: 0; + --tw-skew-x: 0; + --tw-skew-y: 0; + --tw-scale-x: 1; + --tw-scale-y: 1; + --tw-pan-x: ; + --tw-pan-y: ; + --tw-pinch-zoom: ; + --tw-scroll-snap-strictness: proximity; + --tw-ordinal: ; + --tw-slashed-zero: ; + --tw-numeric-figure: ; + --tw-numeric-spacing: ; + --tw-numeric-fraction: ; + --tw-ring-inset: ; + --tw-ring-offset-width: 0px; + --tw-ring-offset-color: #fff; + --tw-ring-color: rgb(59 130 246 / 0.5); + --tw-ring-offset-shadow: 0 0 #0000; + --tw-ring-shadow: 0 0 #0000; + --tw-shadow: 0 0 #0000; + --tw-shadow-colored: 0 0 #0000; + --tw-blur: ; + --tw-brightness: ; + --tw-contrast: ; + --tw-grayscale: ; + --tw-hue-rotate: ; + --tw-invert: ; + --tw-saturate: ; + --tw-sepia: ; + --tw-drop-shadow: ; + --tw-backdrop-blur: ; + --tw-backdrop-brightness: ; + --tw-backdrop-contrast: ; + --tw-backdrop-grayscale: ; + --tw-backdrop-hue-rotate: ; + --tw-backdrop-invert: ; + --tw-backdrop-opacity: ; + --tw-backdrop-saturate: ; + --tw-backdrop-sepia: ; +} + +.prose { + color: var(--tw-prose-body); + max-width: 65ch; +} + +.prose :where([class~="lead"]):not(:where([class~="not-prose"] *)) { + color: var(--tw-prose-lead); + font-size: 1.25em; + line-height: 1.6; + margin-top: 1.2em; + margin-bottom: 1.2em; +} + +.prose :where(a):not(:where([class~="not-prose"] *)) { + color: var(--tw-prose-links); + text-decoration: underline; + font-weight: 500; +} + +.prose :where(strong):not(:where([class~="not-prose"] *)) { + color: var(--tw-prose-bold); + font-weight: 600; +} + +.prose :where(a strong):not(:where([class~="not-prose"] *)) { + color: inherit; +} + +.prose :where(blockquote strong):not(:where([class~="not-prose"] *)) { + color: inherit; +} + +.prose :where(thead th strong):not(:where([class~="not-prose"] *)) { + color: inherit; +} + +.prose :where(ol):not(:where([class~="not-prose"] *)) { + list-style-type: decimal; + margin-top: 1.25em; + margin-bottom: 1.25em; + padding-left: 1.625em; +} + +.prose :where(ol[type="A"]):not(:where([class~="not-prose"] *)) { + list-style-type: upper-alpha; +} + +.prose :where(ol[type="a"]):not(:where([class~="not-prose"] *)) { + list-style-type: lower-alpha; +} + +.prose :where(ol[type="A" s]):not(:where([class~="not-prose"] *)) { + list-style-type: upper-alpha; +} + +.prose :where(ol[type="a" s]):not(:where([class~="not-prose"] *)) { + list-style-type: lower-alpha; +} + +.prose :where(ol[type="I"]):not(:where([class~="not-prose"] *)) { + list-style-type: upper-roman; +} + +.prose :where(ol[type="i"]):not(:where([class~="not-prose"] *)) { + list-style-type: lower-roman; +} + +.prose :where(ol[type="I" s]):not(:where([class~="not-prose"] *)) { + list-style-type: upper-roman; +} + +.prose :where(ol[type="i" s]):not(:where([class~="not-prose"] *)) { + list-style-type: lower-roman; +} + +.prose :where(ol[type="1"]):not(:where([class~="not-prose"] *)) { + list-style-type: decimal; +} + +.prose :where(ul):not(:where([class~="not-prose"] *)) { + list-style-type: disc; + margin-top: 1.25em; + margin-bottom: 1.25em; + padding-left: 1.625em; +} + +.prose :where(ol > li):not(:where([class~="not-prose"] *))::marker { + font-weight: 400; + color: var(--tw-prose-counters); +} + +.prose :where(ul > li):not(:where([class~="not-prose"] *))::marker { + color: var(--tw-prose-bullets); +} + +.prose :where(hr):not(:where([class~="not-prose"] *)) { + border-color: var(--tw-prose-hr); + border-top-width: 1px; + margin-top: 3em; + margin-bottom: 3em; +} + +.prose :where(blockquote):not(:where([class~="not-prose"] *)) { + font-weight: 500; + font-style: italic; + color: var(--tw-prose-quotes); + border-left-width: 0.25rem; + border-left-color: var(--tw-prose-quote-borders); + quotes: "\201C""\201D""\2018""\2019"; + margin-top: 1.6em; + margin-bottom: 1.6em; + padding-left: 1em; +} + +.prose :where(blockquote p:first-of-type):not(:where([class~="not-prose"] *))::before { + content: open-quote; +} + +.prose :where(blockquote p:last-of-type):not(:where([class~="not-prose"] *))::after { + content: close-quote; +} + +.prose :where(h1):not(:where([class~="not-prose"] *)) { + color: var(--tw-prose-headings); + font-weight: 800; + font-size: 2.25em; + margin-top: 0; + margin-bottom: 0.8888889em; + line-height: 1.1111111; +} + +.prose :where(h1 strong):not(:where([class~="not-prose"] *)) { + font-weight: 900; + color: inherit; +} + +.prose :where(h2):not(:where([class~="not-prose"] *)) { + color: var(--tw-prose-headings); + font-weight: 700; + font-size: 1.5em; + margin-top: 2em; + margin-bottom: 1em; + line-height: 1.3333333; +} + +.prose :where(h2 strong):not(:where([class~="not-prose"] *)) { + font-weight: 800; + color: inherit; +} + +.prose :where(h3):not(:where([class~="not-prose"] *)) { + color: var(--tw-prose-headings); + font-weight: 600; + font-size: 1.25em; + margin-top: 1.6em; + margin-bottom: 0.6em; + line-height: 1.6; +} + +.prose :where(h3 strong):not(:where([class~="not-prose"] *)) { + font-weight: 700; + color: inherit; +} + +.prose :where(h4):not(:where([class~="not-prose"] *)) { + color: var(--tw-prose-headings); + font-weight: 600; + margin-top: 1.5em; + margin-bottom: 0.5em; + line-height: 1.5; +} + +.prose :where(h4 strong):not(:where([class~="not-prose"] *)) { + font-weight: 700; + color: inherit; +} + +.prose :where(img):not(:where([class~="not-prose"] *)) { + margin-top: 2em; + margin-bottom: 2em; +} + +.prose :where(figure > *):not(:where([class~="not-prose"] *)) { + margin-top: 0; + margin-bottom: 0; +} + +.prose :where(figcaption):not(:where([class~="not-prose"] *)) { + color: var(--tw-prose-captions); + font-size: 0.875em; + line-height: 1.4285714; + margin-top: 0.8571429em; +} + +.prose :where(code):not(:where([class~="not-prose"] *)) { + color: var(--tw-prose-code); + font-weight: 600; + font-size: 0.875em; +} + +.prose :where(code):not(:where([class~="not-prose"] *))::before { + content: "`"; +} + +.prose :where(code):not(:where([class~="not-prose"] *))::after { + content: "`"; +} + +.prose :where(a code):not(:where([class~="not-prose"] *)) { + color: inherit; +} + +.prose :where(h1 code):not(:where([class~="not-prose"] *)) { + color: inherit; +} + +.prose :where(h2 code):not(:where([class~="not-prose"] *)) { + color: inherit; + font-size: 0.875em; +} + +.prose :where(h3 code):not(:where([class~="not-prose"] *)) { + color: inherit; + font-size: 0.9em; +} + +.prose :where(h4 code):not(:where([class~="not-prose"] *)) { + color: inherit; +} + +.prose :where(blockquote code):not(:where([class~="not-prose"] *)) { + color: inherit; +} + +.prose :where(thead th code):not(:where([class~="not-prose"] *)) { + color: inherit; +} + +.prose :where(pre):not(:where([class~="not-prose"] *)) { + color: var(--tw-prose-pre-code); + background-color: var(--tw-prose-pre-bg); + overflow-x: auto; + font-weight: 400; + font-size: 0.875em; + line-height: 1.7142857; + margin-top: 1.7142857em; + margin-bottom: 1.7142857em; + border-radius: 0.375rem; + padding-top: 0.8571429em; + padding-right: 1.1428571em; + padding-bottom: 0.8571429em; + padding-left: 1.1428571em; +} + +.prose :where(pre code):not(:where([class~="not-prose"] *)) { + background-color: transparent; + border-width: 0; + border-radius: 0; + padding: 0; + font-weight: inherit; + color: inherit; + font-size: inherit; + font-family: inherit; + line-height: inherit; +} + +.prose :where(pre code):not(:where([class~="not-prose"] *))::before { + content: none; +} + +.prose :where(pre code):not(:where([class~="not-prose"] *))::after { + content: none; +} + +.prose :where(table):not(:where([class~="not-prose"] *)) { + width: 100%; + table-layout: auto; + text-align: left; + margin-top: 2em; + margin-bottom: 2em; + font-size: 0.875em; + line-height: 1.7142857; +} + +.prose :where(thead):not(:where([class~="not-prose"] *)) { + border-bottom-width: 1px; + border-bottom-color: var(--tw-prose-th-borders); +} + +.prose :where(thead th):not(:where([class~="not-prose"] *)) { + color: var(--tw-prose-headings); + font-weight: 600; + vertical-align: bottom; + padding-right: 0.5714286em; + padding-bottom: 0.5714286em; + padding-left: 0.5714286em; +} + +.prose :where(tbody tr):not(:where([class~="not-prose"] *)) { + border-bottom-width: 1px; + border-bottom-color: var(--tw-prose-td-borders); +} + +.prose :where(tbody tr:last-child):not(:where([class~="not-prose"] *)) { + border-bottom-width: 0; +} + +.prose :where(tbody td):not(:where([class~="not-prose"] *)) { + vertical-align: baseline; +} + +.prose :where(tfoot):not(:where([class~="not-prose"] *)) { + border-top-width: 1px; + border-top-color: var(--tw-prose-th-borders); +} + +.prose :where(tfoot td):not(:where([class~="not-prose"] *)) { + vertical-align: top; +} + +.prose { + --tw-prose-body: #374151; + --tw-prose-headings: #111827; + --tw-prose-lead: #4b5563; + --tw-prose-links: #111827; + --tw-prose-bold: #111827; + --tw-prose-counters: #6b7280; + --tw-prose-bullets: #d1d5db; + --tw-prose-hr: #e5e7eb; + --tw-prose-quotes: #111827; + --tw-prose-quote-borders: #e5e7eb; + --tw-prose-captions: #6b7280; + --tw-prose-code: #111827; + --tw-prose-pre-code: #e5e7eb; + --tw-prose-pre-bg: #1f2937; + --tw-prose-th-borders: #d1d5db; + --tw-prose-td-borders: #e5e7eb; + --tw-prose-invert-body: #d1d5db; + --tw-prose-invert-headings: #fff; + --tw-prose-invert-lead: #9ca3af; + --tw-prose-invert-links: #fff; + --tw-prose-invert-bold: #fff; + --tw-prose-invert-counters: #9ca3af; + --tw-prose-invert-bullets: #4b5563; + --tw-prose-invert-hr: #374151; + --tw-prose-invert-quotes: #f3f4f6; + --tw-prose-invert-quote-borders: #374151; + --tw-prose-invert-captions: #9ca3af; + --tw-prose-invert-code: #fff; + --tw-prose-invert-pre-code: #d1d5db; + --tw-prose-invert-pre-bg: rgb(0 0 0 / 50%); + --tw-prose-invert-th-borders: #4b5563; + --tw-prose-invert-td-borders: #374151; + font-size: 1rem; + line-height: 1.75; +} + +.prose :where(p):not(:where([class~="not-prose"] *)) { + margin-top: 1.25em; + margin-bottom: 1.25em; +} + +.prose :where(video):not(:where([class~="not-prose"] *)) { + margin-top: 2em; + margin-bottom: 2em; +} + +.prose :where(figure):not(:where([class~="not-prose"] *)) { + margin-top: 2em; + margin-bottom: 2em; +} + +.prose :where(li):not(:where([class~="not-prose"] *)) { + margin-top: 0.5em; + margin-bottom: 0.5em; +} + +.prose :where(ol > li):not(:where([class~="not-prose"] *)) { + padding-left: 0.375em; +} + +.prose :where(ul > li):not(:where([class~="not-prose"] *)) { + padding-left: 0.375em; +} + +.prose :where(.prose > ul > li p):not(:where([class~="not-prose"] *)) { + margin-top: 0.75em; + margin-bottom: 0.75em; +} + +.prose :where(.prose > ul > li > *:first-child):not(:where([class~="not-prose"] *)) { + margin-top: 1.25em; +} + +.prose :where(.prose > ul > li > *:last-child):not(:where([class~="not-prose"] *)) { + margin-bottom: 1.25em; +} + +.prose :where(.prose > ol > li > *:first-child):not(:where([class~="not-prose"] *)) { + margin-top: 1.25em; +} + +.prose :where(.prose > ol > li > *:last-child):not(:where([class~="not-prose"] *)) { + margin-bottom: 1.25em; +} + +.prose :where(ul ul, ul ol, ol ul, ol ol):not(:where([class~="not-prose"] *)) { + margin-top: 0.75em; + margin-bottom: 0.75em; +} + +.prose :where(hr + *):not(:where([class~="not-prose"] *)) { + margin-top: 0; +} + +.prose :where(h2 + *):not(:where([class~="not-prose"] *)) { + margin-top: 0; +} + +.prose :where(h3 + *):not(:where([class~="not-prose"] *)) { + margin-top: 0; +} + +.prose :where(h4 + *):not(:where([class~="not-prose"] *)) { + margin-top: 0; +} + +.prose :where(thead th:first-child):not(:where([class~="not-prose"] *)) { + padding-left: 0; +} + +.prose :where(thead th:last-child):not(:where([class~="not-prose"] *)) { + padding-right: 0; +} + +.prose :where(tbody td, tfoot td):not(:where([class~="not-prose"] *)) { + padding-top: 0.5714286em; + padding-right: 0.5714286em; + padding-bottom: 0.5714286em; + padding-left: 0.5714286em; +} + +.prose :where(tbody td:first-child, tfoot td:first-child):not(:where([class~="not-prose"] *)) { + padding-left: 0; +} + +.prose :where(tbody td:last-child, tfoot td:last-child):not(:where([class~="not-prose"] *)) { + padding-right: 0; +} + +.prose :where(.prose > :first-child):not(:where([class~="not-prose"] *)) { + margin-top: 0; +} + +.prose :where(.prose > :last-child):not(:where([class~="not-prose"] *)) { + margin-bottom: 0; +} + +.mx-auto { + margin-left: auto; + margin-right: auto; +} + +.my-2 { + margin-top: 0.5rem; + margin-bottom: 0.5rem; +} + +.mb-12 { + margin-bottom: 3rem; +} + +.mb-8 { + margin-bottom: 2rem; +} + +.mb-2 { + margin-bottom: 0.5rem; +} + +.mr-1\.5 { + margin-right: 0.375rem; +} + +.mr-1 { + margin-right: 0.25rem; +} + +.mb-4 { + margin-bottom: 1rem; +} + +.mt-0\.5 { + margin-top: 0.125rem; +} + +.mt-0 { + margin-top: 0px; +} + +.ml-8 { + margin-left: 2rem; +} + +.ml-12 { + margin-left: 3rem; +} + +.ml-2 { + margin-left: 0.5rem; +} + +.mr-2 { + margin-right: 0.5rem; +} + +.inline { + display: inline; +} + +.flex { + display: flex; +} + +.w-6 { + width: 1.5rem; +} + +.w-2 { + width: 0.5rem; +} + +.w-64 { + width: 16rem; +} + +.w-32 { + width: 8rem; +} + +.w-16 { + width: 4rem; +} + +.w-8 { + width: 2rem; +} + +.w-52 { + width: 13rem; +} + +.w-48 { + width: 12rem; +} + +.max-w-prose { + max-width: 65ch; +} + +.max-w-screen-md { + max-width: 768px; +} + +.flex-1 { + flex: 1 1 0%; +} + +.flex-none { + flex: none; +} + +.flex-auto { + flex: 1 1 auto; +} + +.shrink { + flex-shrink: 1; +} + +.basis-3\/4 { + flex-basis: 75%; +} + +.basis-1\/4 { + flex-basis: 25%; +} + +.basis-2\/4 { + flex-basis: 50%; +} + +.basis-4\/5 { + flex-basis: 80%; +} + +.basis-1\/5 { + flex-basis: 20%; +} + +.basis-3\/5 { + flex-basis: 60%; +} + +.basis-2\/5 { + flex-basis: 40%; +} + +.flex-row { + flex-direction: row; +} + +.items-center { + align-items: center; +} + +.justify-between { + justify-content: space-between; +} + +.space-x-1 > :not([hidden]) ~ :not([hidden]) { + --tw-space-x-reverse: 0; + margin-right: calc(0.25rem * var(--tw-space-x-reverse)); + margin-left: calc(0.25rem * calc(1 - var(--tw-space-x-reverse))); +} + +.rounded-full { + border-radius: 9999px; +} + +.rounded { + border-radius: 0.25rem; +} + +.border-2 { + border-width: 2px; +} + +.border-0 { + border-width: 0px; +} + +.border-t-4 { + border-top-width: 4px; +} + +.border-solid { + border-style: solid; +} + +.border-gray-400 { + --tw-border-opacity: 1; + border-color: rgb(156 163 175 / var(--tw-border-opacity)); +} + +.border-blue-400 { + --tw-border-opacity: 1; + border-color: rgb(96 165 250 / var(--tw-border-opacity)); +} + +.bg-white { + --tw-bg-opacity: 1; + background-color: rgb(255 255 255 / var(--tw-bg-opacity)); +} + +.bg-yellow-200\/60 { + background-color: rgb(254 240 138 / 0.6); +} + +.bg-slate-100 { + --tw-bg-opacity: 1; + background-color: rgb(241 245 249 / var(--tw-bg-opacity)); +} + +.bg-blue-400 { + --tw-bg-opacity: 1; + background-color: rgb(96 165 250 / var(--tw-bg-opacity)); +} + +.p-1 { + padding: 0.25rem; +} + +.p-4 { + padding: 1rem; +} + +.p-2 { + padding: 0.5rem; +} + +.px-4 { + padding-left: 1rem; + padding-right: 1rem; +} + +.px-2 { + padding-left: 0.5rem; + padding-right: 0.5rem; +} + +.px-1 { + padding-left: 0.25rem; + padding-right: 0.25rem; +} + +.pb-8 { + padding-bottom: 2rem; +} + +.pt-4 { + padding-top: 1rem; +} + +.pb-10 { + padding-bottom: 2.5rem; +} + +.text-right { + text-align: right; +} + +.text-xl { + font-size: 1.25rem; +} + +.text-sm { + font-size: 0.8rem; +} + +.text-base { + font-size: 1rem; +} + +.text-xsm { + font-size: 0.75rem; +} + +.font-bold { + font-weight: 700; +} + +.uppercase { + text-transform: uppercase; +} + +.leading-relaxed { + line-height: 1.625; +} + +.text-gray-800 { + --tw-text-opacity: 1; + color: rgb(31 41 55 / var(--tw-text-opacity)); +} + +.text-blue-600 { + --tw-text-opacity: 1; + color: rgb(37 99 235 / var(--tw-text-opacity)); +} + +.text-gray-400 { + --tw-text-opacity: 1; + color: rgb(156 163 175 / var(--tw-text-opacity)); +} + +.text-red-600 { + --tw-text-opacity: 1; + color: rgb(220 38 38 / var(--tw-text-opacity)); +} + +.text-yellow-600 { + --tw-text-opacity: 1; + color: rgb(202 138 4 / var(--tw-text-opacity)); +} + +.text-green-600 { + --tw-text-opacity: 1; + color: rgb(22 163 74 / var(--tw-text-opacity)); +} + +.text-gray-200 { + --tw-text-opacity: 1; + color: rgb(229 231 235 / var(--tw-text-opacity)); +} + +.text-gray-600 { + --tw-text-opacity: 1; + color: rgb(75 85 99 / var(--tw-text-opacity)); +} + +.text-blue-400 { + --tw-text-opacity: 1; + color: rgb(96 165 250 / var(--tw-text-opacity)); +} + +.no-underline { + text-decoration-line: none; +} + +@tailwind typography; + +@font-face { + font-family: "Rubik"; + + src: url("/assets/Rubik-VariableFont_wght.ttf") format("truetype"); +} + +@font-face { + font-family: "JetBrainsMono"; + + src: url("/assets/JetBrainsMono-VariableFont_wght.ttf") format("truetype"); +} + +* { + font-family: "Rubik", Helvetica, Arial, sans-serif; +} + +.gist tr { + border-bottom: none; +} + +.gist span { + font-family: "JetBrainsMono", Monaco, monospace; +} + +.selection\:bg-blue-100 *::-moz-selection { + --tw-bg-opacity: 1; + background-color: rgb(219 234 254 / var(--tw-bg-opacity)); +} + +.selection\:bg-blue-100 *::selection { + --tw-bg-opacity: 1; + background-color: rgb(219 234 254 / var(--tw-bg-opacity)); +} + +.selection\:text-blue-900 *::-moz-selection { + --tw-text-opacity: 1; + color: rgb(30 58 138 / var(--tw-text-opacity)); +} + +.selection\:text-blue-900 *::selection { + --tw-text-opacity: 1; + color: rgb(30 58 138 / var(--tw-text-opacity)); +} + +.selection\:bg-blue-100::-moz-selection { + --tw-bg-opacity: 1; + background-color: rgb(219 234 254 / var(--tw-bg-opacity)); +} + +.selection\:bg-blue-100::selection { + --tw-bg-opacity: 1; + background-color: rgb(219 234 254 / var(--tw-bg-opacity)); +} + +.selection\:text-blue-900::-moz-selection { + --tw-text-opacity: 1; + color: rgb(30 58 138 / var(--tw-text-opacity)); +} + +.selection\:text-blue-900::selection { + --tw-text-opacity: 1; + color: rgb(30 58 138 / var(--tw-text-opacity)); +} + +.hover\:border-blue-800:hover { + --tw-border-opacity: 1; + border-color: rgb(30 64 175 / var(--tw-border-opacity)); +} + +.hover\:bg-red-100:hover { + --tw-bg-opacity: 1; + background-color: rgb(254 226 226 / var(--tw-bg-opacity)); +} + +.hover\:bg-blue-100:hover { + --tw-bg-opacity: 1; + background-color: rgb(219 234 254 / var(--tw-bg-opacity)); +} + +.hover\:bg-yellow-100:hover { + --tw-bg-opacity: 1; + background-color: rgb(254 249 195 / var(--tw-bg-opacity)); +} + +.hover\:bg-orange-100:hover { + --tw-bg-opacity: 1; + background-color: rgb(255 237 213 / var(--tw-bg-opacity)); +} + +.hover\:bg-yellow-200:hover { + --tw-bg-opacity: 1; + background-color: rgb(254 240 138 / var(--tw-bg-opacity)); +} + +.hover\:bg-blue-800:hover { + --tw-bg-opacity: 1; + background-color: rgb(30 64 175 / var(--tw-bg-opacity)); +} + +.hover\:text-red-400:hover { + --tw-text-opacity: 1; + color: rgb(248 113 113 / var(--tw-text-opacity)); +} + +.hover\:text-blue-400:hover { + --tw-text-opacity: 1; + color: rgb(96 165 250 / var(--tw-text-opacity)); +} + +.hover\:text-yellow-600:hover { + --tw-text-opacity: 1; + color: rgb(202 138 4 / var(--tw-text-opacity)); +} + +.hover\:text-orange-400:hover { + --tw-text-opacity: 1; + color: rgb(251 146 60 / var(--tw-text-opacity)); +} + +.hover\:text-gray-800:hover { + --tw-text-opacity: 1; + color: rgb(31 41 55 / var(--tw-text-opacity)); +} + +.hover\:text-blue-600:hover { + --tw-text-opacity: 1; + color: rgb(37 99 235 / var(--tw-text-opacity)); +} + +.hover\:text-blue-100:hover { + --tw-text-opacity: 1; + color: rgb(219 234 254 / var(--tw-text-opacity)); +} + +.hover\:underline:hover { + text-decoration-line: underline; +} + +.prose-a\:rounded-sm :is(:where(a):not(:where([class~="not-prose"] *))) { + border-radius: 0.125rem; +} + +.prose-a\:p-0\.5 :is(:where(a):not(:where([class~="not-prose"] *))) { + padding: 0.125rem; +} + +.prose-a\:p-0 :is(:where(a):not(:where([class~="not-prose"] *))) { + padding: 0px; +} + +.prose-a\:text-blue-600 :is(:where(a):not(:where([class~="not-prose"] *))) { + --tw-text-opacity: 1; + color: rgb(37 99 235 / var(--tw-text-opacity)); +} + +.prose-a\:no-underline :is(:where(a):not(:where([class~="not-prose"] *))) { + text-decoration-line: none; +} + +.hover\:prose-a\:underline :is(:where(a):not(:where([class~="not-prose"] *))):hover { + text-decoration-line: underline; +} + +.prose-em\:bg-blue-600 :is(:where(em):not(:where([class~="not-prose"] *))) { + --tw-bg-opacity: 1; + background-color: rgb(37 99 235 / var(--tw-bg-opacity)); +} + +.prose-em\:px-1 :is(:where(em):not(:where([class~="not-prose"] *))) { + padding-left: 0.25rem; + padding-right: 0.25rem; +} + +.prose-em\:font-bold :is(:where(em):not(:where([class~="not-prose"] *))) { + font-weight: 700; +} + +.prose-em\:not-italic :is(:where(em):not(:where([class~="not-prose"] *))) { + font-style: normal; +} + +.prose-em\:text-blue-100 :is(:where(em):not(:where([class~="not-prose"] *))) { + --tw-text-opacity: 1; + color: rgb(219 234 254 / var(--tw-text-opacity)); +} + +@media (prefers-color-scheme: dark) { + .dark\:border-gray-600 { + --tw-border-opacity: 1; + border-color: rgb(75 85 99 / var(--tw-border-opacity)); + } + + .dark\:bg-black { + --tw-bg-opacity: 1; + background-color: rgb(0 0 0 / var(--tw-bg-opacity)); + } + + .dark\:bg-yellow-400 { + --tw-bg-opacity: 1; + background-color: rgb(250 204 21 / var(--tw-bg-opacity)); + } + + .dark\:prose-invert { + --tw-prose-body: var(--tw-prose-invert-body); + --tw-prose-headings: var(--tw-prose-invert-headings); + --tw-prose-lead: var(--tw-prose-invert-lead); + --tw-prose-links: var(--tw-prose-invert-links); + --tw-prose-bold: var(--tw-prose-invert-bold); + --tw-prose-counters: var(--tw-prose-invert-counters); + --tw-prose-bullets: var(--tw-prose-invert-bullets); + --tw-prose-hr: var(--tw-prose-invert-hr); + --tw-prose-quotes: var(--tw-prose-invert-quotes); + --tw-prose-quote-borders: var(--tw-prose-invert-quote-borders); + --tw-prose-captions: var(--tw-prose-invert-captions); + --tw-prose-code: var(--tw-prose-invert-code); + --tw-prose-pre-code: var(--tw-prose-invert-pre-code); + --tw-prose-pre-bg: var(--tw-prose-invert-pre-bg); + --tw-prose-th-borders: var(--tw-prose-invert-th-borders); + --tw-prose-td-borders: var(--tw-prose-invert-td-borders); + } + + .dark\:text-gray-200 { + --tw-text-opacity: 1; + color: rgb(229 231 235 / var(--tw-text-opacity)); + } + + .dark\:text-gray-600 { + --tw-text-opacity: 1; + color: rgb(75 85 99 / var(--tw-text-opacity)); + } + + .dark\:text-gray-400 { + --tw-text-opacity: 1; + color: rgb(156 163 175 / var(--tw-text-opacity)); + } + + .dark\:text-red-400 { + --tw-text-opacity: 1; + color: rgb(248 113 113 / var(--tw-text-opacity)); + } + + .dark\:text-yellow-600 { + --tw-text-opacity: 1; + color: rgb(202 138 4 / var(--tw-text-opacity)); + } + + .dark\:text-yellow-800 { + --tw-text-opacity: 1; + color: rgb(133 77 14 / var(--tw-text-opacity)); + } + + .dark\:selection\:bg-blue-600 *::-moz-selection { + --tw-bg-opacity: 1; + background-color: rgb(37 99 235 / var(--tw-bg-opacity)); + } + + .dark\:selection\:bg-blue-600 *::selection { + --tw-bg-opacity: 1; + background-color: rgb(37 99 235 / var(--tw-bg-opacity)); + } + + .dark\:selection\:text-blue-100 *::-moz-selection { + --tw-text-opacity: 1; + color: rgb(219 234 254 / var(--tw-text-opacity)); + } + + .dark\:selection\:text-blue-100 *::selection { + --tw-text-opacity: 1; + color: rgb(219 234 254 / var(--tw-text-opacity)); + } + + .dark\:selection\:bg-blue-600::-moz-selection { + --tw-bg-opacity: 1; + background-color: rgb(37 99 235 / var(--tw-bg-opacity)); + } + + .dark\:selection\:bg-blue-600::selection { + --tw-bg-opacity: 1; + background-color: rgb(37 99 235 / var(--tw-bg-opacity)); + } + + .dark\:selection\:text-blue-100::-moz-selection { + --tw-text-opacity: 1; + color: rgb(219 234 254 / var(--tw-text-opacity)); + } + + .dark\:selection\:text-blue-100::selection { + --tw-text-opacity: 1; + color: rgb(219 234 254 / var(--tw-text-opacity)); + } + + .dark\:hover\:bg-red-200:hover { + --tw-bg-opacity: 1; + background-color: rgb(254 202 202 / var(--tw-bg-opacity)); + } + + .dark\:hover\:bg-blue-200:hover { + --tw-bg-opacity: 1; + background-color: rgb(191 219 254 / var(--tw-bg-opacity)); + } + + .dark\:hover\:bg-yellow-200:hover { + --tw-bg-opacity: 1; + background-color: rgb(254 240 138 / var(--tw-bg-opacity)); + } + + .dark\:hover\:bg-orange-200:hover { + --tw-bg-opacity: 1; + background-color: rgb(254 215 170 / var(--tw-bg-opacity)); + } + + .dark\:hover\:bg-yellow-400\/80:hover { + background-color: rgb(250 204 21 / 0.8); + } + + .dark\:hover\:text-gray-200:hover { + --tw-text-opacity: 1; + color: rgb(229 231 235 / var(--tw-text-opacity)); + } + + .dark\:hover\:text-blue-200:hover { + --tw-text-opacity: 1; + color: rgb(191 219 254 / var(--tw-text-opacity)); + } + + .dark\:hover\:text-yellow-100:hover { + --tw-text-opacity: 1; + color: rgb(254 249 195 / var(--tw-text-opacity)); + } +} + +@media (min-width: 768px) { + .md\:mx-0 { + margin-left: 0px; + margin-right: 0px; + } + + .md\:mb-12 { + margin-bottom: 3rem; + } + + .md\:block { + display: block; + } + + .md\:h-10 { + height: 2.5rem; + } + + .md\:h-4 { + height: 1rem; + } + + .md\:w-10 { + width: 2.5rem; + } + + .md\:w-4 { + width: 1rem; + } + + .md\:flex-auto { + flex: 1 1 auto; + } + + .md\:pt-8 { + padding-top: 2rem; + } + + .md\:text-xl { + font-size: 1.25rem; + } + + .md\:text-sm { + font-size: 0.8rem; + } +} + diff --git a/public/assets/memoji.png b/public/assets/memoji.png new file mode 100644 index 0000000..147b138 Binary files /dev/null and b/public/assets/memoji.png differ diff --git a/script/bootstrap b/script/bootstrap new file mode 100755 index 0000000..1c66163 --- /dev/null +++ b/script/bootstrap @@ -0,0 +1,38 @@ +#!/bin/sh + +# script/bootstrap: Resolve all dependencies that the application requires to +# run. + +set -e + +cd "$(dirname "$0")/.." + +if [ "$1" = "docker" ]; then + docker-compose run --rm app "bundle" + docker-compose run --rm --entrypoint "/bin/sh -lc" assets "npm install" +else + if [ -f "Brewfile" ] && [ "$(uname -s)" = "Darwin" ]; then + brew bundle check >/dev/null 2>&1 || { + echo "==> Installing Homebrew dependencies…" + brew bundle + } + fi + + if [ -f ".tool-versions" ] && [ "$(uname -s)" = "Darwin" ]; then + echo "==> Installing package versions…" + brew bootstrap-asdf + fi + + if [ -f "Gemfile" ]; then + echo "==> Installing gem dependencies…" + bundle check >/dev/null 2>&1 || { + bundle config set without 'production' + bundle install --quiet + } + fi + + if [ -f "package.json" ]; then + echo "==> Installing node packages…" + npm install + fi +fi diff --git a/script/frontend b/script/frontend new file mode 100755 index 0000000..86afaed --- /dev/null +++ b/script/frontend @@ -0,0 +1,9 @@ +#!/bin/sh + +# script/frontend: Launch the frontend asset server locally. + +set -e + +cd "$(dirname "$0")/../slices/main/assets/rental" + +yarn run dev \ No newline at end of file diff --git a/script/puma b/script/puma new file mode 100755 index 0000000..fb43309 --- /dev/null +++ b/script/puma @@ -0,0 +1,5 @@ +#!/bin/sh + +set -e + +bundle exec puma -C config/puma.rb diff --git a/script/server b/script/server new file mode 100755 index 0000000..4d5509e --- /dev/null +++ b/script/server @@ -0,0 +1,19 @@ +#!/bin/sh + +# script/server: Launch the application and any extra required processes +# locally. + +set -e + +cd "$(dirname "$0")/.." + +test -z "$HANAMI_ENV" && HANAMI_ENV='development' + +if ! command -v overmind > /dev/null; then + echo "You must install Overmind to use this script. See:" + echo " . https://github.com/DarthSim/overmind#installation" + exit 1 +fi + +# Boot the app and any other necessary processes +exec env OVERMIND_PORT=5001 overmind start -f Procfile.dev \ No newline at end of file diff --git a/script/setup b/script/setup new file mode 100755 index 0000000..36cda2e --- /dev/null +++ b/script/setup @@ -0,0 +1,25 @@ +#!/bin/sh + +# script/setup: Set up application for the first time after cloning, or set it +# back to the initial first unused state. + +set -e + +if [ ! -f ".env" ]; then + echo "==> Creating .env file" + cp .env-example .env +fi + +if [ "$1" == "docker" ]; then + script/bootstrap docker + docker-compose up +else + cd "$(dirname "$0")/.." + + script/bootstrap + + echo "==> Setting up database..." + bin/hanami db setup + + echo "==> App is now ready to go!" +fi diff --git a/script/support b/script/support new file mode 100755 index 0000000..f403dfc --- /dev/null +++ b/script/support @@ -0,0 +1,9 @@ +#!/bin/sh + +# script/support: Launch supporting services for the application. + +set -e + +cd "$(dirname "$0")/.." + +overmind start -f Procfile.support -s .overmind.support.sock diff --git a/script/test b/script/test new file mode 100755 index 0000000..11f4d2e --- /dev/null +++ b/script/test @@ -0,0 +1,19 @@ +#!/bin/sh + +# script/test: Run test suite for application. Optionally pass in a path to an +# individual test file to run a single test. + +set -e + +cd "$(dirname "$0")/.." + +[ -z "$DEBUG" ] || set -x + +echo "==> Running tests…" + +if [ -n "$1" ]; then + # Pass arguments to test call. This is useful for calling a single test. + bundle exec rspec "$1" +else + bundle exec rake spec +fi diff --git a/script/update b/script/update new file mode 100755 index 0000000..47bab08 --- /dev/null +++ b/script/update @@ -0,0 +1,17 @@ +#!/bin/sh + +# script/update: Update application to run for its current checkout. + +set -e + +cd "$(dirname "$0")/.." + +script/bootstrap + +echo "==> Updating db…" +bin/hanami db create + +bin/hanami db create -e test +bin/hanami db reset -e test + +bin/hanami db migrate diff --git a/spec/adamantium/micropub_request_parser_spec.rb b/spec/adamantium/micropub_request_parser_spec.rb new file mode 100644 index 0000000..28ad74a --- /dev/null +++ b/spec/adamantium/micropub_request_parser_spec.rb @@ -0,0 +1,45 @@ +RSpec.describe Adamantium::MicropubRequestParser do + subject { described_class.new } + + context "json request" do + context "HTML post" do + let(:params) { + { + type: ["h-entry"], + properties: { + name: ["title"], + content: [ + "Hello world" + ] + }, + category: ["ruby", "rspec"] + } + } + + it "parses the params in to the expected shape" do + Timecop.freeze do + result = subject.call(params: params) + expect(result).to be_a Adamantium::Entities::PostRequest + end + end + end + end + + context "form request" do + let(:params) { + { + h: "entry", + name: "title", + content: "Hello world", + category: ["ruby", "rspec"] + } + } + + it "parses the params in to the expected shape" do + Timecop.freeze do + result = subject.call(params: params) + expect(result).to be_a Adamantium::Entities::PostRequest + end + end + end +end diff --git a/spec/adamantium/slug_creator_spec.rb b/spec/adamantium/slug_creator_spec.rb new file mode 100644 index 0000000..1775121 --- /dev/null +++ b/spec/adamantium/slug_creator_spec.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +RSpec.describe Adamantium::SlugCreator do + describe "#call" do + subject { described_class.new } + let(:checker) { ->(_input) { false } } + let(:uuid_regex) { /^[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}$/ } + + it "creates a slugified string" do + expect(subject.call(text: "my string", checker: checker)).to eq "my-string" + end + + it "can handle no input" do + expect(subject.call(text: nil, checker: checker)).to match(uuid_regex) + end + + it "adds a number to the end of the slug if the checker finds an existing slug" do + text = "my-existing-slug" + checker = ->(input) { true if input == text } + expect(subject.call(text: text, checker: checker)).to eq "my-existing-slug-1" + end + end +end diff --git a/spec/adamantium/unit/commands/media/upload_spec.rb b/spec/adamantium/unit/commands/media/upload_spec.rb new file mode 100644 index 0000000..b9b9aa2 --- /dev/null +++ b/spec/adamantium/unit/commands/media/upload_spec.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +require "dry/monads" + +RSpec.describe Adamantium::Commands::Media::Upload do + subject { described_class.new } + + it "saves a file and returns its URL" do + file = { + filename: "foo.txt", + tempfile: Tempfile.new + } + + result = subject.call(file: file) + expected_path = "media/#{Time.now.strftime("%m-%Y")}/foo.txt" + expect(result).to be_success + expect(result.value!).to eq "http://localhost/#{expected_path}" + File.read("public/#{expected_path}") + File.delete("public/#{expected_path}") + end + + it "returns a Failure if the file couldn't be saved" do + file = {filename: "file.txt", tempfile: ""} + + result = subject.call(file: file) + expect(result).to be_failure + end +end diff --git a/spec/requests/create_post_spec.rb b/spec/requests/create_post_spec.rb new file mode 100644 index 0000000..3f399b1 --- /dev/null +++ b/spec/requests/create_post_spec.rb @@ -0,0 +1,61 @@ +# frozen_string_literal: true + +RSpec.describe "Post creation", type: [:request, :db] do + let(:post_repo) { Adamantium::Repos::PostRepo.new } + + context "posts" do + it "is successful" do + params = { + type: ["h-entry"], + properties: { + category: ["ruby", "rspec"], + name: ["Test"], + content: [ + "

Hello world!

" + ] + } + } + + post "/micropub", params + + expect(last_response).to be_successful + expect(post_repo.post_listing.count).to eq 1 + expect(post_repo.post_listing.first.tags.map(&:label)).to eq ["ruby", "rspec"] + end + + it "is successful" do + params = { + type: ["h-entry"], + properties: { + category: ["ruby", "rspec"], + name: [], + content: [ + "

Hello world!

" + ] + } + } + + post "/micropub", params + + expect(last_response).to be_successful + expect(post_repo.post_listing.count).to eq 1 + expect(post_repo.post_listing.first.tags.map(&:label)).to eq ["ruby", "rspec"] + end + end + + context "bookmarks" do + it "is successful" do + params = { + h: "entry", + "bookmark-of": "http://example.com", + name: "Name", + content: "Content of theh post" + } + + post "/micropub", params + + expect(last_response).to be_successful + expect(post_repo.bookmark_listing.count).to eq 1 + end + end +end diff --git a/spec/requests/root_spec.rb b/spec/requests/root_spec.rb new file mode 100644 index 0000000..27ecff2 --- /dev/null +++ b/spec/requests/root_spec.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +RSpec.describe "Root", type: :request do + it "is successful" do + get "/" + + expect(last_response).to be_successful + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb new file mode 100644 index 0000000..733f57b --- /dev/null +++ b/spec/spec_helper.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +require "pathname" +SPEC_ROOT = Pathname(__dir__).realpath.freeze + +ENV["HANAMI_ENV"] ||= "test" +require "hanami/prepare" + +require "timecop" + +require_relative "support/rspec" +require_relative "support/requests" +require_relative "support/db" diff --git a/spec/support/db.rb b/spec/support/db.rb new file mode 100644 index 0000000..4ef65eb --- /dev/null +++ b/spec/support/db.rb @@ -0,0 +1,15 @@ +# require_with_metadata: true +# frozen_string_literal: true + +require_relative "db/helpers" +require_relative "db/database_cleaner" + +RSpec.configure do |config| + config.before :suite do + Hanami.app.start :persistence + end + + config.include Test::DB::Helpers, :db + + # config.include(Test::DB::FactoryHelper.new, factory: nil) +end diff --git a/spec/support/db/database_cleaner.rb b/spec/support/db/database_cleaner.rb new file mode 100644 index 0000000..434d7c2 --- /dev/null +++ b/spec/support/db/database_cleaner.rb @@ -0,0 +1,18 @@ +require "sequel" +require "database_cleaner/sequel" +require_relative "helpers" + +DatabaseCleaner[:sequel].strategy = :transaction + +RSpec.configure do |config| + config.prepend_before :each, type: :db do |example| + strategy = example.metadata[:js] ? :truncation : :transaction + DatabaseCleaner[:sequel].strategy = strategy + + DatabaseCleaner[:sequel].start + end + + config.append_after :each, type: :db do + DatabaseCleaner[:sequel].clean + end +end diff --git a/spec/support/db/helpers.rb b/spec/support/db/helpers.rb new file mode 100644 index 0000000..c2eb130 --- /dev/null +++ b/spec/support/db/helpers.rb @@ -0,0 +1,19 @@ +module Test + module DB + module Helpers + module_function + + def relations + rom.relations + end + + def rom + Hanami.app["persistence.rom"] + end + + def db + Hanami.app["persistence.db"] + end + end + end +end diff --git a/spec/support/requests.rb b/spec/support/requests.rb new file mode 100644 index 0000000..f1c8f9a --- /dev/null +++ b/spec/support/requests.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +require "rack/test" + +RSpec.shared_context "Hanami app" do + let(:app) { Hanami.app } +end + +RSpec.configure do |config| + config.include Rack::Test::Methods, type: :request + config.include_context "Hanami app", type: :request +end diff --git a/spec/support/rspec.rb b/spec/support/rspec.rb new file mode 100644 index 0000000..9634983 --- /dev/null +++ b/spec/support/rspec.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +RSpec.configure do |config| + config.expect_with :rspec do |expectations| + expectations.include_chain_clauses_in_custom_matcher_descriptions = true + end + + config.mock_with :rspec do |mocks| + mocks.verify_partial_doubles = true + end + + config.shared_context_metadata_behavior = :apply_to_host_groups + + config.filter_run_when_matching :focus + + config.disable_monkey_patching! + config.warnings = true + + if config.files_to_run.one? + config.default_formatter = "doc" + end + + config.profile_examples = 10 + + config.order = :random + Kernel.srand config.seed +end diff --git a/tailwind.config.js b/tailwind.config.js new file mode 100644 index 0000000..7bc3a93 --- /dev/null +++ b/tailwind.config.js @@ -0,0 +1,36 @@ +/** @type {import('tailwindcss').Config} */ + +const colors = require("tailwindcss/colors"); + +module.exports = { + content: ["./app/templates/**/*.slim"], + theme: { + fontSize: { + xsm: '0.75rem', + sm: '0.8rem', + base: '1rem', + xl: '1.25rem', + '2xl': '1.563rem', + '3xl': '1.953rem', + '4xl': '2.441rem', + '5xl': '3.052rem', + }, + extend: { + typograpgy: { + emphasis: { + css: { + em: { + colors: colors.pink['400'] + }, + a: { + colors: colors.red['100'] + } + } + } + } + }, + }, + plugins: [ + require('@tailwindcss/typography'), + ], +}