font-variant-emoji: unicode;
}
+.text-indent {
+ text-indent: 2em;
+}
+
+.note {
+ border: 1px solid var(--accent-a);
+ border-radius: 3px;
+ padding: 4px 8px;
+}
+
+.reg-form {
+ border: 1px solid var(--accent-a);
+ border-radius: 3px;
+ padding: 4px 8px;
+ background: var(--quote-bg);
+}
+
/* Style for all form inputs */
input[type="text"],
input[type="email"],
margin-top: -25px;
}
+.post_siteheading {
+ color: var(--link-color);
+ margin-bottom: -0.5em;
+}
+
+.post_title {
+ margin-top: 0;
+}
+
.post {
background-color: var(--post-bg);
padding: 2em;
border-radius: 4px;
padding: 1px 1.5em;
margin: 10px auto;
- max-width: 290px;
+ max-width: 690px;
color: var(--accent-c);
}
transition: transform 0.2s ease;
}
}
+
+/* Margin Notes/Footnotes */
+/* Two-column layout */
+@media (min-width: 1024px) {
+ .post-grid {
+ display: grid;
+ grid-template-columns: minmax(0, 700px) minmax(0, 200px);
+ gap: 45px;
+ margin: 0 auto;
+ }
+
+ .main-content {
+ grid-column: 1;
+ position: relative;
+ }
+
+ .side-notes {
+ grid-column: 2;
+ position: relative;
+ height: 100%;
+ }
+
+ .side-note {
+ position: absolute;
+ width: 100%;
+ font-size: 0.9em;
+ color: var(--light-text);
+ padding: 0.5em;
+ border-left: 2px solid var(--accent-a);
+ background: var(--post-bg);
+ word-break: break-all;
+ transition:
+ border-color 0.2s,
+ background-color 0.2s;
+ }
+
+ .side-note a {
+ color: var(--light-text);
+ text-decoration: underline;
+ font-size: 0.9em;
+ }
+
+ .side-note:hover {
+ border-left-color: var(--link-color);
+ background-color: var(--quote-bg);
+ }
+
+ /* Style reference numbers in main text */
+ .main-content p a[rel="nofollow"] {
+ color: var(--accent-a);
+ text-decoration: none;
+ }
+}
+
+/* Mobile layout */
+@media (max-width: 1023px) {
+ .post-grid {
+ display: block;
+ }
+
+ .side-notes {
+ display: none;
+ }
+
+ /* Show original footnotes on mobile */
+ .main-content p[style*="display: none"] {
+ display: block !important;
+ }
+}
+
+/* Post Navigation */
+.post-navigation {
+ margin: 2em 0;
+ padding: 1.5em 0;
+ border-top: 1px solid var(--accent-a);
+}
+
+.nav-links {
+ display: flex;
+ justify-content: space-between;
+ gap: 1em;
+}
+
+.nav-previous,
+.nav-next {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ text-decoration: none;
+ color: var(--body-text);
+}
+
+.nav-next {
+ text-align: right;
+}
+
+.nav-label {
+ font-size: 0.9em;
+ color: var(--accent-a);
+}
+
+.nav-title {
+ font-weight: bold;
+ color: var(--link-color);
+}
+
+.nav-previous:hover .nav-title,
+.nav-next:hover .nav-title {
+ text-decoration: underline;
+}
+
+@media (max-width: 768px) {
+ .nav-links {
+ flex-direction: column;
+ }
+
+ .nav-next {
+ text-align: left;
+ }
+}
+
+.foot_separator {
+ margin-top: -1.5em;
+}
class PubviewController < ApplicationController
+ include PostsHelper
before_action :set_promo, only: [:index, :post]
def index
@excerpt = @post.generate_excerpt
@tags = @post.format_tags
@rendered_content = @post.rendered_content
+ # Get adjacent posts for navigation
+ adjacent = adjacent_posts(@post)
+ @previous_post = adjacent[:previous]
+ @next_post = adjacent[:next]
else
@missing = "post"
render 'error/error', status: :not_found, missing: 'post'
module PostsHelper
+ def adjacent_posts(post)
+ {
+ previous: Post.dispatches
+ .where('published_at < ?', post.published_at)
+ .order(published_at: :desc)
+ .first,
+ next: Post.dispatches
+ .where('published_at > ?', post.published_at)
+ .order(published_at: :asc)
+ .first
+ }
+ end
end
class MarkdownRenderer
def self.render(text)
- options = {
- filter_html: true,
- hard_wrap: true,
- link_attributes: { rel: 'nofollow' },
- space_after_headers: true,
- fenced_code_blocks: true
- }
+ options = {
+ filter_html: false,
+ hard_wrap: true,
+ link_attributes: { rel: 'nofollow' },
+ space_after_headers: true,
+ fenced_code_blocks: true
+ }
- extensions = {
- autolink: true,
- superscript: true,
- disable_indented_code_blocks: true,
- highlight: true
- }
+ extensions = {
+ autolink: true,
+ superscript: true,
+ disable_indented_code_blocks: true,
+ highlight: true
+ }
- renderer = Redcarpet::Render::HTML.new(options)
- markdown = Redcarpet::Markdown.new(renderer, extensions)
+ # Just wrap the content in our grid containers
+ renderer = Redcarpet::Render::HTML.new(options)
+ markdown = Redcarpet::Markdown.new(renderer, extensions)
- markdown.render(text)
+ '<div class="post-grid">' +
+ '<div class="main-content">' +
+ markdown.render(text) +
+ '</div>' +
+ '<div class="side-notes"></div>' +
+ '</div>'
end
-end
\ No newline at end of file
+end
<%= link_to 'New API Key', new_api_key_path, class: "button" %>
<%= link_to 'Home', root_path, class: "button" %>
</div>
+
+<%= render partial: "layouts/alert" %>
+
+
<div class="post">
<div class="container">
<table>
</tbody>
</table>
</div>
-</div>
\ No newline at end of file
+</div>
<%= link_to 'Back to API Keys', api_keys_path, class: "button" %>
</div>
+<%= render partial: "layouts/alert" %>
+
<div class="post">
<div class="container">
<p>Click the button below to generate a new API key.</p>
-
+
<%= form_with(model: @api_key, local: true) do |form| %>
<% if @api_key.errors.any? %>
<div id="error_explanation">
<h2><%= pluralize(@api_key.errors.count, "error") %> prohibited this API key from being saved:</h2>
-
+
<ul>
<% @api_key.errors.full_messages.each do |message| %>
<li><%= message %></li>
</ul>
</div>
<% end %>
-
+
<div class="actions">
<%= form.submit "Generate API Key" %>
</div>
<% end %>
</div>
-</div>
\ No newline at end of file
+</div>
<%= link_to 'Back to API Keys', api_keys_path, class: "button" %>
</div>
+<%= render partial: "layouts/alert" %>
+
<div class="post">
<div class="container">
<p>
<strong>Key:</strong>
<%= @api_key.key %>
</p>
-
+
<p>
<strong>Created at:</strong>
<%= @api_key.created_at %>
<% content_for :form_content do %>
<%= form_for(resource, as: resource_name, url: registration_path(resource_name)) do |f| %>
<%= render "devise/shared/error_messages", resource: resource %>
-
- <%= image_tag "aidan_about_u.svg", class: "aidan_about_u", alt: "To get started, tell me about yourself:" %>
-
- <div class="field-group">
- <div class="field">
- <%= f.label :first_name %>
- <%= f.text_field :first_name, autofocus: true %>
- </div>
- <div class="field">
- <%= f.label :last_name %>
- <%= f.text_field :last_name %>
- </div>
- </div>
- <div class="field">
- <%= f.label :email %>
- <%= f.email_field :email, autocomplete: "email" %>
- </div>
+ <h4>Hello, I’m <a href="/about">Aidan</a>, it’s great to meet a likeminded human — </h4>
+ <p class="text-indent">↓ To get started, tell me briefly about yourself:</p>
+
+ <div class="reg-form">
+ <div class="field-group">
+ <div class="field">
+ <%= f.label :first_name %>
+ <%= f.text_field :first_name, autofocus: true, placeholder: "Jennifer" %>
+ </div>
+ <div class="field">
+ <%= f.label :last_name %>
+ <%= f.text_field :last_name, placeholder: "Davies" %>
+ </div>
+ </div>
- <div class="field-group">
- <div class="field">
- <%= f.label :password %>
- <%= f.password_field :password, autocomplete: "new-password", placeholder: @minimum_password_length ? "#{@minimum_password_length} characters minimum" : "" %>
- </div>
-
- <div class="field">
- <%= f.label :password_confirmation %>
- <%= f.password_field :password_confirmation, autocomplete: "new-password" %>
- </div>
+ <div class="field">
+ <%= f.label :email %>
+ <%= f.email_field :email, autocomplete: "email", placeholder: "
[email protected]" %>
+ </div>
+
+ <div class="field-group">
+ <div class="field">
+ <%= f.label :password %>
+ <%= f.password_field :password, autocomplete: "new-password", placeholder: @minimum_password_length ? "#{@minimum_password_length} characters minimum" : "" %>
+ </div>
+
+ <div class="field">
+ <%= f.label :password_confirmation %>
+ <%= f.password_field :password_confirmation, autocomplete: "new-password", placeholder: "Same one again..." %>
+ </div>
+ </div>
</div>
-
- <%= image_tag "aidan_dispatches.svg", class: "aidan_about_u", alt: "Want new dispatches delivered right to you?" %>
-
+
+ <h4>Now comes the choice of a lifetime</h4>
+ <p class="text-indent">↓ <em>Would you like dispatches delivered directly to your email?</em></p>
+
<div class="field checkbox">
<%= f.check_box :subscribe_to_newsletter %>
<%= f.label :subscribe_to_newsletter, "I want to receive mind reader dispatches by email newsletter", id: "sub" %>
</div>
- <p>Note: you may be sent two confirmation emails – one to confirm your registration, one to confirm your mailing list preferences.</p>
+ <p class="note">⚠️ Note: if you opt-in to the email newsletter you will be sent <strong>two confirmation emails</strong> – one to confirm your registration, one to confirm your mailing list preferences – you’ll need to confirm both... sorry!</p>
<% if Rails.env.production? %>
<div class="field">
<% end %>
<% end %>
-<%= render template: 'layouts/user_page_template' %>
\ No newline at end of file
+<%= render template: 'layouts/user_page_template' %>
--- /dev/null
+<% if notice or alert %>
+ <div class="notice">
+ <h3>Notice:</h3>
+ <p><%= notice or alert %></p>
+ </div>
+<% end %>
<% if !@post&.short_dispatch? %>
<% if @post&.title? %><h3>Follow via RSS, Email or Membership...</h3><% end %>
-<p><span class="callout">→ 📬 Want an email for each new post?</span> Join the mailing list for free <%= link_to "right here <abbr title=\"internal link\">↙︎</abbr>".html_safe, "/join/" %>.</p>
-<p><span class="callout">→ 💰 Like this work and want to support it?</span> Get started <%= link_to " here <abbr title=\"internal link\">↙︎</abbr>".html_safe, "/join/" %>.</p>
+<p><span class="callout">→ 📬 Join the community for free</span> receive an email with each new post, or optionally support this work financially <%= link_to "learn more here <abbr title=\"internal link\">↙︎</abbr>".html_safe, "/join/" %>.</p>
<p><span class="callout">→ 📰 Prefer RSS?</span> you can subscribe to a combined <%= link_to "bookmarks+dispatches feed here <abbr title=\"internal link\">↙︎</abbr>".html_safe, rss_path %> or <%= link_to "full text dispatches only feed here <abbr title=\"external link\">↗︎</abbr>".html_safe, dispatches_rss_path %>.</p>
<% end %>
<% if @post %>
<% end %>
</div>
+<%= render partial: "layouts/alert" %>
+
<script type="text/javascript">
document.addEventListener('DOMContentLoaded', function() {
// Create and insert the toggle button
--- /dev/null
+<div class="post-navigation">
+ <div class="nav-links">
+ <% if @previous_post %>
+ <%= link_to public_post_path(@previous_post.published_at.year, @previous_post.slug), class: "nav-previous" do %>
+ <span class="nav-label">← Previous</span>
+ <span class="nav-title"><%= @previous_post.title %></span>
+ <% end %>
+ <% end %>
+
+ <% if @next_post %>
+ <%= link_to public_post_path(@next_post.published_at.year, @next_post.slug), class: "nav-next" do %>
+ <span class="nav-label">Next →</span>
+ <span class="nav-title"><%= @next_post.title %></span>
+ <% end %>
+ <% end %>
+ </div>
+</div>
<% if content_for?(:meta_keywords) %>
<meta name="keywords" content="<%= yield(:meta_keywords) %>">
<% end %>
-
+
<%= csrf_meta_tags %>
<%= csp_meta_tag %>
<%= stylesheet_link_tag "application" %>
-
+
<%= auto_discovery_link_tag(:rss, rss_url, title: "mind reader :: dispatches and bookmarks in summary") %>
<%= auto_discovery_link_tag(:rss, dispatches_rss_url, title: "mind reader :: dispatches full text") %>
-
+
<% if ['posts', 'pages'].include?(controller.controller_name) %>
<link rel="stylesheet" href="/simplemde.min.css">
<script src="/simplemde.min.js"></script>
<% end %>
</head>
- <% if !Rails.env.production? %>
- <div class="marquee"><div class="track">NOT PROD NOT PROD NOT PROD NOT PROD NOT PROD NOT PROD NOT PROD NOT PROD NOT PROD NOT PROD NOT PROD NOT PROD NOT PROD NOT PROD NOT PROD NOT PROD NOT PROD NOT PROD NOT PROD NOT PROD NOT PROD NOT PROD NOT PROD NOT PROD NOT PROD NOT PROD NOT PROD NOT PROD NOT PROD NOT PROD NOT PROD NOT PROD NOT PROD NOT PROD NOT PROD NOT PROD NOT PROD NOT PROD NOT PROD NOT PROD NOT PROD NOT PROD NOT PROD NOT PROD NOT PROD NOT PROD NOT PROD NOT PROD</div></div>
- <% end %>
- <% if notice or alert %>
- <div class="notice">
- <h3>Notice:</h3>
- <p><%= notice or alert %></p>
- </div>
- <% end %>
<body>
- <%= yield %>
+ <% if !Rails.env.production? %>
+ <div class="marquee"><div class="track">NOT PROD NOT PROD NOT PROD NOT PROD NOT PROD NOT PROD NOT PROD NOT PROD NOT PROD NOT PROD NOT PROD NOT PROD NOT PROD NOT PROD NOT PROD NOT PROD NOT PROD NOT PROD NOT PROD NOT PROD NOT PROD NOT PROD NOT PROD NOT PROD NOT PROD NOT PROD NOT PROD NOT PROD NOT PROD NOT PROD NOT PROD NOT PROD NOT PROD NOT PROD NOT PROD NOT PROD NOT PROD NOT PROD NOT PROD NOT PROD NOT PROD NOT PROD NOT PROD NOT PROD NOT PROD NOT PROD NOT PROD NOT PROD</div></div>
+ <% end %>
+ <%= yield %>
<footer>
<p class="account"><% if current_user&.first_name? %>Welcome back, <%= link_to current_user.first_name, edit_user_registration_path %>. <% if current_user&.admin? %>You hold the keys to the kingdom.<% end %> <% if current_user&.paid_user? %> Thank you, your financial support makes this work possible.<% elsif current_user&.active_subscriber? %>Thank you for your ongoing support.<% end %><% else %> You are not logged in. <%= link_to "Change that?", new_user_session_path %><% end %></span></p>
<%= link_to "Back to pages", pages_path, class: "button" %>
</div>
+<%= render partial: "layouts/alert" %>
+
<%= render "form", page: @page %>
<%= link_to "New page", new_page_path, class: "button" %> <%= link_to "Home", root_path, class: "button" %>
</div>
+<%= render partial: "layouts/alert" %>
+
<div class="post">
<div class="container">
<table>
<div class="container">
<%= paginate @pages %>
</div>
-<% end %>
\ No newline at end of file
+<% end %>
<%= link_to "Back to pages", pages_path, class: "button" %>
</div>
-<%= render "form", page: @page %>
+<%= render partial: "layouts/alert" %>
+<%= render "form", page: @page %>
<div class="container">
<% content_for :title, "Viewing page" %>
<h1>Page details</h1>
- <%= link_to "Edit this page", edit_page_path(@page), class: "button" %>
+ <%= link_to "Edit this page", edit_page_path(@page), class: "button" %>
<%= link_to "Back to pages", pages_path, class: "button" %>
<%= button_to "Destroy this page", @page, method: :delete %>
</div>
+<%= render partial: "layouts/alert" %>
+
<div class="post">
<div class="container">
<%= render @page %>
</div>
-</div>
\ No newline at end of file
+</div>
<%= link_to "Back to posts", posts_path, class: "button" %> <%= link_to "Show this post", @post, class: "button" %>
</div>
+<%= render partial: "layouts/alert" %>
+
<div class="post">
<div class="container">
<%= render "form", post: @post %>
</div>
-</div>
\ No newline at end of file
+</div>
<%= link_to "Import Data", importer_path, class: "button" %>
</div>
+<%= render partial: "layouts/alert" %>
+
<div class="post">
<div class="container">
<table>
<%= link_to "Back to posts", posts_path, class: "button" %>
</div>
+<%= render partial: "layouts/alert" %>
+
<div class="post">
- <div class="container">
+ <div class="container">
<%= render "form", post: @post %>
</div>
</div>
+<%= render partial: "layouts/alert" %>
+
<div class="container">
<h1>Post details</h1>
<%= link_to "Back to posts", posts_path, class: "button" %>
</blockquote>
<% else %>
<blockquote class="excerpt"><%= item.generate_excerpt %>...</blockquote>
- <% if item.summary? && current_user.present? && (!current_user&.hide_ai_summaries?) %>
- <blockquote class="excerpt">
- <span class="ai-summary"><abbr title="Machine learning summary via Kagi">ML</abbr> Summary</span><span class="ai-summary-block"><%= item.summary %>
- </blockquote>
- <% end %>
<% end %>
<% else %>
<span class="post-link">
<% content_for :meta_keywords, "#{@post.tags}" %>
<% content_for :meta_published, "#{@post.created_at&.strftime('%Y-%m-%d')}" %>
<% if @post.post_type != "bookmark" %>
- <h1><%= @post.title %></h1>
+ <h3 class="post_siteheading">Aidan Cornelius-Bell’s mind reader:</h3>
+ <h1 class="post_title"><%= @post.title %></h1>
<div class="bookmark-buttons">
<%= link_to "↼ More dispatches 👀", "#{root_path}?filter=posts", class: "button button-bottomless" %>
<% if !current_user %>
<% end %>
</div>
<div class="postmeta">
- <p >
Posted <%= @post.published_at.strftime('%B %d, %Y') %> and tagged <%= raw @post.format_tags %><br><% if [email protected]_dispatch?%>Reading Time: about <%= @reading_time %> minute(s) <% end %> from: Aidan Cornelius-Bell.</p>
+ <p >
<% if [email protected]_dispatch?%>About a <%= @reading_time %> minute read, p<% else %>P<% end %>osted <%= @post.published_at.strftime('%B %d, %Y') %> and tagged <%= raw @post.format_tags %></p>
</div>
<% else %>
<h1 style="margin-bottom: -.8em">Bookmark comment permalink:</h1>
<p class="lead"><%= @promo_strings.sample %> <%= link_to "Start right now!", join_path, class: "lead-button" %></p>
<% end %>
<%= raw @rendered_content %>
+
+ <%= render partial: "layouts/post_navigation" %>
+ <hr class="foot_separator">
</div>
</div>
<%= render partial: "layouts/home_post_links" %>
</div>
<% end %>
+
+<script>
+document.addEventListener('DOMContentLoaded', function() {
+ if (window.innerWidth >= 1024) {
+ const mainContent = document.querySelector('.main-content');
+ const sideNotes = document.querySelector('.side-notes');
+
+ if (mainContent && sideNotes) {
+ // Find all the original footnotes first and store them
+ const footnotes = new Map();
+ const footnoteParagraphs = Array.from(mainContent.querySelectorAll('p'))
+ .filter(p => /^\[\d+\]/.test(p.textContent));
+
+ // Store footnotes and hide originals
+ footnoteParagraphs.forEach(p => {
+ const num = p.textContent.match(/^\[(\d+)\]/)[1];
+ footnotes.set(num, p.innerHTML);
+ p.style.display = 'none';
+ });
+
+ // Find and process reference markers in the main text
+ const paragraphs = Array.from(mainContent.querySelectorAll('p'))
+ .filter(p => !footnoteParagraphs.includes(p)); // Exclude footnote paragraphs
+
+ let lastBottom = 0;
+ const padding = 30; // Increased padding between notes
+
+ paragraphs.forEach(p => {
+ const markers = Array.from(p.textContent.matchAll(/\[(\d+)\]/g));
+ markers.forEach(match => {
+ const num = match[1];
+ if (footnotes.has(num)) {
+ const noteContent = footnotes.get(num);
+ const noteDiv = document.createElement('div');
+ noteDiv.className = 'side-note';
+ noteDiv.innerHTML = noteContent;
+ sideNotes.appendChild(noteDiv);
+
+ // Get the position of the reference
+ const text = p.textContent;
+ const referencePosition = text.indexOf(`[${num}]`);
+ const textBefore = text.substring(0, referencePosition);
+ const tempSpan = document.createElement('span');
+ tempSpan.textContent = textBefore;
+ p.insertBefore(tempSpan, p.firstChild);
+ const spanRect = tempSpan.getBoundingClientRect();
+ p.removeChild(tempSpan);
+
+ // Calculate position
+ const pRect = p.getBoundingClientRect();
+ const containerRect = mainContent.getBoundingClientRect();
+ let proposedTop = pRect.top - containerRect.top +
+ (referencePosition > 0 ? spanRect.height : 0);
+
+ // Ensure no overlap
+ proposedTop = Math.max(proposedTop, lastBottom + padding);
+
+ noteDiv.style.top = `${proposedTop}px`;
+
+ // Update lastBottom after the note is rendered
+ const noteRect = noteDiv.getBoundingClientRect();
+ lastBottom = proposedTop + noteRect.height;
+ }
+ });
+ });
+ }
+ }
+});
+</script>
<% end %>
<p>If you are a mailing list subscriber, <%= link_to "check that your premium status", mailing_lists_path %> has transferred so that you get bonus mail for premium members. Please be aware that data here are slightly delayed – don’t panic if things are not yet up to date!</p>
</div>
-
+
<% if @portal_session %>
<p>
<%= link_to "Manage your subscription", @portal_session.url, class: "button", data: { turbo: false } %>
<% if @subscription_status != 'active' %>
<div class="subscription-actions">
- <%= link_to "Subscribe", new_subscription_path, class: "button primary" %>
+ <%= link_to "Become a supporter", new_subscription_path, class: "button primary" %>
</div>
<% end %>
<% end %>
-<%= render template: 'layouts/user_page_template' %>
\ No newline at end of file
+<%= render template: 'layouts/user_page_template' %>
</tbody>
</table>
</div>
-</div>
\ No newline at end of file
+</div>
<%= link_to 'Back to Users', user_manager_index_path, class: "button" %>
</div>
+<%= render partial: "layouts/alert" %>
+
<div class="post">
<div class="container">
<dl>
<dt>Email:</dt>
<dd><%= @user.email %></dd>
-
+
<dt>Name:</dt>
<dd><%= @user.full_name %></dd>
-
+
<dt>Admin:</dt>
<dd><%= @user.admin? ? 'Yes' : 'No' %></dd>
-
+
<dt>2FA Enabled:</dt>
<dd><%= @user.otp_required_for_login? ? 'Yes' : 'No' %></dd>
-
+
<dt>Subscription Status:</dt>
<dd><%= @user.subscription_status %></dd>
-
+
<dt>Mailing List Status:</dt>
<dd><%= @user.buttondown_status %></dd>
-
+
<dt>Support Type:</dt>
<dd><%= @user.support_type %></dd>
-
+
<dt>Last Payment Date:</dt>
<dd><%= @user.last_payment_at&.to_s || 'N/A' %></dd>
-
+
<dt>Last Payment Amount:</dt>
<dd><%= number_to_currency(@user.payment_amount) if @user.payment_amount.present? %></dd>
</dl>
</div>
-</div>
\ No newline at end of file
+</div>