numerous fixes, blog index, paging, tag index, post show - all liveview
This commit is contained in:
parent
2218a678b1
commit
27a8c22e9f
10 changed files with 178 additions and 177 deletions
|
@ -47,12 +47,21 @@ body {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
a {
|
a {
|
||||||
color: $secondary;
|
color: $gray-100;
|
||||||
|
border-bottom: $secondary 2px solid;
|
||||||
|
text-decoration: none;
|
||||||
|
&.navbar-brand {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
&:visited {
|
&:visited {
|
||||||
color: $info;
|
color: $info;
|
||||||
|
.post-title & {
|
||||||
|
color: $gray-100;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
&:hover {
|
&:hover {
|
||||||
color: $primary;
|
color: $primary;
|
||||||
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.border-gray-900 {
|
.border-gray-900 {
|
||||||
|
@ -94,6 +103,7 @@ a {
|
||||||
|
|
||||||
/* social icons */
|
/* social icons */
|
||||||
#social-icons .link-light {
|
#social-icons .link-light {
|
||||||
|
border-bottom: none;
|
||||||
color: $gray-100;
|
color: $gray-100;
|
||||||
&:hover {
|
&:hover {
|
||||||
color: $primary;
|
color: $primary;
|
||||||
|
@ -128,9 +138,10 @@ a {
|
||||||
.post-title a {
|
.post-title a {
|
||||||
color: $gray-100;
|
color: $gray-100;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
border-bottom: none;
|
||||||
&:hover {
|
&:hover {
|
||||||
color: $primary;
|
color: $primary;
|
||||||
text-decoration: underline;
|
border-bottom: $secondary 3px solid;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.post-lede,
|
.post-lede,
|
||||||
|
|
|
@ -3,21 +3,29 @@ defmodule Home73k.Blog do
|
||||||
|
|
||||||
Application.ensure_all_started(:earmark)
|
Application.ensure_all_started(:earmark)
|
||||||
|
|
||||||
posts_paths = "#{Home73k.app_blog_content()}/**/*.md" |> Path.wildcard()
|
post_paths = "#{Home73k.app_blog_content()}/**/*.md" |> Path.wildcard()
|
||||||
|
post_paths_hash = :erlang.md5(post_paths)
|
||||||
|
|
||||||
posts =
|
posts =
|
||||||
for post_path <- posts_paths do
|
for post_path <- post_paths do
|
||||||
@external_resource Path.relative_to_cwd(post_path)
|
@external_resource Path.relative_to_cwd(post_path)
|
||||||
Post.parse!(post_path)
|
Post.parse!(post_path)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def __mix_recompile__?() do
|
||||||
|
Path.wildcard("#{Home73k.app_blog_content()}/**/*.md") |> :erlang.md5() != unquote(post_paths_hash)
|
||||||
|
end
|
||||||
|
|
||||||
@posts Enum.sort_by(posts, & &1.date, {:desc, Date})
|
@posts Enum.sort_by(posts, & &1.date, {:desc, Date})
|
||||||
|
@post_count length(@posts)
|
||||||
|
|
||||||
@tags posts |> Stream.flat_map(& &1.tags) |> Stream.uniq() |> Enum.sort()
|
@tags posts |> Stream.flat_map(& &1.tags) |> Stream.uniq() |> Enum.sort()
|
||||||
|
|
||||||
def list_posts, do: @posts
|
def list_posts, do: @posts
|
||||||
def list_tags, do: @tags
|
def list_tags, do: @tags
|
||||||
|
|
||||||
|
def post_count, do: @post_count
|
||||||
|
|
||||||
defmodule NotFoundError do
|
defmodule NotFoundError do
|
||||||
defexception [:message, plug_status: 404]
|
defexception [:message, plug_status: 404]
|
||||||
end
|
end
|
||||||
|
@ -29,10 +37,10 @@ defmodule Home73k.Blog do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# def get_posts_by_tag!(tag) do
|
def get_posts_by_tag!(tag) do
|
||||||
# case Enum.filter(list_posts(), &(tag in &1.tags)) do
|
case Enum.filter(list_posts(), &(tag in &1.tags)) do
|
||||||
# [] -> raise NotFoundError, "posts with tag=#{tag} not found"
|
[] -> raise NotFoundError, "posts with tag=#{tag} not found"
|
||||||
# posts -> posts
|
posts -> posts
|
||||||
# end
|
end
|
||||||
# end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -93,9 +93,6 @@ defmodule Home73kWeb do
|
||||||
# Import SVG Icon helper
|
# Import SVG Icon helper
|
||||||
import Home73kWeb.IconHelpers
|
import Home73kWeb.IconHelpers
|
||||||
|
|
||||||
# Import Date formatter helper
|
|
||||||
import Home73kWeb.DateHelpers
|
|
||||||
|
|
||||||
import Home73kWeb.ErrorHelpers
|
import Home73kWeb.ErrorHelpers
|
||||||
import Home73kWeb.Gettext
|
import Home73kWeb.Gettext
|
||||||
alias Home73kWeb.Router.Helpers, as: Routes
|
alias Home73kWeb.Router.Helpers, as: Routes
|
||||||
|
|
|
@ -3,49 +3,96 @@ defmodule Home73kWeb.BlogLive do
|
||||||
|
|
||||||
alias Home73k.Blog
|
alias Home73k.Blog
|
||||||
|
|
||||||
|
@page_size 7
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def mount(_params, _session, socket) do
|
def mount(_params, _session, socket) do
|
||||||
socket
|
{:ok, socket}
|
||||||
|> live_okreply()
|
|
||||||
end
|
end
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def handle_params(_params, _url, socket) do
|
def handle_params(params, _url, socket) do
|
||||||
socket
|
socket.assigns.live_action
|
||||||
|> assign(:page_title, "Blog")
|
|> init_per_live_action(socket, params)
|
||||||
|> assign(:posts, Blog.list_posts())
|
|
||||||
|> live_noreply()
|
|> live_noreply()
|
||||||
end
|
end
|
||||||
|
|
||||||
# @impl true
|
defp page_param_as_int(page) do
|
||||||
# def handle_event("suggest", %{"q" => query}, socket) do
|
try do
|
||||||
# {:noreply, assign(socket, results: search(query), query: query)}
|
String.to_integer(page)
|
||||||
# end
|
rescue
|
||||||
|
_ -> nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
# @impl true
|
defp raise_not_found(msg), do: raise Home73k.Blog.NotFoundError, msg
|
||||||
# def handle_event("search", %{"q" => query}, socket) do
|
|
||||||
# case search(query) do
|
|
||||||
# %{^query => vsn} ->
|
|
||||||
# {:noreply, redirect(socket, external: "https://hexdocs.pm/#{query}/#{vsn}")}
|
|
||||||
|
|
||||||
# _ ->
|
defp init_per_live_action(:index, socket, _params) do
|
||||||
# {:noreply,
|
socket
|
||||||
# socket
|
|> assign(:page_title, "Blog")
|
||||||
# |> put_flash(:error, "No dependencies found matching \"#{query}\"")
|
|> assign(:posts, get_posts_for_page!(1))
|
||||||
# |> assign(results: %{}, query: query)}
|
|> assign(:page_count, get_page_count())
|
||||||
# end
|
|> assign_prev_next(1)
|
||||||
# end
|
end
|
||||||
|
|
||||||
# defp search(query) do
|
defp init_per_live_action(:page, socket, %{"page" => page}) do
|
||||||
# if not Home73kWeb.Endpoint.config(:code_reloader) do
|
page_int = page_param_as_int(page)
|
||||||
# raise "action disabled when not in development"
|
page_count = get_page_count()
|
||||||
# end
|
|
||||||
|
|
||||||
# for {app, desc, vsn} <- Application.started_applications(),
|
cond do
|
||||||
# app = to_string(app),
|
is_nil(page_int) || page_int <= 1 ->
|
||||||
# String.starts_with?(app, query) and not List.starts_with?(desc, ~c"ERTS"),
|
push_patch(socket, to: Routes.blog_path(socket, :index))
|
||||||
# into: %{},
|
|
||||||
# do: {app, vsn}
|
|
||||||
# end
|
|
||||||
|
|
||||||
|
page_int > page_count ->
|
||||||
|
raise_not_found("there are only #{page_count} pages of posts")
|
||||||
|
|
||||||
|
true ->
|
||||||
|
posts = get_posts_for_page!(page_int)
|
||||||
|
|
||||||
|
socket
|
||||||
|
|> assign(:page_title, "Blog \\ Page #{page}")
|
||||||
|
|> assign(:posts, posts)
|
||||||
|
|> assign(:page_count, page_count)
|
||||||
|
|> assign_prev_next(page_int)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp init_per_live_action(:show, socket, %{"id" => id}) do
|
||||||
|
post = Blog.get_post_by_id!(id)
|
||||||
|
socket
|
||||||
|
|> assign(:page_title, "Blog \\ post.title")
|
||||||
|
|> assign(:posts, [post])
|
||||||
|
|> assign(:page_count, nil)
|
||||||
|
|> assign_prev_next(0)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp init_per_live_action(:tag, socket, %{"tag" => tag}) do
|
||||||
|
socket
|
||||||
|
|> assign(:page_title, "Blog \\ ##{tag}")
|
||||||
|
|> assign(:posts, Blog.get_posts_by_tag!(tag))
|
||||||
|
|> assign(:page_count, get_page_count())
|
||||||
|
|> assign_prev_next(1)
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
defp get_posts_for_page!(1), do: Blog.list_posts() |> Enum.take(@page_size)
|
||||||
|
|
||||||
|
defp get_posts_for_page!(page_int) do
|
||||||
|
Blog.list_posts()
|
||||||
|
|> Stream.chunk_every(@page_size)
|
||||||
|
|> Enum.at(page_int - 1)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp get_page_count, do: Integer.floor_div(Blog.post_count(), @page_size) + rem(Blog.post_count(), @page_size)
|
||||||
|
|
||||||
|
defp assign_prev_next(socket, page_int) do
|
||||||
|
socket
|
||||||
|
|> assign(:page_prev, page_int < socket.assigns.page_count && page_int + 1 || nil)
|
||||||
|
|> assign(:page_next, page_int > 1 && page_int - 1 || nil)
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
def format_date(date) do
|
||||||
|
Calendar.strftime(date, "%B %-d, %Y")
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -2,38 +2,74 @@
|
||||||
|
|
||||||
<div class="col-12 col-md-10 col-lg-9 col-xl-8 col-xxl-7 pb-2 mb-4 mt-3">
|
<div class="col-12 col-md-10 col-lg-9 col-xl-8 col-xxl-7 pb-2 mb-4 mt-3">
|
||||||
|
|
||||||
<%= for post <- @posts do %>
|
<%= if is_nil(@posts) do %>
|
||||||
|
|
||||||
<div class="post border-bottom border-gray pb-4 mb-3">
|
<div class="post border-bottom border-gray pb-4 mb-3">
|
||||||
|
|
||||||
<h2 class="post-title fs-2 fw-600 mb-2">
|
<h2 class="post-title fs-2 fw-600 mb-2">Nothing found.</h2>
|
||||||
<%= live_redirect "#{post.title}", to: Routes.post_path(@socket, :show, post) %>
|
|
||||||
</h2>
|
|
||||||
|
|
||||||
<div class="post-date font-monospace text-gray-400 <%= if length(post.tags) == 0, do: "mb-3" %>">
|
|
||||||
<%= icon_div @socket, "mdi-calendar-clock", [class: "icon baseline me-2"] %><%= post.date |> format_date() %>
|
|
||||||
by <%= icon_div @socket, "mdi-account", [class: "icon baseline me-1"] %>Adam Piontek
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<%= if length(post.tags) > 0 do %>
|
<% else %>
|
||||||
<div class="post-tags fs-smaller mb-3">
|
|
||||||
<%= icon_div @socket, "mdi-tag-multiple", [class: "icon baseline"] %>
|
<%= for post <- @posts do %>
|
||||||
<%= for {tag, i} <- Enum.with_index(post.tags) do %>
|
|
||||||
#<%= tag %><%= i < (length(post.tags) - 1) && "," || "" %>
|
<div class="post border-bottom border-gray pb-4 mb-3">
|
||||||
<% end %>
|
|
||||||
|
<h2 class="post-title fs-2 fw-600 mb-2">
|
||||||
|
<%= live_redirect "#{post.title}", to: Routes.blog_path(@socket, :show, post) %>
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<div class="post-date font-monospace text-gray-400 <%= if length(post.tags) == 0, do: "mb-3" %>">
|
||||||
|
<%= icon_div @socket, "mdi-calendar-clock", [class: "icon baseline me-2"] %><%= format_date(post.date) %>
|
||||||
|
by <%= icon_div @socket, "mdi-account", [class: "icon baseline me-1"] %>Adam Piontek
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<%= if length(post.tags) > 0 do %>
|
||||||
|
<div class="post-tags fs-smaller mb-3">
|
||||||
|
<%= icon_div @socket, "mdi-tag-multiple", [class: "icon baseline text-gray-400"] %>
|
||||||
|
<%= for {tag, i} <- Enum.with_index(post.tags) do %>
|
||||||
|
<span class="text-gray-400">#</span><%= live_redirect tag, to: Routes.blog_path(@socket, :tag, tag) %><%= i < (length(post.tags) - 1) && "," || "" %>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<div class="post-lede">
|
||||||
|
<%= raw post.lede %>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<%= if @live_action == :show do %>
|
||||||
|
<div class="post-body">
|
||||||
|
<%= raw post.body %>
|
||||||
|
</div>
|
||||||
|
<% else %>
|
||||||
|
<p>
|
||||||
|
<%= live_redirect raw("Read more…"), to: Routes.blog_path(@socket, :show, post), class: "fs-6" %>
|
||||||
|
</p>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<%= if @live_action in [:index, :page] do %>
|
||||||
|
<nav class="d-flex justify-content-between" aria-label="Page navigation">
|
||||||
|
<%= if @page_prev do %>
|
||||||
|
<%= live_patch to: Routes.blog_path(@socket, :page, @page_prev) do %>
|
||||||
|
← Older
|
||||||
|
<% end %>
|
||||||
|
<% else %>
|
||||||
|
<div class="d-block"></div>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
<div class="post-lede">
|
<%= if @page_next do %>
|
||||||
<%= raw post.lede %>
|
<%= live_patch to: @page_next == 1 && Routes.blog_path(@socket, :index) || Routes.blog_path(@socket, :page, @page_next) do %>
|
||||||
</div>
|
Newer →
|
||||||
|
<% end %>
|
||||||
<p>
|
<% end %>
|
||||||
<%= live_redirect raw("Read more…"), to: Routes.post_path(@socket, :show, post), class: "fs-6" %>
|
</nav>
|
||||||
</p>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,58 +0,0 @@
|
||||||
defmodule Home73kWeb.PostLive do
|
|
||||||
use Home73kWeb, :live_view
|
|
||||||
|
|
||||||
alias Home73k.Blog
|
|
||||||
|
|
||||||
@impl true
|
|
||||||
def mount(%{"id" => id}, session, socket) do
|
|
||||||
# IO.inspect(params, label: "postlive params")
|
|
||||||
IO.inspect(session, label: "postlive session")
|
|
||||||
|
|
||||||
post = Blog.get_post_by_id!(id)
|
|
||||||
|
|
||||||
socket
|
|
||||||
|> assign(:page_title, "Blog \\ #{post.title}")
|
|
||||||
|> assign(:post, post)
|
|
||||||
|> live_okreply()
|
|
||||||
end
|
|
||||||
|
|
||||||
# @impl true
|
|
||||||
# def handle_params(params, _url, socket) do
|
|
||||||
# socket
|
|
||||||
# |> assign(:page_title, "Blog")
|
|
||||||
# |> assign(:posts, Blog.list_posts())
|
|
||||||
# |> live_noreply()
|
|
||||||
# end
|
|
||||||
|
|
||||||
# @impl true
|
|
||||||
# def handle_event("suggest", %{"q" => query}, socket) do
|
|
||||||
# {:noreply, assign(socket, results: search(query), query: query)}
|
|
||||||
# end
|
|
||||||
|
|
||||||
# @impl true
|
|
||||||
# def handle_event("search", %{"q" => query}, socket) do
|
|
||||||
# case search(query) do
|
|
||||||
# %{^query => vsn} ->
|
|
||||||
# {:noreply, redirect(socket, external: "https://hexdocs.pm/#{query}/#{vsn}")}
|
|
||||||
|
|
||||||
# _ ->
|
|
||||||
# {:noreply,
|
|
||||||
# socket
|
|
||||||
# |> put_flash(:error, "No dependencies found matching \"#{query}\"")
|
|
||||||
# |> assign(results: %{}, query: query)}
|
|
||||||
# end
|
|
||||||
# end
|
|
||||||
|
|
||||||
# defp search(query) do
|
|
||||||
# if not Home73kWeb.Endpoint.config(:code_reloader) do
|
|
||||||
# raise "action disabled when not in development"
|
|
||||||
# end
|
|
||||||
|
|
||||||
# for {app, desc, vsn} <- Application.started_applications(),
|
|
||||||
# app = to_string(app),
|
|
||||||
# String.starts_with?(app, query) and not List.starts_with?(desc, ~c"ERTS"),
|
|
||||||
# into: %{},
|
|
||||||
# do: {app, vsn}
|
|
||||||
# end
|
|
||||||
|
|
||||||
end
|
|
|
@ -1,36 +0,0 @@
|
||||||
<main class="container d-flex justify-content-center">
|
|
||||||
|
|
||||||
<div class="col-12 col-md-10 col-lg-9 col-xl-8 col-xxl-7 pb-2 mb-4 mt-3">
|
|
||||||
|
|
||||||
<div class="post border-bottom border-gray pb-4 mb-3">
|
|
||||||
|
|
||||||
<h2 class="post-title fs-2 fw-normal mb-2"><%= raw @post.title %></h2>
|
|
||||||
|
|
||||||
<div class="post-date font-monospace text-gray-400 <%= if length(@post.tags) == 0, do: "mb-3" %>">
|
|
||||||
<%= icon_div @socket, "mdi-calendar-clock", [class: "icon baseline me-2"] %><%= @post.date |> format_date() %>
|
|
||||||
by <%= icon_div @socket, "mdi-account", [class: "icon baseline me-1"] %>Adam Piontek
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<%= if length(@post.tags) > 0 do %>
|
|
||||||
<div class="post-tags fs-smaller mb-3">
|
|
||||||
<%= icon_div @socket, "mdi-tag-multiple", [class: "icon baseline"] %>
|
|
||||||
<%= for {tag, i} <- Enum.with_index(@post.tags) do %>
|
|
||||||
#<%= tag %><%= i < (length(@post.tags) - 1) && "," || "" %>
|
|
||||||
<% end %>
|
|
||||||
</div>
|
|
||||||
<% end %>
|
|
||||||
|
|
||||||
<div class="post-lede">
|
|
||||||
<%= raw @post.lede %>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="post-body">
|
|
||||||
<%= raw @post.body %>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</main>
|
|
|
@ -17,13 +17,17 @@ defmodule Home73kWeb.Router do
|
||||||
scope "/", Home73kWeb do
|
scope "/", Home73kWeb do
|
||||||
pipe_through :browser
|
pipe_through :browser
|
||||||
|
|
||||||
|
# Pages
|
||||||
get "/", HomeController, :index
|
get "/", HomeController, :index
|
||||||
get "/about", HomeController, :about
|
get "/about", HomeController, :about
|
||||||
get "/resume", HomeController, :resume
|
get "/resume", HomeController, :resume
|
||||||
get "/folio", HomeController, :folio
|
get "/folio", HomeController, :folio
|
||||||
|
|
||||||
|
# Blog
|
||||||
live "/blog", BlogLive, :index
|
live "/blog", BlogLive, :index
|
||||||
# live "/blog/page/:page", BlogLive, :older
|
live "/blog/page/:page", BlogLive, :page
|
||||||
live "/blog/:id", PostLive, :show
|
live "/blog/tag/:tag", BlogLive, :tag
|
||||||
|
live "/blog/:id", BlogLive, :show
|
||||||
end
|
end
|
||||||
|
|
||||||
# Other scopes may use custom stacks.
|
# Other scopes may use custom stacks.
|
||||||
|
|
|
@ -1,9 +0,0 @@
|
||||||
defmodule Home73kWeb.DateHelpers do
|
|
||||||
@moduledoc """
|
|
||||||
Formatters for dates
|
|
||||||
"""
|
|
||||||
|
|
||||||
def format_date(date) do
|
|
||||||
Calendar.strftime(date, "%B %-d, %Y")
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -21,10 +21,11 @@ Naturally I ended up writing some alternate lyrics. There's only two; I never go
|
||||||
- ["Florida Cargo" (to the tune of "Florida Kilos")](#florida-cargo)
|
- ["Florida Cargo" (to the tune of "Florida Kilos")](#florida-cargo)
|
||||||
- ["The Other Schooner" (to the tune of "The Other Woman")](#other-schooner)
|
- ["The Other Schooner" (to the tune of "The Other Woman")](#other-schooner)
|
||||||
|
|
||||||
#### "Florida Cargo" (to the tune of "Florida Kilos")
|
|
||||||
|
|
||||||
<a name="florida-cargo"></a>
|
<a name="florida-cargo"></a>
|
||||||
|
|
||||||
|
#### "Florida Cargo" (to the tune of "Florida Kilos")
|
||||||
|
|
||||||
White shine, royal sailor, doubloons,
|
White shine, royal sailor, doubloons,
|
||||||
Don't you see them gleam,
|
Don't you see them gleam,
|
||||||
They're special, just for you.
|
They're special, just for you.
|
||||||
|
@ -103,10 +104,10 @@ White shine, royal sailor,
|
||||||
Gold teeth, royal sailor, yeah,
|
Gold teeth, royal sailor, yeah,
|
||||||
Drink the night away.
|
Drink the night away.
|
||||||
|
|
||||||
#### "The Other Schooner" (to the tune of "The Other Woman")
|
|
||||||
|
|
||||||
<a name="other-schooner"></a>
|
<a name="other-schooner"></a>
|
||||||
|
|
||||||
|
#### "The Other Schooner" (to the tune of "The Other Woman")
|
||||||
|
|
||||||
The other schooner has time to trim and furl her sails
|
The other schooner has time to trim and furl her sails
|
||||||
The other schooner is perfect where her rival fails
|
The other schooner is perfect where her rival fails
|
||||||
And she's never seen with caulking in her hull anywhere
|
And she's never seen with caulking in her hull anywhere
|
||||||
|
|
Loading…
Reference in a new issue