Compare commits

...

2 commits

Author SHA1 Message Date
7265184fc5 WIP 2025-02-11 13:15:58 +01:00
2386a56036 Add dev secret template to README 2025-02-11 13:15:43 +01:00
13 changed files with 421 additions and 584 deletions

View file

@ -16,3 +16,14 @@ 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,580 +1,222 @@
/*
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;
> 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);
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;
}
h1 {
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;
font-size: 1.75em;
letter-spacing: -0.066em;
}
h2 {
font-size: var(--section-heading-lv2-font-size);
border-bottom: 3px solid var(--panel-bg-color);
}
h3 {
font-size: var(--section-heading-lv3-font-size);
}
h4 {
font-size: var(--section-heading-lv4-font-size);
}
pre {
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 {
font-size: 0.75em;
position: absolute;
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;
font-size: 1.33em;
}
p,
li {
color: var(--em-color);
margin: 0.25em 0;
line-height: 1.66em;
}
li:first-child {
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;
}
li:last-child {
:last-child {
margin-bottom: 0;
}
li::marker {
color: var(--link-color);
&.collapsed {
border-top-left-radius: 0;
border-top-right-radius: 0;
}
}
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;
pre {
padding: 0.5em;
background-color: #1e2025;
color: #96df71;
position: relative;
margin: 1em 2em;
overflow-y: auto;
button {
position: absolute;
top: 2px;
right: 2px;
}
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;
left: -999em;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
border: 0;
}
.flx {
display: flex;
justify-content: space-between;
align-items: center;
.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;
.sticky {
position: sticky;
top: 0;
}
.interactive-map {
aspect-ratio: var(--map-ratio);
}
.home-h1 {
font-size: 1.33em;
}
.list-link {
&::after {
content: " →";
a {
color: #fff;
}
}
.feed-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;
}
}
.skiplink {
position: absolute;
top: -5em;
transition: top var(--animation-duration) ease-out;
padding: 0.25em 0.5em;
top: -1.75em;
left: 1em;
background-color: #ff0;
color: #000;
text-decoration: none;
padding: 0.25em;
transition: top 0.25s ease-out;
&:focus {
top: 1em;
}
}
.tree {
nav {
display: flex;
margin: 2em 0 1em;
> ul {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
gap: 0.66em;
font-size: var(--tree-font-size);
> li {
text-align: center;
display: grid;
grid-template-columns: 50px 1fr auto;
align-items: center;
margin: 0;
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);
&:focus-within {
background-color: rgba(128, 128, 128, 0.25);
}
> small {
opacity: 0.66;
font-family: var(--monospace-fonts);
font-size: 0.66em;
}
}
> .article {
--tree-item-accent-color: rebeccapurple;
}
> .album {
--tree-item-accent-color: goldenrod;
}
> .link {
--tree-item-accent-color: honeydew;
}
> .events {
--tree-item-accent-color: firebrick;
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;
}
a {
color: var(--color);
text-decoration: none;
flex: 1;
&:hover,
&:focus {
text-decoration: underline;
}
}
}
.landing {
display: flex;
flex-direction: column;
align-items: center;
padding: 2em 0;
box-sizing: border-box;
gap: 1.66em;
}
.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: " /";
color: #fff;
line-height: 1;
}
}
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 {
.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;
}
}
.profiles {
img + img {
top: 12.5em;
left: 5em;
width: 125px;
}
img + img + img {
top: 18.5em;
left: 0.5em;
width: 90px;
}
}
.bookmarks {
display: flex;
gap: 1.66em;
list-style: none;
margin: 0;
> li {
margin: 0;
padding: 0;
}
flex-direction: column;
gap: 2em;
}
/* === /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;
@keyframes burst {
0% {
transform: scale(1);
}
/* === /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;
50% {
transform: scale(1.2);
}
100% {
transform: scale(0.8);
}
}
@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,6 +42,31 @@ 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,
@ -198,6 +223,13 @@ 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,8 +1,19 @@
<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="breadcrumbs" itemscope itemtype="https://schema.org/BreadcrumbList">
<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>
@ -19,8 +30,7 @@
</span>
<% end %>
</span>
</nav>
<main id="content">
</div>
<%= @inner_content %>
</main>
<%= if show_footer?(assigns) do %>

View file

@ -41,6 +41,13 @@ 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
@ -174,6 +181,21 @@ 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="article" vocab="https://schema.org/" typeof="Article">
<article class="collapsed article card" vocab="https://schema.org/" typeof="Article">
<h1 property="name"><%= @heading %></h1>
<div property="articleBody">

View file

@ -0,0 +1,10 @@
<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="article" vocab="https://schema.org/" typeof="Article">
<article class="card article" vocab="https://schema.org/" typeof="Article">
<h1 property="name"><%= @heading %></h1>
<div property="articleBody">

View file

@ -68,6 +68,26 @@ 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")
@ -93,7 +113,7 @@ defmodule Mse25Web.PageController do
end
def links(conn, _params) do
links = Directus.get_links!(limit: @almost_infinity) |> group_by_date
links = Directus.get_links!(limit: @almost_infinity) |> group_by_pub_date
render(conn, :links,
page_title: "Delningar",
@ -109,10 +129,17 @@ defmodule Mse25Web.PageController do
|> Enum.sort(fn {a, _a}, {b, _b} -> b < a end)
end
defp group_by_date(items) do
defp group_by_pub_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,4 +3,8 @@ defmodule Mse25Web.PageHTML do
import Mse25.EventHelpers
embed_templates "page_html/*"
defp fancy_timestamp(datestr) do
datestr |> IO.inspect()
end
end

View file

@ -1,3 +1,4 @@
<div class="head">
<h1>
<%= @page_title %>
</h1>
@ -24,8 +25,10 @@
<button>Sök</button>
</p>
</form>
</div>
<div class="cards">
<%= for {year, articles} <- @articles do %>
<section id={"y" <> year}>
<section class="card" id={"y" <> year}>
<h2 class="sticky"><%= year %></h2>
<div class="articles">
<%= for article <- articles do %>
@ -39,3 +42,4 @@
</div>
</section>
<% end %>
</div>

View file

@ -0,0 +1,74 @@
<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,6 +31,7 @@ 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