Compare commits

..

No commits in common. "7265184fc5222d95b84f8e9545486b19946bd4e3" and "82276d6a488dd86888079ede5addb81b27470b7a" have entirely different histories.

13 changed files with 582 additions and 419 deletions

View file

@ -16,14 +16,3 @@ Ready to run in production? Please [check our deployment guides](https://hexdocs
* Docs: https://hexdocs.pm/phoenix
* Forum: https://elixirforum.com/c/phoenix-forum
* Source: https://github.com/phoenixframework/phoenix
## Dev secret template
```
import Config
directus_url = ""
directus_token = ""
config :mse25, :directus, base_url: directus_url, token: directus_token
```

View file

@ -1,222 +1,580 @@
/*
Main CSS file for madr.se
If you have any questions regarding the CSS, feel free
to contact me: yttan at madr dot se
Table of contents, 7-1 inspired
1. Base
2. Components
3. Layout
4. Pages
5. Themes
6. Vendors
7. Shame
*/
/* === 1. Base === */
/*
Normalize and resets.
Only properties, element and attribute selectors are allowed in this
section.
All dynamic values that should change according to user preferences
(dark or light color mode, reduced motion etc) and agent abilities
(small handheld screen, big desktop screen) are handled by properties
when appliable.
This is to avoid redeclarating CSS rules.
*/
:root {
/* colors, dark mode default */
--color: hsl(0 0 90%);
--bgcolor: hsl(180 75% 6%);
--tree-item-accent-color: dimgrey;
--panel-bg-color: hsla(0 0 50% / 0.16);
--monospace-color: springgreen;
--monospace-color-inline: seagreen;
--a-color: gold;
/* typography, mobile first */
--base-font-size: 1.33em;
--page-title-font-size: 2em;
--tree-font-size: 0.85em;
--section-heading-lv2-font-size: 1.5em;
--section-heading-lv3-font-size: 1.2em;
--section-heading-lv4-font-size: 1em;
--system-serif-fonts: Cambria, Cochin, Georgia, Times, "Times New Roman",
serif;
--system-sansserif-fonts: apple-system, system-ui, BlinkMacSystemFont,
Segoe UI, Roboto, Helvetica Neue, Arial, sans-serif;
--monospace-fonts: "JetBrains mono", monaco, menlo, meslo, "Courier New",
Courier, monospace;
/* whitespace */
--gap-sm: 0.5em;
--gap-md: 1em;
--gap-lg: 3em;
/* transitions */
--animation-duration: 0.5s;
/* dimensions and aspect ratios */
--map-ratio: 1;
@media (min-width: 666px) {
--base-font-size: 1.66em;
--tree-font-size: 0.67em;
}
}
html {
color: var(--color);
background-color: var(--bgcolor);
font: normal var(--base-font-size) / 1.5 var(--system-sansserif-fonts);
}
body {
background: #222;
margin: 0;
background-image: linear-gradient(
175deg,
#212223,
#222 350px,
#302928 345px,
#282828
);
font-family:
system-ui,
-apple-system,
BlinkMacSystemFont,
"Segoe UI",
Roboto,
Oxygen,
Ubuntu,
Cantarell,
"Open Sans",
"Helvetica Neue",
sans-serif;
> footer > p {
margin-top: var(--gap-lg);
color: #666;
font-size: 0.66em;
}
}
a {
color: var(--a-color);
}
input,
button {
font-size: 1.2em;
padding: 0.25em;
}
h1,
h2,
h3 {
font-family: var(--system-serif-fonts);
}
h1 {
font-size: 1.75em;
letter-spacing: -0.066em;
margin: 0.5em 0;
line-height: 0.95;
font-size: var(--page-title-font-size);
@media (min-width: 666px) {
text-transform: lowercase;
margin-top: 0.5em;
margin-bottom: 0.5em;
text-align: right;
line-height: 0.9;
color: var(--tree-item-accent-color);
font-weight: normal;
}
}
h2,
h3 {
margin-top: 2em;
line-height: 1.1;
}
h2 {
font-size: 1.33em;
font-size: var(--section-heading-lv2-font-size);
border-bottom: 3px solid var(--panel-bg-color);
}
p,
li {
line-height: 1.66em;
h3 {
font-size: var(--section-heading-lv3-font-size);
}
h3,
h4 {
font-size: 1em;
}
main {
margin: 0 0 0 13em;
max-width: 37em;
}
.cards {
display: flex;
flex-direction: column;
gap: 1.33em;
}
.card {
background-color: #fff;
border: 2px solid #444;
padding: 3em;
border-radius: 9px;
box-shadow: 0 0 5px #000;
:first-child {
margin-top: 0;
}
:last-child {
margin-bottom: 0;
}
&.collapsed {
border-top-left-radius: 0;
border-top-right-radius: 0;
}
font-size: var(--section-heading-lv4-font-size);
}
pre {
padding: 0.5em;
background-color: #1e2025;
color: #96df71;
position: relative;
margin: 1em 2em;
margin: 2em 0;
background-color: #022;
color: var(--monospace-color);
overflow-y: auto;
padding: 0.66em;
box-shadow: 4px 4px 0 var(--panel-bg-color);
position: relative;
line-height: 1.2;
font-size: 0.8em;
button {
> button {
font-size: 0.75em;
position: absolute;
top: 2px;
right: 2px;
top: 0.25em;
right: 0.25em;
}
}
code {
font-family: var(--monospace-fonts);
&.inline {
color: var(--monospace-color-inline);
background: #f3f3f3;
font-size: 0.9em;
}
}
section {
position: relative;
& > h2 {
background: var(--bgcolor);
color: var(--color);
padding: 0.5em 0.25em;
border-bottom: 0;
}
}
img {
max-width: 100%;
display: block;
height: auto;
}
ul,
ol {
clear: left;
}
p {
margin: 1em 0;
}
article {
line-height: 1.33;
}
figure {
margin: 0;
}
figcaption {
text-align: center;
margin-top: 0.5em;
}
table {
width: 100%;
}
td,
th {
background-color: var(--background-color-l);
padding: 0.25em;
font-size: 0.8em;
border: 1px solid rgb(128, 128, 128, 0.5);
}
th {
background-color: var(--background-color-ll);
text-transform: uppercase;
color: var(--em-color);
font-weight: normal;
}
li {
color: var(--em-color);
margin: 0.25em 0;
}
li:first-child {
margin-top: 0;
}
li:last-child {
margin-bottom: 0;
}
li::marker {
color: var(--link-color);
}
blockquote {
color: var(--em-color);
font-size: 1.2em;
line-height: 1.2;
font-style: italic;
border-left: 5px solid var(--background-color-l);
margin: 1em 1em 1em 0;
padding-left: 1em;
}
blockquote p::after,
blockquote p::before {
content: '"';
}
/* === /Base === */
/* === 2. Components === */
/*
Use kebab case named classes to identify components, and nesting
to group subcomponents.
Element selectors are preferred as subcomponents, due to the simple
nature of this site. As a general rule though, classes are the most
versatile.
*/
.home-search,
.profiles {
font-size: var(--tree-font-size);
}
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
border: 0;
left: -999em;
}
.breadcrumbs {
border-radius: 8px 8px 0 0;
padding: 1em;
> .trail > span::after {
content: "/";
}
background: #080808;
background-image: linear-gradient(
#080808 0,
#080808 50%,
#161616 50%,
#080808 100%
);
color: #fff;
.flx {
display: flex;
justify-content: space-between;
align-items: center;
}
a {
color: #fff;
.sticky {
position: sticky;
top: 0;
}
.interactive-map {
aspect-ratio: var(--map-ratio);
}
.home-h1 {
font-size: 1.33em;
}
.list-link {
&::after {
content: " →";
}
}
.head {
color: #fff;
background-color: #345;
padding: 1em 2em;
border-radius: 0 0 8px 8px;
margin-bottom: 1.5em;
:first-child {
margin-top: 0;
}
a {
color: #fff;
.feed-link {
&::after {
content: " ↗";
}
}
.skiplink {
position: absolute;
top: -1.75em;
left: 1em;
background-color: #ff0;
color: #000;
text-decoration: none;
padding: 0.25em;
transition: top 0.25s ease-out;
top: -5em;
transition: top var(--animation-duration) ease-out;
padding: 0.25em 0.5em;
&:focus {
top: 1em;
}
}
nav {
.tree {
list-style: none;
margin: 0;
padding: 0;
display: flex;
margin: 2em 0 1em;
flex-direction: column;
gap: 0.66em;
font-size: var(--tree-font-size);
> ul {
list-style: none;
> li {
text-align: center;
display: grid;
grid-template-columns: 50px 1fr auto;
align-items: center;
margin: 0;
padding: 0;
display: flex;
gap: 1.5em;
background: #111;
padding: 0.33em 1.5em 0.25em;
border-radius: 0 1.33em 1.33em 0;
border: 2px solid #333;
border-left-width: 0;
padding-left: 13em;
}
gap: 0.5em;
padding: 0.75em;
min-height: 50px;
background-color: rgba(128, 128, 128, 0.1);
border: 1px solid rgba(192, 192, 192, 0.1);
a {
text-decoration: none;
color: #fff;
line-height: 1;
}
}
&:focus-within {
background-color: rgba(128, 128, 128, 0.25);
}
.anders {
img {
position: fixed;
top: 1em;
left: 1em;
aspect-ratio: 1;
border-radius: 50%;
width: 175px;
&:hover {
animation: 1s ease-out 0s infinite alternate burst;
> small {
opacity: 0.66;
font-family: var(--monospace-fonts);
font-size: 0.66em;
}
}
img + img {
top: 12.5em;
left: 5em;
width: 125px;
> .article {
--tree-item-accent-color: rebeccapurple;
}
img + img + img {
top: 18.5em;
left: 0.5em;
width: 90px;
> .album {
--tree-item-accent-color: goldenrod;
}
> .link {
--tree-item-accent-color: honeydew;
}
> .events {
--tree-item-accent-color: firebrick;
}
a {
color: var(--color);
text-decoration: none;
flex: 1;
&:hover,
&:focus {
text-decoration: underline;
}
}
}
.bookmarks {
.landing {
display: flex;
flex-direction: column;
gap: 2em;
align-items: center;
padding: 2em 0;
box-sizing: border-box;
gap: 1.66em;
}
@keyframes burst {
0% {
transform: scale(1);
.breadcrumbs {
display: block;
margin: var(--gap-sm) 0;
padding: var(--gap-sm);
border: 1px solid rgb(128, 128, 128, 0.25);
background-color: var(--panel-bg-color);
border-radius: 0;
> span {
display: inline;
&:after {
content: " /";
}
}
50% {
transform: scale(1.2);
}
100% {
transform: scale(0.8);
a {
color: var(--color);
}
}
.months {
grid-auto-flow: rows;
display: grid;
list-style: none;
grid-template-columns: repeat(3, 1fr);
gap: 0.5em;
padding-left: 0;
margin: 2.75em 0;
> li {
margin: 0;
}
}
.article {
> div > p:first-child::first-letter {
@media (min-width: 500px) {
float: left;
font-size: 7em;
border: 8px double hsl(0 0 50%);
padding: 0.1em;
margin-right: 0.066em;
font-style: normal;
font-family: var(--system-serif-fonts);
}
}
> footer {
font-style: italic;
font-size: 0.8em;
text-align: right;
> p {
margin-top: 2.75em;
margin-bottom: 0;
}
}
}
.articles {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 3em;
grid-auto-rows: 1fr;
> * {
display: flex;
flex-direction: column;
justify-content: flex-end;
border-top: 6px solid red;
&:focus-within {
background-color: #333;
}
}
time {
border-top: 1px solid crimson;
padding: 0.25em 0.5em;
}
}
.links {
> :is(h2, h3) {
margin-top: 3em;
}
}
.brutal-legend {
display: flex;
gap: 1em;
flex-direction: row-reverse;
> p {
flex: 1;
margin: 0;
}
> img {
aspect-ratio: 1;
}
}
.profiles {
display: flex;
gap: 1.66em;
list-style: none;
margin: 0;
> li {
margin: 0;
padding: 0;
}
}
/* === /Components === */
/* === 3. Layout === */
/*
Containers and wrappers for components.
Only class selectors allowed, with the following element selectors as
exceptions: aside, body, footer, header, main and nav.
*/
body {
margin: 0 auto;
max-width: 33em;
box-sizing: border-box;
min-height: 100vh;
padding: 0 0.5em;
}
/* === /Layout === */
/* === 4. Pages === */
/*
Styles that should only apply to certain pages.
*/
/* === /Pages === */
/* === 5. Themes === */
/*
Styles to create user-customized themes.
This section adapts the design to the following user preferences:
Color theme, reduced motion
*/
@media (prefers-color-scheme: light) {
:root {
--color: #000;
--bgcolor: #fff;
--a-color: blue;
}
}
@media (prefers-reduced-motion) {
:root {
--animation-duration: 0;
}
}
@media (min-width: 800px) {
:root {
--map-ratio: 3 / 2;
}
}
@media (min-width: 1000px) {
:root {
--page-title-font-size: 4em;
}
}
/* === /Themes === */
/* === 6. Vendors === */
/*
Styles belonging to third-party components.
*/
.footnotes-list {
color: var(--aside-color);
font-size: 80%;
}
/* === /Vendors === */
/* === 7. Shame === */
/*
Styles necessary for specifity issues and for cutting corners
(breaking the rules in short terms in waiting for an opportunity
to rewrite or fix a problem for good).
madr.se has no reason to feel ashamed. Yet.
*/
/* === /Shame === */

View file

@ -42,31 +42,6 @@ defmodule Mse25.Directus do
get("/articles?" <> params)
end
def get_note(slug) do
get_item(:notes, slug)
end
def get_notes!(options \\ []) do
params =
[
"fields=" <>
Enum.join(
[
"id",
"contents",
"images",
"date_created",
"location"
],
","
)
]
|> annual?(:notes, options)
|> query_params_string(options, :notes)
get("/notes?" <> params)
end
def get_album(externalId) do
case get_item(
:albums,
@ -223,13 +198,6 @@ defmodule Mse25.Directus do
end
end
defp get_item(:notes, externalId, fields) do
case get("/notes?fields=" <> fields <> "&filter[id][_eq]=" <> externalId) do
[] -> {:not_found, externalId}
[item | _] -> {:ok, item}
end
end
defp get_item(collection, slug, fields) do
case get(
"/" <> to_string(collection) <> "?fields=" <> fields <> "&filter[slug][_eq]=" <> slug

View file

@ -1,36 +1,26 @@
<a href="#content" class="skiplink">Hoppa till innehållet</a>
<nav>
<ul>
<li><a href="/webblogg">Webblogg</a></li>
<li><a href="/delningar">Länkar</a></li>
<li><a href="/anteckningar">Anteckningar</a></li>
<li><a href="/evenemang">Evenemang</a></li>
<li><a href="/om">Om</a></li>
</ul>
</nav>
<main id="content">
<div class="breadcrumbs">
<span class="sr-only">Du är här:</span>
<span class="trail" itemscope itemtype="https://schema.org/BreadcrumbList">
<span itemprop="itemListElement" itemscope itemtype="https://schema.org/ListItem">
<a href="/" rel="home">
<span itemprop="name">madr.se</span>
</a>
<meta itemprop="position" content="1" />
</span>
<%= for {index, {parent_slug, parent_name}} <- breadcrumbs(@breadcrumbs) do %>
<span class="sr-only">&gt;</span>
<span itemprop="itemListElement" itemscope itemtype="https://schema.org/ListItem">
<a href={parent_slug}>
<span itemprop="name"><%= parent_name %></span>
</a>
<meta itemprop="position" content={index} />
</span>
<% end %>
<span class="sr-only">Du är här:</span>
<span class="breadcrumbs" itemscope itemtype="https://schema.org/BreadcrumbList">
<span itemprop="itemListElement" itemscope itemtype="https://schema.org/ListItem">
<a href="/" rel="home">
<span itemprop="name">madr.se</span>
</a>
<meta itemprop="position" content="1" />
</span>
</div>
<%= for {index, {parent_slug, parent_name}} <- breadcrumbs(@breadcrumbs) do %>
<span class="sr-only">&gt;</span>
<span itemprop="itemListElement" itemscope itemtype="https://schema.org/ListItem">
<a href={parent_slug}>
<span itemprop="name"><%= parent_name %></span>
</a>
<meta itemprop="position" content={index} />
</span>
<% end %>
</span>
</nav>
<main id="content">
<%= @inner_content %>
</main>
<%= if show_footer?(assigns) do %>

View file

@ -41,13 +41,6 @@ defmodule Mse25Web.ItemController do
end
end
defp fetch([_year, album_id], :note) do
case Directus.get_note(album_id) do
{:ok, response} -> {:ok, :note, response}
not_found -> not_found
end
end
defp fetch([year, "brutal-legend-" <> external_id]) do
fetch([year, external_id], :album)
end
@ -181,21 +174,6 @@ defmodule Mse25Web.ItemController do
]
end
defp assigns(:note, %{
"contents" => text,
"images" => images,
"date_created" => published_at
}) do
year = String.slice(published_at, 0..3)
[
text: Earmark.as_html!(text),
breadcrumbs: [{"anteckningar", "Anteckningar"}, {year, year, ""}],
date_created: String.slice(published_at, 0..9),
images: images
]
end
defp assigns(:album, %{
"year" => year,
"album" => album,

View file

@ -1,4 +1,4 @@
<article class="collapsed article card" vocab="https://schema.org/" typeof="Article">
<article class="article" vocab="https://schema.org/" typeof="Article">
<h1 property="name"><%= @heading %></h1>
<div property="articleBody">

View file

@ -1,10 +0,0 @@
<article class="collapsed article card" vocab="https://schema.org/" typeof="Article">
<h1 property="name articleBody"><%= raw(@contents) %></h1>
<footer>
<p>
Publicerad <time property="datePublished"><%= @published_at %></time>
av <span property="publisher">Anders Englöf Ytterström</span>
</p>
</footer>
</article>

View file

@ -1,4 +1,4 @@
<article class="card article" vocab="https://schema.org/" typeof="Article">
<article class="article" vocab="https://schema.org/" typeof="Article">
<h1 property="name"><%= @heading %></h1>
<div property="articleBody">

View file

@ -68,26 +68,6 @@ defmodule Mse25Web.PageController do
)
end
def notes(conn, params) do
{notes, page_title} =
case params do
%{"q" => query_string} ->
{Directus.get_notes!(limit: @almost_infinity, query: query_string),
"Anteckningar: \"#{query_string}\""}
_ ->
{Directus.get_notes!(limit: @almost_infinity), "Anteckningar"}
end
render(conn, :notes,
page_title: page_title,
breadcrumbs: [],
notes: group_by_creation_date(notes),
q: params["q"],
nosearch?: params["q"] == nil or params["q"] == ""
)
end
def events(conn, params) do
{_, %{"title" => title, "contents" => contents}} = Directus.get_page("evenemang")
@ -113,7 +93,7 @@ defmodule Mse25Web.PageController do
end
def links(conn, _params) do
links = Directus.get_links!(limit: @almost_infinity) |> group_by_pub_date
links = Directus.get_links!(limit: @almost_infinity) |> group_by_date
render(conn, :links,
page_title: "Delningar",
@ -129,17 +109,10 @@ defmodule Mse25Web.PageController do
|> Enum.sort(fn {a, _a}, {b, _b} -> b < a end)
end
defp group_by_pub_date(items) do
defp group_by_date(items) do
items
|> Enum.group_by(fn %{"pubDate" => pub_date} -> pub_date end)
|> Map.to_list()
|> Enum.sort(fn {a, _a}, {b, _b} -> b < a end)
end
defp group_by_creation_date(items) do
items
|> Enum.group_by(fn %{"date_created" => pub_date} -> String.slice(pub_date, 0..9) end)
|> Map.to_list()
|> Enum.sort(fn {a, _a}, {b, _b} -> b < a end)
end
end

View file

@ -3,8 +3,4 @@ defmodule Mse25Web.PageHTML do
import Mse25.EventHelpers
embed_templates "page_html/*"
defp fancy_timestamp(datestr) do
datestr |> IO.inspect()
end
end

View file

@ -1,45 +1,41 @@
<div class="head">
<h1>
<%= @page_title %>
</h1>
<p>
Inlägg skrivna sedan 2006.
<%= if @nosearch? do %>
Gå direkt till:
<% end %>
</p>
<ul class="months">
<%= for {year, articles} <- @articles do %>
<li>
<a href={"#y" <> year}><%= year %></a> (<%= Enum.count(articles) %>)
</li>
<% end %>
</ul>
<form method="get" action="/webblogg">
<p>
<%= if @nosearch? do %>
Eller
<% end %>
<label for="q">sök innehåll</label>:
<input type="search" value={@q} name="q" id="q" size="7" />
<button>Sök</button>
</p>
</form>
</div>
<div class="cards">
<%= for {year, articles} <- @articles do %>
<section class="card" id={"y" <> year}>
<h2 class="sticky"><%= year %></h2>
<div class="articles">
<%= for article <- articles do %>
<article class="article" vocab="https://schema.org/" typeof="Article">
<h2 property="name">
<a href={"/" <> article["slug"]}><%= article["title"] %></a>
</h2>
<time><%= article["pubDate"] %></time>
</article>
<% end %>
</div>
</section>
<h1>
<%= @page_title %>
</h1>
<p>
Inlägg skrivna sedan 2006.
<%= if @nosearch? do %>
Gå direkt till:
<% end %>
</div>
</p>
<ul class="months">
<%= for {year, articles} <- @articles do %>
<li>
<a href={"#y" <> year}><%= year %></a> (<%= Enum.count(articles) %>)
</li>
<% end %>
</ul>
<form method="get" action="/webblogg">
<p>
<%= if @nosearch? do %>
Eller
<% end %>
<label for="q">sök innehåll</label>:
<input type="search" value={@q} name="q" id="q" size="7" />
<button>Sök</button>
</p>
</form>
<%= for {year, articles} <- @articles do %>
<section id={"y" <> year}>
<h2 class="sticky"><%= year %></h2>
<div class="articles">
<%= for article <- articles do %>
<article class="article" vocab="https://schema.org/" typeof="Article">
<h2 property="name">
<a href={"/" <> article["slug"]}><%= article["title"] %></a>
</h2>
<time><%= article["pubDate"] %></time>
</article>
<% end %>
</div>
</section>
<% end %>

View file

@ -1,74 +0,0 @@
<div class="head">
<h1>Anteckningar</h1>
<p>Blandade tankar, oftast på engelska. Replikerat på Mastodon.</p>
</div>
<div class="cards">
<%= for {date, links} <- @notes do %>
<section class="card" id={"d" <> date}>
<h2>
<%= date
|> Date.from_iso8601!()
|> Calendar.strftime(
"%A, %d %B %Y",
month_names: fn m ->
Enum.at(
[
"januari",
"februari",
"mars",
"april",
"maj",
"juni",
"juli",
"augusti",
"september",
"oktober",
"november",
"december"
],
m - 1
)
end,
day_of_week_names: fn d ->
Enum.at(
[
"måndag",
"tisdag",
"onsdag",
"torsdag",
"fredag",
"lördag",
"söndag"
],
d - 1
)
end
)
|> String.replace(~r/ 0/, " ") %>
</h2>
<div class="bookmarks">
<%= for link <- links do %>
<article vocab="https://schema.org/" typeof="WebContent Review" class="bookmark">
<h3>
<div property="reviewBody">
<%= link["contents"] |> Earmark.as_html!() |> raw %>
</div>
</h3>
<footer>
<p>
Posted on
<a class="permalink" href={"/notes/" <> to_string(link["id"])} title="Permalänk">
<%= fancy_timestamp(link["date_created"]) %>
</a>
at
<a href="https://www.openstreetmap.org/#map=15/50.82806/-0.12861">
pos
</a>
</p>
</footer>
</article>
<% end %>
</div>
</section>
<% end %>
</div>

View file

@ -31,7 +31,6 @@ defmodule Mse25Web.Router do
get "/evenemang", PageController, :events
get "/webblogg", PageController, :articles
get "/delningar", PageController, :links
get "/anteckningar", PageController, :notes
get "/sok", PageController, :search
get "/prenumerera.xml", FeedController, :feed