Initial commit

This commit is contained in:
2023-01-27 22:55:09 +11:00
commit 833f3ea8b2
130 changed files with 5637 additions and 0 deletions

62
app/action.rb Normal file
View File

@@ -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

0
app/actions/.keep Normal file
View File

View File

@@ -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

View File

@@ -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

14
app/actions/feeds/rss.rb Normal file
View File

@@ -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

13
app/actions/key/show.rb Normal file
View File

@@ -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

View File

@@ -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

15
app/actions/pages/show.rb Normal file
View File

@@ -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

View File

@@ -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

View File

@@ -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

15
app/actions/posts/show.rb Normal file
View File

@@ -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

View File

@@ -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

12
app/actions/site/home.rb Normal file
View File

@@ -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

15
app/actions/tags/show.rb Normal file
View File

@@ -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

27
app/assets/index.css Normal file
View File

@@ -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;
}

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

2
app/content/home.md Normal file
View File

@@ -0,0 +1,2 @@
Hi! 👋 I'm Daniel, a software engineer living in Canberra, Australia.

View File

@@ -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)

View File

@@ -0,0 +1 @@
# Art things

View File

@@ -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

View File

@@ -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 << "</#{tags.pop}>"
when :text
results << p_e[0][0..new_len]
new_len -= p_e[0].length
else
results << "<!-- #{p_e.inspect} -->"
end
end
if at_end
results << "..."
end
tags.reverse_each do |tag|
results << "</#{tag}>"
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

View File

@@ -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

View File

@@ -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

84
app/repos/post_repo.rb Normal file
View File

@@ -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

View File

@@ -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

9
app/repos/tag_repo.rb Normal file
View File

@@ -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

View File

@@ -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"

View File

@@ -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

View File

@@ -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

View File

@@ -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.

View File

View File

@@ -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!

View File

@@ -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"

View File

@@ -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"

View File

@@ -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

View File

@@ -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

View File

@@ -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"

View File

@@ -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

View File

@@ -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

View File

@@ -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 &rarr;
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"

View File

@@ -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"

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

23
app/views/feeds/rss.rb Normal file
View File

@@ -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

6
app/views/not_found.rb Normal file
View File

@@ -0,0 +1,6 @@
module Adamantium
module Views
class NotFound < View
end
end
end

17
app/views/pages/show.rb Normal file
View File

@@ -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

15
app/views/posts/index.rb Normal file
View File

@@ -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

13
app/views/posts/show.rb Normal file
View File

@@ -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

21
app/views/site/home.rb Normal file
View File

@@ -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

22
app/views/tags/show.rb Normal file
View File

@@ -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