Compare commits
10 commits
794752592e
...
82276d6a48
| Author | SHA1 | Date | |
|---|---|---|---|
| 82276d6a48 | |||
| 4540c93d6e | |||
| a14f206a2a | |||
| 27fb1b7a86 | |||
| 57e935ec00 | |||
| ef937ca0eb | |||
| 7e2010efb0 | |||
| 3725dc17d0 | |||
| 54bf81b54d | |||
| b6164295f3 |
519
assets/app.css
|
|
@ -1,55 +1,155 @@
|
||||||
|
/*
|
||||||
|
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 {
|
:root {
|
||||||
--color: #fff;
|
/* colors, dark mode default */
|
||||||
--bgcolor: #201;
|
--color: hsl(0 0 90%);
|
||||||
--box-padding: 12px;
|
--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 {
|
html {
|
||||||
color: var(--color);
|
color: var(--color);
|
||||||
background-color: var(--bgcolor);
|
background-color: var(--bgcolor);
|
||||||
background-image: linear-gradient(#000, #201 333px);
|
font: normal var(--base-font-size) / 1.5 var(--system-sansserif-fonts);
|
||||||
font:
|
|
||||||
normal small/1.5 apple-system,
|
|
||||||
system-ui,
|
|
||||||
BlinkMacSystemFont,
|
|
||||||
Segoe UI,
|
|
||||||
Roboto,
|
|
||||||
Helvetica Neue,
|
|
||||||
Arial,
|
|
||||||
sans-serif;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
min-height: 100vh;
|
|
||||||
|
> 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,
|
h1,
|
||||||
h2,
|
h2,
|
||||||
h3 {
|
h3 {
|
||||||
line-height: 1.1;
|
font-family: var(--system-serif-fonts);
|
||||||
font-family: serif;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
main {
|
h1 {
|
||||||
background-color: #000;
|
margin: 0.5em 0;
|
||||||
margin: 0 auto;
|
line-height: 0.95;
|
||||||
max-width: 40em;
|
font-size: var(--page-title-font-size);
|
||||||
box-sizing: border-box;
|
|
||||||
padding: 3em;
|
@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: 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 {
|
pre {
|
||||||
margin: 2em 0;
|
margin: 2em 0;
|
||||||
background-color: #022;
|
background-color: #022;
|
||||||
|
color: var(--monospace-color);
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
padding: 0.33em;
|
padding: 0.66em;
|
||||||
box-shadow: 4px 4px 0 #333;
|
box-shadow: 4px 4px 0 var(--panel-bg-color);
|
||||||
position: relative;
|
position: relative;
|
||||||
line-height: 1.2;
|
line-height: 1.2;
|
||||||
font-size: 0.8em;
|
font-size: 0.8em;
|
||||||
|
|
||||||
> button {
|
> button {
|
||||||
|
font-size: 0.75em;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0.25em;
|
top: 0.25em;
|
||||||
right: 0.25em;
|
right: 0.25em;
|
||||||
|
|
@ -57,28 +157,125 @@ pre {
|
||||||
}
|
}
|
||||||
|
|
||||||
code {
|
code {
|
||||||
font-family: "JetBrains mono", monaco, menlo, meslo, "Courier New", Courier,
|
font-family: var(--monospace-fonts);
|
||||||
monospace;
|
|
||||||
|
&.inline {
|
||||||
|
color: var(--monospace-color-inline);
|
||||||
|
background: #f3f3f3;
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
section {
|
section {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
& > h2 {
|
& > h2 {
|
||||||
background: #000;
|
background: var(--bgcolor);
|
||||||
padding: 1em 0;
|
color: var(--color);
|
||||||
|
padding: 0.5em 0.25em;
|
||||||
|
border-bottom: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.skiplink {
|
img {
|
||||||
position: absolute;
|
max-width: 100%;
|
||||||
top: -5em;
|
display: block;
|
||||||
transition: top 0.4s ease-out;
|
height: auto;
|
||||||
padding: 0.25em 0.5em;
|
}
|
||||||
|
|
||||||
&:focus {
|
ul,
|
||||||
top: 1em;
|
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;
|
||||||
|
left: -999em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.flx {
|
.flx {
|
||||||
|
|
@ -87,32 +284,94 @@ section {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sticky {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.interactive-map {
|
||||||
|
aspect-ratio: var(--map-ratio);
|
||||||
|
}
|
||||||
|
|
||||||
|
.home-h1 {
|
||||||
|
font-size: 1.33em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-link {
|
||||||
|
&::after {
|
||||||
|
content: " →";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.feed-link {
|
||||||
|
&::after {
|
||||||
|
content: " ↗";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.skiplink {
|
||||||
|
position: absolute;
|
||||||
|
top: -5em;
|
||||||
|
transition: top var(--animation-duration) ease-out;
|
||||||
|
padding: 0.25em 0.5em;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
top: 1em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.tree {
|
.tree {
|
||||||
color: crimson;
|
list-style: none;
|
||||||
align-items: center;
|
margin: 0;
|
||||||
justify-content: center;
|
padding: 0;
|
||||||
margin: 1em auto;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 0.5em;
|
gap: 0.66em;
|
||||||
|
font-size: var(--tree-font-size);
|
||||||
|
|
||||||
> * {
|
> li {
|
||||||
padding: 0.5em 1em;
|
text-align: center;
|
||||||
border-radius: 5px;
|
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);
|
background-color: rgba(128, 128, 128, 0.1);
|
||||||
|
border: 1px solid rgba(192, 192, 192, 0.1);
|
||||||
|
|
||||||
&:focus-within {
|
&:focus-within {
|
||||||
background-color: rgba(128, 128, 128, 0.25);
|
background-color: rgba(128, 128, 128, 0.25);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
> small {
|
||||||
|
opacity: 0.66;
|
||||||
|
font-family: var(--monospace-fonts);
|
||||||
|
font-size: 0.66em;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
> :nth-child(even) {
|
> .article {
|
||||||
text-align: right;
|
--tree-item-accent-color: rebeccapurple;
|
||||||
|
}
|
||||||
|
|
||||||
|
> .album {
|
||||||
|
--tree-item-accent-color: goldenrod;
|
||||||
|
}
|
||||||
|
|
||||||
|
> .link {
|
||||||
|
--tree-item-accent-color: honeydew;
|
||||||
|
}
|
||||||
|
|
||||||
|
> .events {
|
||||||
|
--tree-item-accent-color: firebrick;
|
||||||
}
|
}
|
||||||
|
|
||||||
a {
|
a {
|
||||||
color: #fff;
|
color: var(--color);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
flex: 1;
|
||||||
|
|
||||||
&:hover,
|
&:hover,
|
||||||
&:focus {
|
&:focus {
|
||||||
|
|
@ -122,45 +381,35 @@ section {
|
||||||
}
|
}
|
||||||
|
|
||||||
.landing {
|
.landing {
|
||||||
min-height: 100vh;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 3em;
|
padding: 2em 0;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
gap: 3em;
|
gap: 1.66em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.breadcrumbs {
|
.breadcrumbs {
|
||||||
list-style: none;
|
display: block;
|
||||||
padding-left: 0;
|
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;
|
||||||
|
|
||||||
> li {
|
> span {
|
||||||
display: inline;
|
display: inline;
|
||||||
|
|
||||||
&:after {
|
&:after {
|
||||||
content: " /";
|
content: " /";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
.article {
|
a {
|
||||||
font-size: large;
|
color: var(--color);
|
||||||
box-sizing: border-box;
|
|
||||||
|
|
||||||
> footer {
|
|
||||||
font-style: italic;
|
|
||||||
text-align: right;
|
|
||||||
font-size: 0.8em;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.sticky {
|
|
||||||
position: sticky;
|
|
||||||
top: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.months {
|
.months {
|
||||||
grid-auto-flow: rows;
|
grid-auto-flow: rows;
|
||||||
display: grid;
|
display: grid;
|
||||||
|
|
@ -168,7 +417,36 @@ section {
|
||||||
grid-template-columns: repeat(3, 1fr);
|
grid-template-columns: repeat(3, 1fr);
|
||||||
gap: 0.5em;
|
gap: 0.5em;
|
||||||
padding-left: 0;
|
padding-left: 0;
|
||||||
margin: 3em 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 {
|
.articles {
|
||||||
|
|
@ -188,7 +466,7 @@ section {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
date {
|
time {
|
||||||
border-top: 1px solid crimson;
|
border-top: 1px solid crimson;
|
||||||
padding: 0.25em 0.5em;
|
padding: 0.25em 0.5em;
|
||||||
}
|
}
|
||||||
|
|
@ -199,3 +477,104 @@ section {
|
||||||
margin-top: 3em;
|
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 === */
|
||||||
|
|
|
||||||
BIN
assets/favicon-16x16.png
Normal file
|
After Width: | Height: | Size: 915 B |
BIN
assets/favicon-32x32.png
Normal file
|
After Width: | Height: | Size: 2.5 KiB |
|
|
@ -26,7 +26,7 @@ config :esbuild,
|
||||||
version: "0.17.11",
|
version: "0.17.11",
|
||||||
mse25: [
|
mse25: [
|
||||||
args:
|
args:
|
||||||
~w(js/app.js --bundle --target=es2017 --outdir=../priv/static/assets --external:/fonts/* --external:/images/* --external:/css/*),
|
~w(js/app.js --bundle --target=es2017 --outdir=../priv/static/assets --external:/bl/* --external:/images/* --external:/*.{vcf,png,ico,pdf}),
|
||||||
cd: Path.expand("../assets", __DIR__),
|
cd: Path.expand("../assets", __DIR__),
|
||||||
env: %{"NODE_PATH" => Path.expand("../deps", __DIR__)}
|
env: %{"NODE_PATH" => Path.expand("../deps", __DIR__)}
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,19 @@
|
||||||
defmodule Mse25.Directus do
|
defmodule Mse25.Directus do
|
||||||
|
@moduledoc """
|
||||||
|
Simple Directus client, utilizing Req to do CRUD
|
||||||
|
operations.
|
||||||
|
|
||||||
|
Currently, this client only read data, and supports
|
||||||
|
various ways of filtering data.
|
||||||
|
|
||||||
|
It is by no means generic, since fieldsets are not
|
||||||
|
agnostic. It may however be used as a base to create
|
||||||
|
a more generic client implementation in Elixir.
|
||||||
|
|
||||||
|
Directus documentation:
|
||||||
|
https://docs.directus.io/
|
||||||
|
"""
|
||||||
|
|
||||||
@draft_filter "filter[status][_eq]=published"
|
@draft_filter "filter[status][_eq]=published"
|
||||||
|
|
||||||
def get_article(slug) do
|
def get_article(slug) do
|
||||||
|
|
@ -15,7 +30,8 @@ defmodule Mse25.Directus do
|
||||||
"slug",
|
"slug",
|
||||||
"title",
|
"title",
|
||||||
"date_updated",
|
"date_updated",
|
||||||
"pubDate"
|
"pubDate",
|
||||||
|
"contents"
|
||||||
],
|
],
|
||||||
","
|
","
|
||||||
)
|
)
|
||||||
|
|
@ -27,21 +43,30 @@ defmodule Mse25.Directus do
|
||||||
end
|
end
|
||||||
|
|
||||||
def get_album(externalId) do
|
def get_album(externalId) do
|
||||||
get_item(
|
case get_item(
|
||||||
:albums,
|
:albums,
|
||||||
externalId,
|
externalId,
|
||||||
[
|
[
|
||||||
"purchased_at",
|
"*",
|
||||||
"album",
|
"songs.title",
|
||||||
"year",
|
"songs.artist.name"
|
||||||
"youtubeId",
|
]
|
||||||
"externalId",
|
|> Enum.join(",")
|
||||||
"cover",
|
) do
|
||||||
"songs.title",
|
{:ok,
|
||||||
"songs.artist.name"
|
data = %{
|
||||||
]
|
"album" => album,
|
||||||
|> Enum.join(",")
|
"year" => year,
|
||||||
)
|
"songs" => [%{"artist" => %{"name" => artist}} | _]
|
||||||
|
}} ->
|
||||||
|
{:ok,
|
||||||
|
data
|
||||||
|
|> Map.put("artist", artist)
|
||||||
|
|> Map.put("summary", "#{artist} - #{album} (#{to_string(year)})")}
|
||||||
|
|
||||||
|
not_found ->
|
||||||
|
not_found
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def get_albums!(options \\ []) do
|
def get_albums!(options \\ []) do
|
||||||
|
|
@ -51,25 +76,33 @@ defmodule Mse25.Directus do
|
||||||
"fields=" <>
|
"fields=" <>
|
||||||
Enum.join(
|
Enum.join(
|
||||||
[
|
[
|
||||||
"purchased_at",
|
"*",
|
||||||
"album",
|
|
||||||
"year",
|
|
||||||
"externalId",
|
|
||||||
"cover.filename_download",
|
|
||||||
"cover.width",
|
|
||||||
"cover.height",
|
|
||||||
"songs.title",
|
"songs.title",
|
||||||
"songs.artist.name"
|
"songs.artist.name"
|
||||||
],
|
],
|
||||||
","
|
","
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
|
|> annual?(:albums, options)
|
||||||
|> query_params_string(options, :brutal_legends)
|
|> query_params_string(options, :brutal_legends)
|
||||||
|
|
||||||
get("/albums?" <> params)
|
get("/albums?" <> params)
|
||||||
|> Enum.map(fn m = %{"songs" => [%{"artist" => %{"name" => a}} | _]} ->
|
|> Enum.map(
|
||||||
Map.put(m, "artist", a)
|
fn m = %{
|
||||||
end)
|
"album" => album,
|
||||||
|
"year" => year,
|
||||||
|
"songs" => [%{"artist" => %{"name" => artist}} | _],
|
||||||
|
"purchased_at" => purchased_at
|
||||||
|
} ->
|
||||||
|
m
|
||||||
|
|> Map.put("artist", artist)
|
||||||
|
|> Map.put(
|
||||||
|
"purchase_year",
|
||||||
|
String.slice(purchased_at, 0..3)
|
||||||
|
)
|
||||||
|
|> Map.put("summary", "#{artist} - #{album} (#{to_string(year)})")
|
||||||
|
end
|
||||||
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
def get_event(slug) do
|
def get_event(slug) do
|
||||||
|
|
@ -101,6 +134,7 @@ defmodule Mse25.Directus do
|
||||||
"fields=" <>
|
"fields=" <>
|
||||||
Enum.join(
|
Enum.join(
|
||||||
[
|
[
|
||||||
|
"id",
|
||||||
"title",
|
"title",
|
||||||
"lead",
|
"lead",
|
||||||
"slug",
|
"slug",
|
||||||
|
|
@ -108,8 +142,11 @@ defmodule Mse25.Directus do
|
||||||
"category",
|
"category",
|
||||||
"started_at",
|
"started_at",
|
||||||
"ended_at",
|
"ended_at",
|
||||||
|
"contents",
|
||||||
|
"date_created",
|
||||||
"bands.artists_id.name",
|
"bands.artists_id.name",
|
||||||
"mia.artists_id.name"
|
"mia.artists_id.name",
|
||||||
|
"location.*"
|
||||||
],
|
],
|
||||||
","
|
","
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,4 @@
|
||||||
defmodule Mse25.EventHelpers do
|
defmodule Mse25.EventHelpers do
|
||||||
def bandlist(bands) do
|
|
||||||
bands
|
|
||||||
|> Enum.map(fn b -> b["artists_id"]["name"] end)
|
|
||||||
|> Enum.join(", ")
|
|
||||||
|> String.replace(~r/, ([^,]+?)$/, " och \\1")
|
|
||||||
end
|
|
||||||
|
|
||||||
def hilights?(%{"bands" => bands, "category" => category}) do
|
def hilights?(%{"bands" => bands, "category" => category}) do
|
||||||
_festival_band?(bands, category)
|
_festival_band?(bands, category)
|
||||||
end
|
end
|
||||||
|
|
@ -30,4 +23,18 @@ defmodule Mse25.EventHelpers do
|
||||||
def _festival_band?(_b, _c) do
|
def _festival_band?(_b, _c) do
|
||||||
false
|
false
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def bandlist(bands) do
|
||||||
|
bands
|
||||||
|
|> Enum.map(fn b -> b["artists_id"]["name"] end)
|
||||||
|
|> Enum.join(", ")
|
||||||
|
|> String.replace(~r/, ([^,]+?)$/, " och \\1")
|
||||||
|
end
|
||||||
|
|
||||||
|
def rdfa_bandlist(bands) do
|
||||||
|
bands
|
||||||
|
|> Enum.map(fn b -> "<span property=\"performer\">#{b["artists_id"]["name"]}</span>" end)
|
||||||
|
|> Enum.join(", ")
|
||||||
|
|> String.replace(~r/, ([^,]+?)$/, " och \\1")
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -1,23 +1,35 @@
|
||||||
defmodule Mse25.Timeline do
|
defmodule Mse25.Timeline do
|
||||||
alias Mse25.Directus
|
alias Mse25.Directus
|
||||||
|
|
||||||
def archive() do
|
@almost_infinity 9999
|
||||||
|
|
||||||
|
def archive(limit \\ @almost_infinity) do
|
||||||
items =
|
items =
|
||||||
Task.await_many([
|
Task.await_many([
|
||||||
Task.async(fn -> Directus.get_albums!() end),
|
Task.async(fn -> Directus.get_albums!() end),
|
||||||
Task.async(fn -> Directus.get_articles!(limit: 9999) end),
|
Task.async(fn -> Directus.get_articles!(limit: limit) end),
|
||||||
Task.async(fn -> Directus.get_links!(limit: 9999) end),
|
Task.async(fn -> Directus.get_links!(limit: limit) end),
|
||||||
Task.async(fn -> Directus.get_events!(limit: 9999) end)
|
Task.async(fn -> Directus.get_events!(limit: limit) end)
|
||||||
])
|
])
|
||||||
|
|
||||||
|
archive =
|
||||||
|
items
|
||||||
|
|> List.flatten()
|
||||||
|
|> Enum.sort_by(&sort_key/1)
|
||||||
|
|> Enum.reverse()
|
||||||
|
|> Enum.take(limit)
|
||||||
|
|> Enum.map(&categorize/1)
|
||||||
|
|
||||||
|
{:ok, %{archive: archive}}
|
||||||
end
|
end
|
||||||
|
|
||||||
def annual(year) do
|
def annual(year) do
|
||||||
items =
|
items =
|
||||||
Task.await_many([
|
Task.await_many([
|
||||||
Task.async(fn -> Directus.get_albums!(limit: 9999, year: year) end),
|
Task.async(fn -> Directus.get_albums!(limit: @almost_infinity, year: year) end),
|
||||||
Task.async(fn -> Directus.get_articles!(limit: 9999, year: year) end),
|
Task.async(fn -> Directus.get_articles!(limit: @almost_infinity, year: year) end),
|
||||||
Task.async(fn -> Directus.get_links!(limit: 9999, year: year) end),
|
Task.async(fn -> Directus.get_links!(limit: @almost_infinity, year: year) end),
|
||||||
Task.async(fn -> Directus.get_events!(limit: 9999, year: year) end)
|
Task.async(fn -> Directus.get_events!(limit: @almost_infinity, year: year) end)
|
||||||
])
|
])
|
||||||
|
|
||||||
counts =
|
counts =
|
||||||
|
|
@ -40,10 +52,20 @@ defmodule Mse25.Timeline do
|
||||||
def search(query) do
|
def search(query) do
|
||||||
items =
|
items =
|
||||||
Task.await_many([
|
Task.await_many([
|
||||||
Task.async(fn -> Directus.get_articles!(limit: 9999, query: query) end),
|
Task.async(fn -> Directus.get_articles!(limit: @almost_infinity, query: query) end),
|
||||||
Task.async(fn -> Directus.get_links!(limit: 9999, query: query) end),
|
Task.async(fn -> Directus.get_links!(limit: @almost_infinity, query: query) end),
|
||||||
Task.async(fn -> Directus.get_events!(limit: 9999, query: query) end)
|
Task.async(fn -> Directus.get_events!(limit: @almost_infinity, query: query) end)
|
||||||
])
|
])
|
||||||
|
|
||||||
|
results =
|
||||||
|
items
|
||||||
|
|> List.flatten()
|
||||||
|
|> Enum.sort_by(&sort_key/1)
|
||||||
|
|> Enum.map(&categorize/1)
|
||||||
|
# |> Enum.group_by(fn item -> sort_key(item) |> String.slice(5..6) end)
|
||||||
|
|> Enum.reverse()
|
||||||
|
|
||||||
|
{:ok, %{query: query, results: results, count: length(results)}}
|
||||||
end
|
end
|
||||||
|
|
||||||
defp sort_key(%{"pubDate" => date}), do: date
|
defp sort_key(%{"pubDate" => date}), do: date
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,8 @@ defmodule Mse25Web do
|
||||||
those modules here.
|
those modules here.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def static_paths, do: ~w(assets fonts images favicon.ico robots.txt)
|
def static_paths,
|
||||||
|
do: ~w(assets fonts images bl favicon.ico robots.txt aey.vcf app.css cv)
|
||||||
|
|
||||||
def router do
|
def router do
|
||||||
quote do
|
quote do
|
||||||
|
|
@ -26,13 +27,6 @@ defmodule Mse25Web do
|
||||||
# Import common connection and controller functions to use in pipelines
|
# Import common connection and controller functions to use in pipelines
|
||||||
import Plug.Conn
|
import Plug.Conn
|
||||||
import Phoenix.Controller
|
import Phoenix.Controller
|
||||||
import Phoenix.LiveView.Router
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def channel do
|
|
||||||
quote do
|
|
||||||
use Phoenix.Channel
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -49,23 +43,6 @@ defmodule Mse25Web do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def live_view do
|
|
||||||
quote do
|
|
||||||
use Phoenix.LiveView,
|
|
||||||
layout: {Mse25Web.Layouts, :app}
|
|
||||||
|
|
||||||
unquote(html_helpers())
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def live_component do
|
|
||||||
quote do
|
|
||||||
use Phoenix.LiveComponent
|
|
||||||
|
|
||||||
unquote(html_helpers())
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def html do
|
def html do
|
||||||
quote do
|
quote do
|
||||||
use Phoenix.Component
|
use Phoenix.Component
|
||||||
|
|
@ -81,16 +58,10 @@ defmodule Mse25Web do
|
||||||
|
|
||||||
defp html_helpers do
|
defp html_helpers do
|
||||||
quote do
|
quote do
|
||||||
# HTML escaping functionality
|
|
||||||
import Phoenix.HTML
|
import Phoenix.HTML
|
||||||
# Core UI components and translation
|
|
||||||
import Mse25Web.CoreComponents
|
import Mse25Web.CoreComponents
|
||||||
import Mse25Web.Gettext
|
import Mse25Web.Gettext
|
||||||
|
|
||||||
# Shortcut for generating JS commands
|
|
||||||
alias Phoenix.LiveView.JS
|
|
||||||
|
|
||||||
# Routes generation with the ~p sigil
|
|
||||||
unquote(verified_routes())
|
unquote(verified_routes())
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -1,676 +1,2 @@
|
||||||
defmodule Mse25Web.CoreComponents do
|
defmodule Mse25Web.CoreComponents do
|
||||||
@moduledoc """
|
|
||||||
Provides core UI components.
|
|
||||||
|
|
||||||
At first glance, this module may seem daunting, but its goal is to provide
|
|
||||||
core building blocks for your application, such as modals, tables, and
|
|
||||||
forms. The components consist mostly of markup and are well-documented
|
|
||||||
with doc strings and declarative assigns. You may customize and style
|
|
||||||
them in any way you want, based on your application growth and needs.
|
|
||||||
|
|
||||||
The default components use Tailwind CSS, a utility-first CSS framework.
|
|
||||||
See the [Tailwind CSS documentation](https://tailwindcss.com) to learn
|
|
||||||
how to customize them or feel free to swap in another framework altogether.
|
|
||||||
|
|
||||||
Icons are provided by [heroicons](https://heroicons.com). See `icon/1` for usage.
|
|
||||||
"""
|
|
||||||
use Phoenix.Component
|
|
||||||
|
|
||||||
alias Phoenix.LiveView.JS
|
|
||||||
import Mse25Web.Gettext
|
|
||||||
|
|
||||||
@doc """
|
|
||||||
Renders a modal.
|
|
||||||
|
|
||||||
## Examples
|
|
||||||
|
|
||||||
<.modal id="confirm-modal">
|
|
||||||
This is a modal.
|
|
||||||
</.modal>
|
|
||||||
|
|
||||||
JS commands may be passed to the `:on_cancel` to configure
|
|
||||||
the closing/cancel event, for example:
|
|
||||||
|
|
||||||
<.modal id="confirm" on_cancel={JS.navigate(~p"/posts")}>
|
|
||||||
This is another modal.
|
|
||||||
</.modal>
|
|
||||||
|
|
||||||
"""
|
|
||||||
attr :id, :string, required: true
|
|
||||||
attr :show, :boolean, default: false
|
|
||||||
attr :on_cancel, JS, default: %JS{}
|
|
||||||
slot :inner_block, required: true
|
|
||||||
|
|
||||||
def modal(assigns) do
|
|
||||||
~H"""
|
|
||||||
<div
|
|
||||||
id={@id}
|
|
||||||
phx-mounted={@show && show_modal(@id)}
|
|
||||||
phx-remove={hide_modal(@id)}
|
|
||||||
data-cancel={JS.exec(@on_cancel, "phx-remove")}
|
|
||||||
class="relative z-50 hidden"
|
|
||||||
>
|
|
||||||
<div id={"#{@id}-bg"} class="bg-zinc-50/90 fixed inset-0 transition-opacity" aria-hidden="true" />
|
|
||||||
<div
|
|
||||||
class="fixed inset-0 overflow-y-auto"
|
|
||||||
aria-labelledby={"#{@id}-title"}
|
|
||||||
aria-describedby={"#{@id}-description"}
|
|
||||||
role="dialog"
|
|
||||||
aria-modal="true"
|
|
||||||
tabindex="0"
|
|
||||||
>
|
|
||||||
<div class="flex min-h-full items-center justify-center">
|
|
||||||
<div class="w-full max-w-3xl p-4 sm:p-6 lg:py-8">
|
|
||||||
<.focus_wrap
|
|
||||||
id={"#{@id}-container"}
|
|
||||||
phx-window-keydown={JS.exec("data-cancel", to: "##{@id}")}
|
|
||||||
phx-key="escape"
|
|
||||||
phx-click-away={JS.exec("data-cancel", to: "##{@id}")}
|
|
||||||
class="shadow-zinc-700/10 ring-zinc-700/10 relative hidden rounded-2xl bg-white p-14 shadow-lg ring-1 transition"
|
|
||||||
>
|
|
||||||
<div class="absolute top-6 right-5">
|
|
||||||
<button
|
|
||||||
phx-click={JS.exec("data-cancel", to: "##{@id}")}
|
|
||||||
type="button"
|
|
||||||
class="-m-3 flex-none p-3 opacity-20 hover:opacity-40"
|
|
||||||
aria-label={gettext("close")}
|
|
||||||
>
|
|
||||||
<.icon name="hero-x-mark-solid" class="h-5 w-5" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div id={"#{@id}-content"}>
|
|
||||||
<%= render_slot(@inner_block) %>
|
|
||||||
</div>
|
|
||||||
</.focus_wrap>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
"""
|
|
||||||
end
|
|
||||||
|
|
||||||
@doc """
|
|
||||||
Renders flash notices.
|
|
||||||
|
|
||||||
## Examples
|
|
||||||
|
|
||||||
<.flash kind={:info} flash={@flash} />
|
|
||||||
<.flash kind={:info} phx-mounted={show("#flash")}>Welcome Back!</.flash>
|
|
||||||
"""
|
|
||||||
attr :id, :string, doc: "the optional id of flash container"
|
|
||||||
attr :flash, :map, default: %{}, doc: "the map of flash messages to display"
|
|
||||||
attr :title, :string, default: nil
|
|
||||||
attr :kind, :atom, values: [:info, :error], doc: "used for styling and flash lookup"
|
|
||||||
attr :rest, :global, doc: "the arbitrary HTML attributes to add to the flash container"
|
|
||||||
|
|
||||||
slot :inner_block, doc: "the optional inner block that renders the flash message"
|
|
||||||
|
|
||||||
def flash(assigns) do
|
|
||||||
assigns = assign_new(assigns, :id, fn -> "flash-#{assigns.kind}" end)
|
|
||||||
|
|
||||||
~H"""
|
|
||||||
<div
|
|
||||||
:if={msg = render_slot(@inner_block) || Phoenix.Flash.get(@flash, @kind)}
|
|
||||||
id={@id}
|
|
||||||
phx-click={JS.push("lv:clear-flash", value: %{key: @kind}) |> hide("##{@id}")}
|
|
||||||
role="alert"
|
|
||||||
class={[
|
|
||||||
"fixed top-2 right-2 mr-2 w-80 sm:w-96 z-50 rounded-lg p-3 ring-1",
|
|
||||||
@kind == :info && "bg-emerald-50 text-emerald-800 ring-emerald-500 fill-cyan-900",
|
|
||||||
@kind == :error && "bg-rose-50 text-rose-900 shadow-md ring-rose-500 fill-rose-900"
|
|
||||||
]}
|
|
||||||
{@rest}
|
|
||||||
>
|
|
||||||
<p :if={@title} class="flex items-center gap-1.5 text-sm font-semibold leading-6">
|
|
||||||
<.icon :if={@kind == :info} name="hero-information-circle-mini" class="h-4 w-4" />
|
|
||||||
<.icon :if={@kind == :error} name="hero-exclamation-circle-mini" class="h-4 w-4" />
|
|
||||||
<%= @title %>
|
|
||||||
</p>
|
|
||||||
<p class="mt-2 text-sm leading-5"><%= msg %></p>
|
|
||||||
<button type="button" class="group absolute top-1 right-1 p-2" aria-label={gettext("close")}>
|
|
||||||
<.icon name="hero-x-mark-solid" class="h-5 w-5 opacity-40 group-hover:opacity-70" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
"""
|
|
||||||
end
|
|
||||||
|
|
||||||
@doc """
|
|
||||||
Shows the flash group with standard titles and content.
|
|
||||||
|
|
||||||
## Examples
|
|
||||||
|
|
||||||
<.flash_group flash={@flash} />
|
|
||||||
"""
|
|
||||||
attr :flash, :map, required: true, doc: "the map of flash messages"
|
|
||||||
attr :id, :string, default: "flash-group", doc: "the optional id of flash container"
|
|
||||||
|
|
||||||
def flash_group(assigns) do
|
|
||||||
~H"""
|
|
||||||
<div id={@id}>
|
|
||||||
<.flash kind={:info} title={gettext("Success!")} flash={@flash} />
|
|
||||||
<.flash kind={:error} title={gettext("Error!")} flash={@flash} />
|
|
||||||
<.flash
|
|
||||||
id="client-error"
|
|
||||||
kind={:error}
|
|
||||||
title={gettext("We can't find the internet")}
|
|
||||||
phx-disconnected={show(".phx-client-error #client-error")}
|
|
||||||
phx-connected={hide("#client-error")}
|
|
||||||
hidden
|
|
||||||
>
|
|
||||||
<%= gettext("Attempting to reconnect") %>
|
|
||||||
<.icon name="hero-arrow-path" class="ml-1 h-3 w-3 animate-spin" />
|
|
||||||
</.flash>
|
|
||||||
|
|
||||||
<.flash
|
|
||||||
id="server-error"
|
|
||||||
kind={:error}
|
|
||||||
title={gettext("Something went wrong!")}
|
|
||||||
phx-disconnected={show(".phx-server-error #server-error")}
|
|
||||||
phx-connected={hide("#server-error")}
|
|
||||||
hidden
|
|
||||||
>
|
|
||||||
<%= gettext("Hang in there while we get back on track") %>
|
|
||||||
<.icon name="hero-arrow-path" class="ml-1 h-3 w-3 animate-spin" />
|
|
||||||
</.flash>
|
|
||||||
</div>
|
|
||||||
"""
|
|
||||||
end
|
|
||||||
|
|
||||||
@doc """
|
|
||||||
Renders a simple form.
|
|
||||||
|
|
||||||
## Examples
|
|
||||||
|
|
||||||
<.simple_form for={@form} phx-change="validate" phx-submit="save">
|
|
||||||
<.input field={@form[:email]} label="Email"/>
|
|
||||||
<.input field={@form[:username]} label="Username" />
|
|
||||||
<:actions>
|
|
||||||
<.button>Save</.button>
|
|
||||||
</:actions>
|
|
||||||
</.simple_form>
|
|
||||||
"""
|
|
||||||
attr :for, :any, required: true, doc: "the data structure for the form"
|
|
||||||
attr :as, :any, default: nil, doc: "the server side parameter to collect all input under"
|
|
||||||
|
|
||||||
attr :rest, :global,
|
|
||||||
include: ~w(autocomplete name rel action enctype method novalidate target multipart),
|
|
||||||
doc: "the arbitrary HTML attributes to apply to the form tag"
|
|
||||||
|
|
||||||
slot :inner_block, required: true
|
|
||||||
slot :actions, doc: "the slot for form actions, such as a submit button"
|
|
||||||
|
|
||||||
def simple_form(assigns) do
|
|
||||||
~H"""
|
|
||||||
<.form :let={f} for={@for} as={@as} {@rest}>
|
|
||||||
<div class="mt-10 space-y-8 bg-white">
|
|
||||||
<%= render_slot(@inner_block, f) %>
|
|
||||||
<div :for={action <- @actions} class="mt-2 flex items-center justify-between gap-6">
|
|
||||||
<%= render_slot(action, f) %>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</.form>
|
|
||||||
"""
|
|
||||||
end
|
|
||||||
|
|
||||||
@doc """
|
|
||||||
Renders a button.
|
|
||||||
|
|
||||||
## Examples
|
|
||||||
|
|
||||||
<.button>Send!</.button>
|
|
||||||
<.button phx-click="go" class="ml-2">Send!</.button>
|
|
||||||
"""
|
|
||||||
attr :type, :string, default: nil
|
|
||||||
attr :class, :string, default: nil
|
|
||||||
attr :rest, :global, include: ~w(disabled form name value)
|
|
||||||
|
|
||||||
slot :inner_block, required: true
|
|
||||||
|
|
||||||
def button(assigns) do
|
|
||||||
~H"""
|
|
||||||
<button
|
|
||||||
type={@type}
|
|
||||||
class={[
|
|
||||||
"phx-submit-loading:opacity-75 rounded-lg bg-zinc-900 hover:bg-zinc-700 py-2 px-3",
|
|
||||||
"text-sm font-semibold leading-6 text-white active:text-white/80",
|
|
||||||
@class
|
|
||||||
]}
|
|
||||||
{@rest}
|
|
||||||
>
|
|
||||||
<%= render_slot(@inner_block) %>
|
|
||||||
</button>
|
|
||||||
"""
|
|
||||||
end
|
|
||||||
|
|
||||||
@doc """
|
|
||||||
Renders an input with label and error messages.
|
|
||||||
|
|
||||||
A `Phoenix.HTML.FormField` may be passed as argument,
|
|
||||||
which is used to retrieve the input name, id, and values.
|
|
||||||
Otherwise all attributes may be passed explicitly.
|
|
||||||
|
|
||||||
## Types
|
|
||||||
|
|
||||||
This function accepts all HTML input types, considering that:
|
|
||||||
|
|
||||||
* You may also set `type="select"` to render a `<select>` tag
|
|
||||||
|
|
||||||
* `type="checkbox"` is used exclusively to render boolean values
|
|
||||||
|
|
||||||
* For live file uploads, see `Phoenix.Component.live_file_input/1`
|
|
||||||
|
|
||||||
See https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input
|
|
||||||
for more information. Unsupported types, such as hidden and radio,
|
|
||||||
are best written directly in your templates.
|
|
||||||
|
|
||||||
## Examples
|
|
||||||
|
|
||||||
<.input field={@form[:email]} type="email" />
|
|
||||||
<.input name="my-input" errors={["oh no!"]} />
|
|
||||||
"""
|
|
||||||
attr :id, :any, default: nil
|
|
||||||
attr :name, :any
|
|
||||||
attr :label, :string, default: nil
|
|
||||||
attr :value, :any
|
|
||||||
|
|
||||||
attr :type, :string,
|
|
||||||
default: "text",
|
|
||||||
values: ~w(checkbox color date datetime-local email file month number password
|
|
||||||
range search select tel text textarea time url week)
|
|
||||||
|
|
||||||
attr :field, Phoenix.HTML.FormField,
|
|
||||||
doc: "a form field struct retrieved from the form, for example: @form[:email]"
|
|
||||||
|
|
||||||
attr :errors, :list, default: []
|
|
||||||
attr :checked, :boolean, doc: "the checked flag for checkbox inputs"
|
|
||||||
attr :prompt, :string, default: nil, doc: "the prompt for select inputs"
|
|
||||||
attr :options, :list, doc: "the options to pass to Phoenix.HTML.Form.options_for_select/2"
|
|
||||||
attr :multiple, :boolean, default: false, doc: "the multiple flag for select inputs"
|
|
||||||
|
|
||||||
attr :rest, :global,
|
|
||||||
include: ~w(accept autocomplete capture cols disabled form list max maxlength min minlength
|
|
||||||
multiple pattern placeholder readonly required rows size step)
|
|
||||||
|
|
||||||
def input(%{field: %Phoenix.HTML.FormField{} = field} = assigns) do
|
|
||||||
errors = if Phoenix.Component.used_input?(field), do: field.errors, else: []
|
|
||||||
|
|
||||||
assigns
|
|
||||||
|> assign(field: nil, id: assigns.id || field.id)
|
|
||||||
|> assign(:errors, Enum.map(errors, &translate_error(&1)))
|
|
||||||
|> assign_new(:name, fn -> if assigns.multiple, do: field.name <> "[]", else: field.name end)
|
|
||||||
|> assign_new(:value, fn -> field.value end)
|
|
||||||
|> input()
|
|
||||||
end
|
|
||||||
|
|
||||||
def input(%{type: "checkbox"} = assigns) do
|
|
||||||
assigns =
|
|
||||||
assign_new(assigns, :checked, fn ->
|
|
||||||
Phoenix.HTML.Form.normalize_value("checkbox", assigns[:value])
|
|
||||||
end)
|
|
||||||
|
|
||||||
~H"""
|
|
||||||
<div>
|
|
||||||
<label class="flex items-center gap-4 text-sm leading-6 text-zinc-600">
|
|
||||||
<input type="hidden" name={@name} value="false" disabled={@rest[:disabled]} />
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
id={@id}
|
|
||||||
name={@name}
|
|
||||||
value="true"
|
|
||||||
checked={@checked}
|
|
||||||
class="rounded border-zinc-300 text-zinc-900 focus:ring-0"
|
|
||||||
{@rest}
|
|
||||||
/>
|
|
||||||
<%= @label %>
|
|
||||||
</label>
|
|
||||||
<.error :for={msg <- @errors}><%= msg %></.error>
|
|
||||||
</div>
|
|
||||||
"""
|
|
||||||
end
|
|
||||||
|
|
||||||
def input(%{type: "select"} = assigns) do
|
|
||||||
~H"""
|
|
||||||
<div>
|
|
||||||
<.label for={@id}><%= @label %></.label>
|
|
||||||
<select
|
|
||||||
id={@id}
|
|
||||||
name={@name}
|
|
||||||
class="mt-2 block w-full rounded-md border border-gray-300 bg-white shadow-sm focus:border-zinc-400 focus:ring-0 sm:text-sm"
|
|
||||||
multiple={@multiple}
|
|
||||||
{@rest}
|
|
||||||
>
|
|
||||||
<option :if={@prompt} value=""><%= @prompt %></option>
|
|
||||||
<%= Phoenix.HTML.Form.options_for_select(@options, @value) %>
|
|
||||||
</select>
|
|
||||||
<.error :for={msg <- @errors}><%= msg %></.error>
|
|
||||||
</div>
|
|
||||||
"""
|
|
||||||
end
|
|
||||||
|
|
||||||
def input(%{type: "textarea"} = assigns) do
|
|
||||||
~H"""
|
|
||||||
<div>
|
|
||||||
<.label for={@id}><%= @label %></.label>
|
|
||||||
<textarea
|
|
||||||
id={@id}
|
|
||||||
name={@name}
|
|
||||||
class={[
|
|
||||||
"mt-2 block w-full rounded-lg text-zinc-900 focus:ring-0 sm:text-sm sm:leading-6 min-h-[6rem]",
|
|
||||||
@errors == [] && "border-zinc-300 focus:border-zinc-400",
|
|
||||||
@errors != [] && "border-rose-400 focus:border-rose-400"
|
|
||||||
]}
|
|
||||||
{@rest}
|
|
||||||
><%= Phoenix.HTML.Form.normalize_value("textarea", @value) %></textarea>
|
|
||||||
<.error :for={msg <- @errors}><%= msg %></.error>
|
|
||||||
</div>
|
|
||||||
"""
|
|
||||||
end
|
|
||||||
|
|
||||||
# All other inputs text, datetime-local, url, password, etc. are handled here...
|
|
||||||
def input(assigns) do
|
|
||||||
~H"""
|
|
||||||
<div>
|
|
||||||
<.label for={@id}><%= @label %></.label>
|
|
||||||
<input
|
|
||||||
type={@type}
|
|
||||||
name={@name}
|
|
||||||
id={@id}
|
|
||||||
value={Phoenix.HTML.Form.normalize_value(@type, @value)}
|
|
||||||
class={[
|
|
||||||
"mt-2 block w-full rounded-lg text-zinc-900 focus:ring-0 sm:text-sm sm:leading-6",
|
|
||||||
@errors == [] && "border-zinc-300 focus:border-zinc-400",
|
|
||||||
@errors != [] && "border-rose-400 focus:border-rose-400"
|
|
||||||
]}
|
|
||||||
{@rest}
|
|
||||||
/>
|
|
||||||
<.error :for={msg <- @errors}><%= msg %></.error>
|
|
||||||
</div>
|
|
||||||
"""
|
|
||||||
end
|
|
||||||
|
|
||||||
@doc """
|
|
||||||
Renders a label.
|
|
||||||
"""
|
|
||||||
attr :for, :string, default: nil
|
|
||||||
slot :inner_block, required: true
|
|
||||||
|
|
||||||
def label(assigns) do
|
|
||||||
~H"""
|
|
||||||
<label for={@for} class="block text-sm font-semibold leading-6 text-zinc-800">
|
|
||||||
<%= render_slot(@inner_block) %>
|
|
||||||
</label>
|
|
||||||
"""
|
|
||||||
end
|
|
||||||
|
|
||||||
@doc """
|
|
||||||
Generates a generic error message.
|
|
||||||
"""
|
|
||||||
slot :inner_block, required: true
|
|
||||||
|
|
||||||
def error(assigns) do
|
|
||||||
~H"""
|
|
||||||
<p class="mt-3 flex gap-3 text-sm leading-6 text-rose-600">
|
|
||||||
<.icon name="hero-exclamation-circle-mini" class="mt-0.5 h-5 w-5 flex-none" />
|
|
||||||
<%= render_slot(@inner_block) %>
|
|
||||||
</p>
|
|
||||||
"""
|
|
||||||
end
|
|
||||||
|
|
||||||
@doc """
|
|
||||||
Renders a header with title.
|
|
||||||
"""
|
|
||||||
attr :class, :string, default: nil
|
|
||||||
|
|
||||||
slot :inner_block, required: true
|
|
||||||
slot :subtitle
|
|
||||||
slot :actions
|
|
||||||
|
|
||||||
def header(assigns) do
|
|
||||||
~H"""
|
|
||||||
<header class={[@actions != [] && "flex items-center justify-between gap-6", @class]}>
|
|
||||||
<div>
|
|
||||||
<h1 class="text-lg font-semibold leading-8 text-zinc-800">
|
|
||||||
<%= render_slot(@inner_block) %>
|
|
||||||
</h1>
|
|
||||||
<p :if={@subtitle != []} class="mt-2 text-sm leading-6 text-zinc-600">
|
|
||||||
<%= render_slot(@subtitle) %>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div class="flex-none"><%= render_slot(@actions) %></div>
|
|
||||||
</header>
|
|
||||||
"""
|
|
||||||
end
|
|
||||||
|
|
||||||
@doc ~S"""
|
|
||||||
Renders a table with generic styling.
|
|
||||||
|
|
||||||
## Examples
|
|
||||||
|
|
||||||
<.table id="users" rows={@users}>
|
|
||||||
<:col :let={user} label="id"><%= user.id %></:col>
|
|
||||||
<:col :let={user} label="username"><%= user.username %></:col>
|
|
||||||
</.table>
|
|
||||||
"""
|
|
||||||
attr :id, :string, required: true
|
|
||||||
attr :rows, :list, required: true
|
|
||||||
attr :row_id, :any, default: nil, doc: "the function for generating the row id"
|
|
||||||
attr :row_click, :any, default: nil, doc: "the function for handling phx-click on each row"
|
|
||||||
|
|
||||||
attr :row_item, :any,
|
|
||||||
default: &Function.identity/1,
|
|
||||||
doc: "the function for mapping each row before calling the :col and :action slots"
|
|
||||||
|
|
||||||
slot :col, required: true do
|
|
||||||
attr :label, :string
|
|
||||||
end
|
|
||||||
|
|
||||||
slot :action, doc: "the slot for showing user actions in the last table column"
|
|
||||||
|
|
||||||
def table(assigns) do
|
|
||||||
assigns =
|
|
||||||
with %{rows: %Phoenix.LiveView.LiveStream{}} <- assigns do
|
|
||||||
assign(assigns, row_id: assigns.row_id || fn {id, _item} -> id end)
|
|
||||||
end
|
|
||||||
|
|
||||||
~H"""
|
|
||||||
<div class="overflow-y-auto px-4 sm:overflow-visible sm:px-0">
|
|
||||||
<table class="w-[40rem] mt-11 sm:w-full">
|
|
||||||
<thead class="text-sm text-left leading-6 text-zinc-500">
|
|
||||||
<tr>
|
|
||||||
<th :for={col <- @col} class="p-0 pb-4 pr-6 font-normal"><%= col[:label] %></th>
|
|
||||||
<th :if={@action != []} class="relative p-0 pb-4">
|
|
||||||
<span class="sr-only"><%= gettext("Actions") %></span>
|
|
||||||
</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody
|
|
||||||
id={@id}
|
|
||||||
phx-update={match?(%Phoenix.LiveView.LiveStream{}, @rows) && "stream"}
|
|
||||||
class="relative divide-y divide-zinc-100 border-t border-zinc-200 text-sm leading-6 text-zinc-700"
|
|
||||||
>
|
|
||||||
<tr :for={row <- @rows} id={@row_id && @row_id.(row)} class="group hover:bg-zinc-50">
|
|
||||||
<td
|
|
||||||
:for={{col, i} <- Enum.with_index(@col)}
|
|
||||||
phx-click={@row_click && @row_click.(row)}
|
|
||||||
class={["relative p-0", @row_click && "hover:cursor-pointer"]}
|
|
||||||
>
|
|
||||||
<div class="block py-4 pr-6">
|
|
||||||
<span class="absolute -inset-y-px right-0 -left-4 group-hover:bg-zinc-50 sm:rounded-l-xl" />
|
|
||||||
<span class={["relative", i == 0 && "font-semibold text-zinc-900"]}>
|
|
||||||
<%= render_slot(col, @row_item.(row)) %>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td :if={@action != []} class="relative w-14 p-0">
|
|
||||||
<div class="relative whitespace-nowrap py-4 text-right text-sm font-medium">
|
|
||||||
<span class="absolute -inset-y-px -right-4 left-0 group-hover:bg-zinc-50 sm:rounded-r-xl" />
|
|
||||||
<span
|
|
||||||
:for={action <- @action}
|
|
||||||
class="relative ml-4 font-semibold leading-6 text-zinc-900 hover:text-zinc-700"
|
|
||||||
>
|
|
||||||
<%= render_slot(action, @row_item.(row)) %>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
"""
|
|
||||||
end
|
|
||||||
|
|
||||||
@doc """
|
|
||||||
Renders a data list.
|
|
||||||
|
|
||||||
## Examples
|
|
||||||
|
|
||||||
<.list>
|
|
||||||
<:item title="Title"><%= @post.title %></:item>
|
|
||||||
<:item title="Views"><%= @post.views %></:item>
|
|
||||||
</.list>
|
|
||||||
"""
|
|
||||||
slot :item, required: true do
|
|
||||||
attr :title, :string, required: true
|
|
||||||
end
|
|
||||||
|
|
||||||
def list(assigns) do
|
|
||||||
~H"""
|
|
||||||
<div class="mt-14">
|
|
||||||
<dl class="-my-4 divide-y divide-zinc-100">
|
|
||||||
<div :for={item <- @item} class="flex gap-4 py-4 text-sm leading-6 sm:gap-8">
|
|
||||||
<dt class="w-1/4 flex-none text-zinc-500"><%= item.title %></dt>
|
|
||||||
<dd class="text-zinc-700"><%= render_slot(item) %></dd>
|
|
||||||
</div>
|
|
||||||
</dl>
|
|
||||||
</div>
|
|
||||||
"""
|
|
||||||
end
|
|
||||||
|
|
||||||
@doc """
|
|
||||||
Renders a back navigation link.
|
|
||||||
|
|
||||||
## Examples
|
|
||||||
|
|
||||||
<.back navigate={~p"/posts"}>Back to posts</.back>
|
|
||||||
"""
|
|
||||||
attr :navigate, :any, required: true
|
|
||||||
slot :inner_block, required: true
|
|
||||||
|
|
||||||
def back(assigns) do
|
|
||||||
~H"""
|
|
||||||
<div class="mt-16">
|
|
||||||
<.link
|
|
||||||
navigate={@navigate}
|
|
||||||
class="text-sm font-semibold leading-6 text-zinc-900 hover:text-zinc-700"
|
|
||||||
>
|
|
||||||
<.icon name="hero-arrow-left-solid" class="h-3 w-3" />
|
|
||||||
<%= render_slot(@inner_block) %>
|
|
||||||
</.link>
|
|
||||||
</div>
|
|
||||||
"""
|
|
||||||
end
|
|
||||||
|
|
||||||
@doc """
|
|
||||||
Renders a [Heroicon](https://heroicons.com).
|
|
||||||
|
|
||||||
Heroicons come in three styles – outline, solid, and mini.
|
|
||||||
By default, the outline style is used, but solid and mini may
|
|
||||||
be applied by using the `-solid` and `-mini` suffix.
|
|
||||||
|
|
||||||
You can customize the size and colors of the icons by setting
|
|
||||||
width, height, and background color classes.
|
|
||||||
|
|
||||||
Icons are extracted from the `deps/heroicons` directory and bundled within
|
|
||||||
your compiled app.css by the plugin in your `assets/tailwind.config.js`.
|
|
||||||
|
|
||||||
## Examples
|
|
||||||
|
|
||||||
<.icon name="hero-x-mark-solid" />
|
|
||||||
<.icon name="hero-arrow-path" class="ml-1 w-3 h-3 animate-spin" />
|
|
||||||
"""
|
|
||||||
attr :name, :string, required: true
|
|
||||||
attr :class, :string, default: nil
|
|
||||||
|
|
||||||
def icon(%{name: "hero-" <> _} = assigns) do
|
|
||||||
~H"""
|
|
||||||
<span class={[@name, @class]} />
|
|
||||||
"""
|
|
||||||
end
|
|
||||||
|
|
||||||
## JS Commands
|
|
||||||
|
|
||||||
def show(js \\ %JS{}, selector) do
|
|
||||||
JS.show(js,
|
|
||||||
to: selector,
|
|
||||||
time: 300,
|
|
||||||
transition:
|
|
||||||
{"transition-all transform ease-out duration-300",
|
|
||||||
"opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95",
|
|
||||||
"opacity-100 translate-y-0 sm:scale-100"}
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
def hide(js \\ %JS{}, selector) do
|
|
||||||
JS.hide(js,
|
|
||||||
to: selector,
|
|
||||||
time: 200,
|
|
||||||
transition:
|
|
||||||
{"transition-all transform ease-in duration-200",
|
|
||||||
"opacity-100 translate-y-0 sm:scale-100",
|
|
||||||
"opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"}
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
def show_modal(js \\ %JS{}, id) when is_binary(id) do
|
|
||||||
js
|
|
||||||
|> JS.show(to: "##{id}")
|
|
||||||
|> JS.show(
|
|
||||||
to: "##{id}-bg",
|
|
||||||
time: 300,
|
|
||||||
transition: {"transition-all transform ease-out duration-300", "opacity-0", "opacity-100"}
|
|
||||||
)
|
|
||||||
|> show("##{id}-container")
|
|
||||||
|> JS.add_class("overflow-hidden", to: "body")
|
|
||||||
|> JS.focus_first(to: "##{id}-content")
|
|
||||||
end
|
|
||||||
|
|
||||||
def hide_modal(js \\ %JS{}, id) do
|
|
||||||
js
|
|
||||||
|> JS.hide(
|
|
||||||
to: "##{id}-bg",
|
|
||||||
transition: {"transition-all transform ease-in duration-200", "opacity-100", "opacity-0"}
|
|
||||||
)
|
|
||||||
|> hide("##{id}-container")
|
|
||||||
|> JS.hide(to: "##{id}", transition: {"block", "block", "hidden"})
|
|
||||||
|> JS.remove_class("overflow-hidden", to: "body")
|
|
||||||
|> JS.pop_focus()
|
|
||||||
end
|
|
||||||
|
|
||||||
@doc """
|
|
||||||
Translates an error message using gettext.
|
|
||||||
"""
|
|
||||||
def translate_error({msg, opts}) do
|
|
||||||
# When using gettext, we typically pass the strings we want
|
|
||||||
# to translate as a static argument:
|
|
||||||
#
|
|
||||||
# # Translate the number of files with plural rules
|
|
||||||
# dngettext("errors", "1 file", "%{count} files", count)
|
|
||||||
#
|
|
||||||
# However the error messages in our forms and APIs are generated
|
|
||||||
# dynamically, so we need to translate them by calling Gettext
|
|
||||||
# with our gettext backend as first argument. Translations are
|
|
||||||
# available in the errors.po file (as we use the "errors" domain).
|
|
||||||
if count = opts[:count] do
|
|
||||||
Gettext.dngettext(Mse25Web.Gettext, "errors", msg, msg, count, opts)
|
|
||||||
else
|
|
||||||
Gettext.dgettext(Mse25Web.Gettext, "errors", msg, opts)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
@doc """
|
|
||||||
Translates the errors for a field from a keyword list of errors.
|
|
||||||
"""
|
|
||||||
def translate_errors(errors, field) when is_list(errors) do
|
|
||||||
for {^field, {msg, opts}} <- errors, do: translate_error({msg, opts})
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,114 @@
|
||||||
defmodule Mse25Web.Layouts do
|
defmodule Mse25Web.Layouts do
|
||||||
@moduledoc """
|
|
||||||
This module holds different layouts used by your application.
|
|
||||||
|
|
||||||
See the `layouts` directory for all templates available.
|
|
||||||
The "root" layout is a skeleton rendered as part of the
|
|
||||||
application router. The "app" layout is set as the default
|
|
||||||
layout on both `use Mse25Web, :controller` and
|
|
||||||
`use Mse25Web, :live_view`.
|
|
||||||
"""
|
|
||||||
use Mse25Web, :html
|
use Mse25Web, :html
|
||||||
|
|
||||||
|
@url "https://madr.se"
|
||||||
|
@list_views ["webblogg", "delningar", "evenemang"]
|
||||||
|
|
||||||
embed_templates "layouts/*"
|
embed_templates "layouts/*"
|
||||||
|
|
||||||
|
def canonical(%{year: _, conn: %{path_info: path}}) do
|
||||||
|
~s"""
|
||||||
|
<link rel="canonical" href="#{@url}/#{Enum.join(path, "/")}" />
|
||||||
|
"""
|
||||||
|
end
|
||||||
|
|
||||||
|
def canonical(_) do
|
||||||
|
""
|
||||||
|
end
|
||||||
|
|
||||||
|
def opengraph(%{heading: title, lead: lead, conn: %{path_info: path}}) do
|
||||||
|
~s"""
|
||||||
|
<meta property="og:title" content="#{title}" />
|
||||||
|
<meta property="og:description" content="#{lead}" />
|
||||||
|
<meta property="og:type" content="event" />
|
||||||
|
<meta property="og:url" content="#{@url}/#{Enum.join(path, "/")}" />
|
||||||
|
<meta property="og:site_name" content="madr.se" />
|
||||||
|
"""
|
||||||
|
end
|
||||||
|
|
||||||
|
def opengraph(%{heading: title, conn: %{path_info: path}}) do
|
||||||
|
~s"""
|
||||||
|
<meta property="og:title" content="#{title}" />
|
||||||
|
<meta property="og:type" content="article" />
|
||||||
|
<meta property="og:url" content="#{@url}/#{Enum.join(path, "/")}" />
|
||||||
|
<meta property="og:site_name" content="madr.se" />
|
||||||
|
"""
|
||||||
|
end
|
||||||
|
|
||||||
|
def opengraph(%{page_title: title, conn: %{path_info: path}}) do
|
||||||
|
~s"""
|
||||||
|
<meta property="og:title" content="#{title}" />
|
||||||
|
<meta property="og:type" content="page" />
|
||||||
|
<meta property="og:url" content="#{@url}/#{Enum.join(path, "/")}" />
|
||||||
|
<meta property="og:site_name" content="madr.se" />
|
||||||
|
"""
|
||||||
|
end
|
||||||
|
|
||||||
|
def robots(%{conn: %{path_info: [first | []]}}) do
|
||||||
|
case Integer.parse(first) do
|
||||||
|
:error ->
|
||||||
|
case Enum.member?(@list_views, first) do
|
||||||
|
true ->
|
||||||
|
~s"""
|
||||||
|
<meta name="robots" content="noindex,follow" />
|
||||||
|
"""
|
||||||
|
|
||||||
|
false ->
|
||||||
|
~s"""
|
||||||
|
<meta name="robots" content="index,follow" />
|
||||||
|
"""
|
||||||
|
end
|
||||||
|
|
||||||
|
{_i, _d} ->
|
||||||
|
~s"""
|
||||||
|
<meta name="robots" content="noindex,follow" />
|
||||||
|
"""
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def robots(%{conn: %{path_info: [_p, _c]}}) do
|
||||||
|
~s"""
|
||||||
|
<meta name="robots" content="index,follow" />
|
||||||
|
"""
|
||||||
|
end
|
||||||
|
|
||||||
|
def robots(_) do
|
||||||
|
~s"""
|
||||||
|
<meta name="robots" content="noindex,follow" />
|
||||||
|
"""
|
||||||
|
end
|
||||||
|
|
||||||
|
def breadcrumbs(nodes) do
|
||||||
|
breadcrumbs([], "", 1, nodes)
|
||||||
|
end
|
||||||
|
|
||||||
|
def breadcrumbs(seen, _path, _index, []) do
|
||||||
|
Enum.reverse(seen)
|
||||||
|
end
|
||||||
|
|
||||||
|
def breadcrumbs(seen, path, index, [{slug, name} | nodes]) do
|
||||||
|
breadcrumbs(
|
||||||
|
[{index + 1, {path <> "/" <> to_string(slug), name}} | seen],
|
||||||
|
path <> "/" <> to_string(slug),
|
||||||
|
index + 1,
|
||||||
|
nodes
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def breadcrumbs(seen, path, index, [{slug, name, custom_prefix} | nodes]) do
|
||||||
|
breadcrumbs(
|
||||||
|
[{index + 1, {custom_prefix <> "/" <> to_string(slug), name}} | seen],
|
||||||
|
path <> "/" <> to_string(slug),
|
||||||
|
index + 1,
|
||||||
|
nodes
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def show_interactive_event_map?(assigns) do
|
||||||
|
Map.has_key?(assigns, :events)
|
||||||
|
end
|
||||||
|
|
||||||
|
def show_footer?(%{heading: "Kolofon"}), do: false
|
||||||
|
|
||||||
|
def show_footer?(%{}), do: true
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,37 @@
|
||||||
<main>
|
<a href="#content" class="skiplink">Hoppa till innehållet</a>
|
||||||
|
|
||||||
|
<nav>
|
||||||
|
<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>
|
||||||
|
<%= for {index, {parent_slug, parent_name}} <- breadcrumbs(@breadcrumbs) do %>
|
||||||
|
<span class="sr-only">></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 %>
|
<%= @inner_content %>
|
||||||
</main>
|
</main>
|
||||||
<!--
|
<%= if show_footer?(assigns) do %>
|
||||||
<footer>
|
<footer>
|
||||||
<p>
|
<p vocab="https://schema.org/" typeof="Person">
|
||||||
<a href="https://madr.se" rel="home">madr.se</a>
|
<a href="https://madr.se" property="url">madr.se</a>
|
||||||
av Anders Englöf Ytterström, sedan 2006. <a href="/colophon">Kolofon</a>.
|
är <span property="name">Anders Englöf Ytterström</span>s hemsida. Anders är <span property="jobTitle">webbutvecklare</span>, linuxentusiast, ljudtekniker och hårdrockare, bosatt i <span
|
||||||
</p>
|
property="address"
|
||||||
<ul>
|
typeof="PostalAddress"
|
||||||
<li><a href="https://github.com/madr" rel="external">Github</a></li>
|
><span property="addressLocality">Borlänge</span> (<span property="addressRegion">Dalarna</span>)</span>.
|
||||||
<li><a href="https://linkedin.com/anders-ytterstrom" rel="external">LinkedIn</a></li>
|
Läs <a href="/colophon">kolofonen</a>.
|
||||||
<li><a href="https://discogs.com/madr" rel="external">Discogs</a></li>
|
</p>
|
||||||
<li><a href="https://songkick.com/madr" rel="external">Songkick</a></li>
|
</footer>
|
||||||
</ul>
|
<% end %>
|
||||||
</footer>
|
|
||||||
-->
|
|
||||||
|
|
|
||||||
|
|
@ -2,14 +2,32 @@
|
||||||
<html lang="sv">
|
<html lang="sv">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
|
<title><%= assigns.page_title || "Anders Englöf Ytterström" %> | madr.se</title>
|
||||||
<meta name="viewport" content="width=device-width" />
|
<meta name="viewport" content="width=device-width" />
|
||||||
<meta name="csrf-token" content={get_csrf_token()} />
|
<meta name="robots" content="noimageai" />
|
||||||
<title><%= assigns[:page_title] || "Anders Englöf Ytterström" %> | madr.se</title>
|
<meta name="robots" content="noai" />
|
||||||
|
<meta name="author" content="Anders Englöf Ytterström" />
|
||||||
<link rel="stylesheet" href={~p"/assets/app.css"} />
|
<link rel="stylesheet" href={~p"/assets/app.css"} />
|
||||||
|
<link
|
||||||
|
href="/prenumerera.xml"
|
||||||
|
type="application/rss+xml"
|
||||||
|
rel="alternate"
|
||||||
|
title="madr.se: inlägg, evenemang, delningar"
|
||||||
|
/>
|
||||||
|
<%= canonical(assigns) |> raw %>
|
||||||
|
<%= opengraph(assigns) |> raw %>
|
||||||
|
<%= robots(assigns) |> raw %>
|
||||||
|
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
|
||||||
|
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
|
||||||
|
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
|
||||||
</head>
|
</head>
|
||||||
<body class="bg-white">
|
<body>
|
||||||
<%= @inner_content %>
|
<%= @inner_content %>
|
||||||
<script src={~p"/assets/app.js"}>
|
<script src={~p"/assets/app.js"}>
|
||||||
</script>
|
</script>
|
||||||
|
<%= if show_interactive_event_map?(assigns) do %>
|
||||||
|
<script src="/event-map.js">
|
||||||
|
</script>
|
||||||
|
<% end %>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
144
lib/mse25_web/controllers/feed_controller.ex
Normal file
|
|
@ -0,0 +1,144 @@
|
||||||
|
defmodule Mse25Web.FeedController do
|
||||||
|
use Mse25Web, :controller
|
||||||
|
alias Mse25.Directus
|
||||||
|
alias Mse25.Timeline
|
||||||
|
plug :put_layout, false
|
||||||
|
|
||||||
|
def feed(conn, _params) do
|
||||||
|
{:ok, %{archive: items}} = Timeline.archive(20)
|
||||||
|
|
||||||
|
text(
|
||||||
|
conn |> put_resp_content_type("application/rss+xml"),
|
||||||
|
items
|
||||||
|
|> Mse25Web.FeedView.rss(conn.host)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def calendar(conn, _) do
|
||||||
|
text(
|
||||||
|
conn |> put_resp_content_type("text/calendar"),
|
||||||
|
Directus.get_events!(upcoming: true, limit: 9999)
|
||||||
|
|> Enum.map(fn %{
|
||||||
|
"id" => id,
|
||||||
|
"title" => title,
|
||||||
|
"lead" => lead,
|
||||||
|
"started_at" => starts_at,
|
||||||
|
"ended_at" => ends_at,
|
||||||
|
"date_created" => created_at,
|
||||||
|
"location" => %{
|
||||||
|
"name" => venue,
|
||||||
|
"address" => region,
|
||||||
|
"position" => %{
|
||||||
|
"coordinates" => [lat, lng]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} ->
|
||||||
|
%{
|
||||||
|
id: id,
|
||||||
|
title: title,
|
||||||
|
lead: lead,
|
||||||
|
region: region,
|
||||||
|
venue: venue,
|
||||||
|
latitude: lat,
|
||||||
|
longitude: lng,
|
||||||
|
all_day?: true,
|
||||||
|
updated_at: created_at |> String.slice(0..18) |> String.replace(~r/[-:]/, ""),
|
||||||
|
created_at: created_at |> String.slice(0..18) |> String.replace(~r/[-:]/, ""),
|
||||||
|
starts_at: String.replace(starts_at, "-", ""),
|
||||||
|
ends_at: String.replace(ends_at, "-", "")
|
||||||
|
}
|
||||||
|
end)
|
||||||
|
|> Mse25Web.FeedView.calendar()
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def albums(conn, _) do
|
||||||
|
json(
|
||||||
|
conn,
|
||||||
|
Directus.get_albums!()
|
||||||
|
|> Enum.map(fn %{
|
||||||
|
"album" => album,
|
||||||
|
"artist" => artist,
|
||||||
|
"externalId" => id,
|
||||||
|
"year" => year,
|
||||||
|
"purchased_at" => purchased_on,
|
||||||
|
"contents" => contents,
|
||||||
|
"songs" => songs
|
||||||
|
} ->
|
||||||
|
{img, ""} = Integer.parse(id)
|
||||||
|
|
||||||
|
%{
|
||||||
|
id: id,
|
||||||
|
img: to_string(img - 1) <> ".jpg",
|
||||||
|
title: album,
|
||||||
|
artist: artist,
|
||||||
|
album: album,
|
||||||
|
year: year,
|
||||||
|
purchased_on: purchased_on,
|
||||||
|
description: Earmark.as_html!(contents),
|
||||||
|
songs: Enum.map(songs, fn %{"title" => song} -> song end)
|
||||||
|
}
|
||||||
|
end)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def events(conn, _) do
|
||||||
|
json(
|
||||||
|
conn,
|
||||||
|
Directus.get_events!(limit: 9999)
|
||||||
|
|> Enum.map(fn %{
|
||||||
|
"title" => title,
|
||||||
|
"lead" => lead,
|
||||||
|
"poster" => img,
|
||||||
|
"started_at" => date,
|
||||||
|
"location" => %{
|
||||||
|
"name" => venue,
|
||||||
|
"address" => region,
|
||||||
|
"position" => %{
|
||||||
|
"coordinates" => [lat, lng]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"bands" => bands
|
||||||
|
} ->
|
||||||
|
%{
|
||||||
|
title: title,
|
||||||
|
lead: lead,
|
||||||
|
img: img,
|
||||||
|
date: String.slice(date, 0..9),
|
||||||
|
region: region,
|
||||||
|
venue: venue,
|
||||||
|
location: [lng, lat],
|
||||||
|
bands: Enum.map(bands, fn %{"artists_id" => %{"name" => band}} -> band end)
|
||||||
|
}
|
||||||
|
end)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def interactive_event_map(conn, _) do
|
||||||
|
text(
|
||||||
|
conn |> put_resp_content_type("text/javascript"),
|
||||||
|
Directus.get_events!(limit: 9999)
|
||||||
|
|> Enum.map(fn %{
|
||||||
|
"title" => title,
|
||||||
|
"started_at" => date,
|
||||||
|
"location" => %{
|
||||||
|
"name" => venue,
|
||||||
|
"address" => region,
|
||||||
|
"position" => %{
|
||||||
|
"coordinates" => [lat, lng]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} ->
|
||||||
|
%{
|
||||||
|
title: title,
|
||||||
|
date: String.slice(date, 0..9),
|
||||||
|
region: region,
|
||||||
|
venue: venue,
|
||||||
|
longitude: lng,
|
||||||
|
latitude: lat
|
||||||
|
}
|
||||||
|
end)
|
||||||
|
|> Mse25Web.FeedView.event_map()
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
269
lib/mse25_web/controllers/feed_view.ex
Normal file
|
|
@ -0,0 +1,269 @@
|
||||||
|
defmodule Mse25Web.FeedView do
|
||||||
|
use Mse25Web, :html
|
||||||
|
|
||||||
|
def rss(items, _host) do
|
||||||
|
~s"""
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:georss="http://www.georss.org/georss" xmlns:gml="http://www.opengis.net/gml">
|
||||||
|
<channel>
|
||||||
|
<title>madr.se</title>
|
||||||
|
<description>The online home of Anders Englöf Ytterström, a metalhead and musician living and working in Borlänge, Sweden.</description>
|
||||||
|
<language>sv</language>
|
||||||
|
<link>https://madr.se/</link>
|
||||||
|
<managingEditor>yttan@fastmail.se (Anders Englöf Ytterström)</managingEditor>
|
||||||
|
<webMaster>yttan@fastmail.se (Anders Englöf Ytterström)</webMaster>
|
||||||
|
<atom:link href="https://madr.se/prenumerera.xml" rel="self" type="application/rss+xml" />
|
||||||
|
#{Enum.map(items, &rss_item/1)}
|
||||||
|
</channel>
|
||||||
|
</rss>
|
||||||
|
"""
|
||||||
|
end
|
||||||
|
|
||||||
|
def calendar(upcoming) do
|
||||||
|
~s"""
|
||||||
|
BEGIN:VCALENDAR
|
||||||
|
VERSION:2.0
|
||||||
|
PRODID:-//https://madr.se//kommande-evenemang.ics//SE
|
||||||
|
CALSCALE:GREGORIAN
|
||||||
|
X-ORIGINAL-URL:https://madr.se
|
||||||
|
X-WR-CALDESC: Kommande evenemang, madr.se
|
||||||
|
METHOD:PUBLISH
|
||||||
|
REFRESH-INTERVAL;VALUE=DURATION:PT1H
|
||||||
|
X-Robots-Tag:noindex
|
||||||
|
X-PUBLISHED-TTL:PT1H
|
||||||
|
BEGIN:VTIMEZONE
|
||||||
|
TZID:CEST
|
||||||
|
BEGIN:STANDARD
|
||||||
|
TZOFFSETFROM:+0200
|
||||||
|
TZOFFSETTO:+0200
|
||||||
|
TZNAME:CEST
|
||||||
|
DTSTART:20000630T000000
|
||||||
|
END:STANDARD
|
||||||
|
END:VTIMEZONE
|
||||||
|
#{upcoming |> Enum.map(fn %{id: id, title: title, created_at: created_at, starts_at: starts_at, ends_at: ends_at, longitude: longitude, latitude: latitude, lead: lead, venue: venue, region: region} -> ~s"""
|
||||||
|
BEGIN:VEVENT
|
||||||
|
UID:#{starts_at}.#{id}@madr.se
|
||||||
|
DTSTAMP:#{created_at}
|
||||||
|
CREATED:#{created_at}
|
||||||
|
LAST-MODIFIED:#{created_at}
|
||||||
|
DTSTART;TZID=CEST:#{starts_at}T060606
|
||||||
|
DTEND;TZID=CEST:#{ends_at}T060606
|
||||||
|
SUMMARY:#{title}
|
||||||
|
DESCRIPTION:#{lead}
|
||||||
|
LOCATION:#{venue}\, #{region}
|
||||||
|
GEO:#{latitude};#{longitude}
|
||||||
|
END:VEVENT
|
||||||
|
""" end) |> Enum.join("")}END:VCALENDAR
|
||||||
|
"""
|
||||||
|
end
|
||||||
|
|
||||||
|
def event_map(markers) do
|
||||||
|
~s"""
|
||||||
|
(function(g, document) {
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
const mapData = [
|
||||||
|
#{markers |> Enum.map(fn %{date: date, latitude: latitude, longitude: longitude, title: title, region: region, venue: venue} -> ~s"""
|
||||||
|
{
|
||||||
|
location: [#{longitude}, #{latitude}],
|
||||||
|
title: "#{title}",
|
||||||
|
date: "#{date}",
|
||||||
|
region: "#{region}",
|
||||||
|
venue: "#{venue}"
|
||||||
|
}
|
||||||
|
""" end) |> Enum.join(",")}
|
||||||
|
]
|
||||||
|
|
||||||
|
// insert Leaflet styles (<link>) to <head> and <script> to
|
||||||
|
// bottom of <body>, and initiate the map when the assets have
|
||||||
|
// loaded.
|
||||||
|
var install = function() {
|
||||||
|
var styles, leaflet;
|
||||||
|
|
||||||
|
styles = document.createElement("link");
|
||||||
|
styles.rel = "stylesheet";
|
||||||
|
styles.href = "https://unpkg.com/leaflet@1.6.0/dist/leaflet.css";
|
||||||
|
styles.integrity =
|
||||||
|
"sha512-xwE/Az9zrjBIphAcBb3F6JVqxf46+CDLwfLMHloNu6KEQCAWi6HcDUbeOfBIptF7tcCzusKFjFw2yuvEpDL9wQ==";
|
||||||
|
styles.crossOrigin = "";
|
||||||
|
|
||||||
|
document.head.appendChild(styles);
|
||||||
|
|
||||||
|
leaflet = document.createElement("script");
|
||||||
|
leaflet.src = "https://unpkg.com/leaflet@1.6.0/dist/leaflet.js";
|
||||||
|
leaflet.integrity =
|
||||||
|
"sha512-gZwIG9x3wUXg2hdXF6+rVkLF/0Vi9U8D2Ntg4Ga5I5BZpVkVxlJWbSQtXPSiUTtC0TjtGOmxa1AJPuV0CPthew==";
|
||||||
|
leaflet.crossOrigin = "";
|
||||||
|
leaflet.async = true;
|
||||||
|
leaflet.onload = function() {
|
||||||
|
init(mapData);
|
||||||
|
};
|
||||||
|
|
||||||
|
document.body.appendChild(leaflet);
|
||||||
|
};
|
||||||
|
|
||||||
|
// inititate the map.
|
||||||
|
// create markers for all events: one marker per venue. Display all
|
||||||
|
// events on each venue through a popup by clicking on the marker.
|
||||||
|
// set bounds to have all markers visible.
|
||||||
|
var init = function(mapData) {
|
||||||
|
var events,
|
||||||
|
markers = {},
|
||||||
|
L = g.L;
|
||||||
|
|
||||||
|
if (L) {
|
||||||
|
// create map and set initial bounds
|
||||||
|
events = L.map("leaflet", { zoomDelta: 0.25, zoomSnap: 0 }).fitBounds(
|
||||||
|
mapData.map(function(e) {
|
||||||
|
return e.location;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// use OpenStreetMap tile layer: it's free!
|
||||||
|
L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png?{foo}", {
|
||||||
|
foo: "bar",
|
||||||
|
attribution:
|
||||||
|
'Map data © <a href="https://www.openstreetmap.org/">OpenStreetMap</a> contributors, <a href="https://creativecommons.org/licenses/by-sa/2.0/">CC-BY-SA</a>'
|
||||||
|
}).addTo(events);
|
||||||
|
|
||||||
|
// group events by venue
|
||||||
|
mapData.forEach(function(e) {
|
||||||
|
if (markers[e.venue]) {
|
||||||
|
markers[e.venue].events.push(e.date + " - " + e.title);
|
||||||
|
} else {
|
||||||
|
markers[e.venue] = {
|
||||||
|
region: e.region,
|
||||||
|
location: e.location,
|
||||||
|
events: [e.date + " - " + e.title]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// create markers
|
||||||
|
for (var m in markers) {
|
||||||
|
if (markers.hasOwnProperty(m)) {
|
||||||
|
L.marker(markers[m].location)
|
||||||
|
.addTo(events)
|
||||||
|
.bindPopup(
|
||||||
|
"<strong style='color: black'>" +
|
||||||
|
m +
|
||||||
|
", " +
|
||||||
|
markers[m].region +
|
||||||
|
"</strong><br>" +
|
||||||
|
markers[m].events.join("<br>")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
install();
|
||||||
|
})(this, document);
|
||||||
|
"""
|
||||||
|
end
|
||||||
|
|
||||||
|
def pub_date(nil), do: ""
|
||||||
|
def pub_date(%{"pubDate" => date}), do: format_rfc822(date)
|
||||||
|
def pub_date(%{"started_at" => date}), do: format_rfc822(date)
|
||||||
|
def pub_date(%{"purchased_at" => date}), do: format_rfc822(date)
|
||||||
|
|
||||||
|
defp format_rfc822(date_time) when is_binary(date_time) do
|
||||||
|
date_time
|
||||||
|
|> Date.from_iso8601!()
|
||||||
|
|> format_rfc822()
|
||||||
|
end
|
||||||
|
|
||||||
|
defp format_rfc822(date_time), do: Calendar.strftime(date_time, "%a, %d %b %Y 06:06:06 +0200")
|
||||||
|
|
||||||
|
defp rss_item(%{
|
||||||
|
:t => :articles,
|
||||||
|
"title" => title,
|
||||||
|
"contents" => contents,
|
||||||
|
"slug" => slug,
|
||||||
|
"pubDate" => date
|
||||||
|
}) do
|
||||||
|
~s"""
|
||||||
|
<item>
|
||||||
|
<title>#{title}</title>
|
||||||
|
<link>https://madr.se/#{slug}</link>
|
||||||
|
<description>
|
||||||
|
<![CDATA[#{Earmark.as_html!(contents)}]]>
|
||||||
|
</description>
|
||||||
|
<pubDate>#{format_rfc822(date)}</pubDate>
|
||||||
|
<guid>https://madr.se/#{slug}</guid>
|
||||||
|
</item>
|
||||||
|
"""
|
||||||
|
end
|
||||||
|
|
||||||
|
defp rss_item(%{
|
||||||
|
:t => :events,
|
||||||
|
"title" => title,
|
||||||
|
"contents" => contents,
|
||||||
|
"slug" => slug,
|
||||||
|
"location" => %{
|
||||||
|
"position" => %{
|
||||||
|
"coordinates" => [lng, lat]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"started_at" => date
|
||||||
|
}) do
|
||||||
|
~s"""
|
||||||
|
<item>
|
||||||
|
<title>#{title}</title>
|
||||||
|
<link>https://madr.se/#{slug}</link>
|
||||||
|
<description>
|
||||||
|
<![CDATA[#{Earmark.as_html!(contents)}]]>
|
||||||
|
</description>
|
||||||
|
<pubDate>#{format_rfc822(date)}</pubDate>
|
||||||
|
<guid>https://madr.se/#{slug}</guid>
|
||||||
|
<georss:where>
|
||||||
|
<gml:Point>
|
||||||
|
<gml:pos>#{to_string(lng) <> " " <> to_string(lat)}</gml:pos>
|
||||||
|
</gml:Point>
|
||||||
|
</georss:where>
|
||||||
|
</item>
|
||||||
|
"""
|
||||||
|
end
|
||||||
|
|
||||||
|
defp rss_item(%{
|
||||||
|
:t => :links,
|
||||||
|
"title" => title,
|
||||||
|
"contents" => contents,
|
||||||
|
"slug" => slug,
|
||||||
|
"pubDate" => date,
|
||||||
|
"source" => link
|
||||||
|
}) do
|
||||||
|
~s"""
|
||||||
|
<item>
|
||||||
|
<title>#{title}</title>
|
||||||
|
<link>#{link}</link>
|
||||||
|
<description>
|
||||||
|
<![CDATA[#{Earmark.as_html!(contents)}]]>
|
||||||
|
</description>
|
||||||
|
<pubDate>#{format_rfc822(date)}</pubDate>
|
||||||
|
<guid>https://madr.se/#{slug}</guid>
|
||||||
|
</item>
|
||||||
|
"""
|
||||||
|
end
|
||||||
|
|
||||||
|
defp rss_item(%{
|
||||||
|
:t => :albums,
|
||||||
|
"summary" => summary,
|
||||||
|
"contents" => contents,
|
||||||
|
"purchased_at" => date,
|
||||||
|
"externalId" => id,
|
||||||
|
"purchase_year" => year
|
||||||
|
}) do
|
||||||
|
~s"""
|
||||||
|
<item>
|
||||||
|
<title>#{summary}</title>
|
||||||
|
<link>https://madr.se/#{year}/#{id}</link>
|
||||||
|
<description>
|
||||||
|
<![CDATA[#{Earmark.as_html!(contents)}]]>
|
||||||
|
</description>
|
||||||
|
<pubDate>#{format_rfc822(date)}</pubDate>
|
||||||
|
<guid>https://madr.se/#{year}/#{id}</guid>
|
||||||
|
</item>
|
||||||
|
"""
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -34,13 +34,24 @@ defmodule Mse25Web.ItemController do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp fetch([_year, album_id], :album) do
|
||||||
|
case Directus.get_album(album_id) do
|
||||||
|
{:ok, response} -> {:ok, :album, response}
|
||||||
|
not_found -> not_found
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp fetch([year, "brutal-legend-" <> external_id]) do
|
||||||
|
fetch([year, external_id], :album)
|
||||||
|
end
|
||||||
|
|
||||||
defp fetch([year, slug]) do
|
defp fetch([year, slug]) do
|
||||||
fetch([year, slug], :article)
|
fetch([year, slug], :article)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp fetch([slug]) do
|
defp fetch([slug]) do
|
||||||
case Integer.parse(slug) do
|
case Integer.parse(slug) do
|
||||||
{:error} ->
|
:error ->
|
||||||
case Directus.get_page(slug) do
|
case Directus.get_page(slug) do
|
||||||
{:ok, response} -> {:ok, :page, response}
|
{:ok, response} -> {:ok, :page, response}
|
||||||
error -> error
|
error -> error
|
||||||
|
|
@ -62,6 +73,7 @@ defmodule Mse25Web.ItemController do
|
||||||
do: [
|
do: [
|
||||||
year: year,
|
year: year,
|
||||||
page_title: "Innehåll från " <> to_string(year),
|
page_title: "Innehåll från " <> to_string(year),
|
||||||
|
breadcrumbs: [{year, year}],
|
||||||
timeline: timeline,
|
timeline: timeline,
|
||||||
brutal_legends_count: Map.get(counts, :albums, 0),
|
brutal_legends_count: Map.get(counts, :albums, 0),
|
||||||
article_count: Map.get(counts, :articles, 0),
|
article_count: Map.get(counts, :articles, 0),
|
||||||
|
|
@ -75,8 +87,11 @@ defmodule Mse25Web.ItemController do
|
||||||
"pubDate" => published_at,
|
"pubDate" => published_at,
|
||||||
"date_updated" => updated_at
|
"date_updated" => updated_at
|
||||||
}) do
|
}) do
|
||||||
|
year = String.slice(published_at, 0..3)
|
||||||
|
|
||||||
[
|
[
|
||||||
page_title: heading,
|
page_title: heading,
|
||||||
|
breadcrumbs: [{"webblogg", "Webblogg"}, {year, year, ""}],
|
||||||
heading: heading,
|
heading: heading,
|
||||||
contents: Earmark.as_html!(contents),
|
contents: Earmark.as_html!(contents),
|
||||||
published_at: published_at,
|
published_at: published_at,
|
||||||
|
|
@ -85,7 +100,7 @@ defmodule Mse25Web.ItemController do
|
||||||
nil -> published_at
|
nil -> published_at
|
||||||
ua -> String.slice(ua, 0..9)
|
ua -> String.slice(ua, 0..9)
|
||||||
end,
|
end,
|
||||||
year: String.slice(published_at, 0..3)
|
year: year
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -93,22 +108,28 @@ defmodule Mse25Web.ItemController do
|
||||||
"title" => heading,
|
"title" => heading,
|
||||||
"contents" => contents,
|
"contents" => contents,
|
||||||
"started_at" => started_at,
|
"started_at" => started_at,
|
||||||
|
"ended_at" => ended_at,
|
||||||
"lead" => lead,
|
"lead" => lead,
|
||||||
"poster" => poster,
|
"poster" => poster,
|
||||||
"bands" => bands,
|
"bands" => bands,
|
||||||
"mia" => mia,
|
"mia" => mia,
|
||||||
"category" => category
|
"category" => category
|
||||||
}) do
|
}) do
|
||||||
|
year = String.slice(started_at, 0..3)
|
||||||
|
|
||||||
[
|
[
|
||||||
page_title: heading,
|
page_title: heading,
|
||||||
|
breadcrumbs: [{"evenemang", "Evenemang"}, {year, year, ""}],
|
||||||
heading: heading,
|
heading: heading,
|
||||||
contents: Earmark.as_html!(contents),
|
contents: Earmark.as_html!(contents),
|
||||||
lead: lead,
|
lead: lead,
|
||||||
year: String.slice(started_at, 0..3),
|
year: year,
|
||||||
poster: poster,
|
poster: poster,
|
||||||
bands: bands,
|
bands: bands,
|
||||||
mia: mia,
|
mia: mia,
|
||||||
category: category
|
category: category,
|
||||||
|
started_at: started_at,
|
||||||
|
ended_at: ended_at
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -120,14 +141,17 @@ defmodule Mse25Web.ItemController do
|
||||||
"source" => url,
|
"source" => url,
|
||||||
"h1" => title
|
"h1" => title
|
||||||
}) do
|
}) do
|
||||||
|
year = String.slice(published_at, 0..3)
|
||||||
|
|
||||||
[
|
[
|
||||||
page_title: heading,
|
page_title: heading,
|
||||||
|
breadcrumbs: [{"delningar", "Delningar"}, {year, year, ""}],
|
||||||
heading: heading,
|
heading: heading,
|
||||||
contents: Earmark.as_html!(contents),
|
contents: Earmark.as_html!(contents),
|
||||||
published_at: published_at,
|
published_at: published_at,
|
||||||
url: url,
|
url: url,
|
||||||
title: title,
|
title: title,
|
||||||
year: String.slice(published_at, 0..3),
|
year: year,
|
||||||
updated_at:
|
updated_at:
|
||||||
case updated_at do
|
case updated_at do
|
||||||
nil -> published_at
|
nil -> published_at
|
||||||
|
|
@ -142,9 +166,39 @@ defmodule Mse25Web.ItemController do
|
||||||
"date_updated" => updated_at
|
"date_updated" => updated_at
|
||||||
}) do
|
}) do
|
||||||
[
|
[
|
||||||
|
page_title: heading,
|
||||||
|
breadcrumbs: [],
|
||||||
heading: heading,
|
heading: heading,
|
||||||
contents: Earmark.as_html!(contents),
|
contents: Earmark.as_html!(contents),
|
||||||
updated_at: String.slice(updated_at, 0..9)
|
updated_at: String.slice(updated_at, 0..9)
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp assigns(:album, %{
|
||||||
|
"year" => year,
|
||||||
|
"album" => album,
|
||||||
|
"contents" => contents,
|
||||||
|
"cover" => cover,
|
||||||
|
"purchased_at" => purchased_at,
|
||||||
|
"externalId" => count,
|
||||||
|
"songs" => songs,
|
||||||
|
"summary" => summary,
|
||||||
|
"artist" => artist
|
||||||
|
}) do
|
||||||
|
purchase_year = String.slice(purchased_at, 0..3)
|
||||||
|
|
||||||
|
[
|
||||||
|
page_title: summary,
|
||||||
|
breadcrumbs: [{purchase_year, purchase_year}],
|
||||||
|
count: count,
|
||||||
|
album: album,
|
||||||
|
cover: cover,
|
||||||
|
year: to_string(year),
|
||||||
|
purchase_year: purchase_year,
|
||||||
|
contents: Earmark.as_html!(contents),
|
||||||
|
songs: Enum.map(songs, fn %{"title" => name} -> "\"" <> name <> "\"" end),
|
||||||
|
artist: artist,
|
||||||
|
summary: summary
|
||||||
|
]
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
31
lib/mse25_web/controllers/item_html/album.html.heex
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
<article class="album" vocab="https://schema.org/" typeof="MusicAlbum Review">
|
||||||
|
<h1>
|
||||||
|
<%= @summary %>
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Som jag tidigare skrivit om har jag ett roligt projekt pågående: att äga alla låtar i spelet Brütal Legend äldre än 1990 på vinyl, där det är möjligt.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="brutal-legend">
|
||||||
|
<p>
|
||||||
|
#<%= @count %>: <span property="byArtist"><%= @artist %></span>
|
||||||
|
- <%= csl(@songs) %>, från <span property="name"><%= @album %></span>
|
||||||
|
(<span property="copyrightYear"><%= @year %></span>)
|
||||||
|
</p>
|
||||||
|
<%= if @cover do %>
|
||||||
|
<img
|
||||||
|
property="image"
|
||||||
|
src={"https://n.madr.se/assets/" <> @cover <> "?key=cover"}
|
||||||
|
alt="Skivomslag"
|
||||||
|
loading="lazy"
|
||||||
|
height="333"
|
||||||
|
width="333"
|
||||||
|
/>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div property="reviewBody">
|
||||||
|
<%= raw(@contents) %>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
|
@ -1,95 +1,99 @@
|
||||||
<header>
|
<h1><%= @page_title %></h1>
|
||||||
<ol class="breadcrumbs" itemscope itemtype="https://schema.org/BreadcrumbList">
|
<ul>
|
||||||
<li itemprop="itemListElement" itemscope itemtype="https://schema.org/ListItem">
|
<li><a href={"/" <> to_string(@year - 1)}>Tillbaka till <%= @year - 1 %></a></li>
|
||||||
<a href="/" rel="home">
|
<li><a href={"/" <> to_string(@year + 1)}>Framåt till <%= @year + 1 %></a></li>
|
||||||
<span itemprop="name">madr.se</span>
|
</ul>
|
||||||
</a>
|
<ul>
|
||||||
<meta itemprop="position" content="1" />
|
<%= if @link_count > 0 do %>
|
||||||
|
<li><%= @link_count %> länkar värda att uppmärksamma och kommentera</li>
|
||||||
|
<% end %>
|
||||||
|
<%= if @article_count > 0 do %>
|
||||||
|
<li><%= @article_count %> inlägg i webbloggen</li>
|
||||||
|
<% end %>
|
||||||
|
<%= if @event_count > 0 do %>
|
||||||
|
<li><%= @event_count %> besökta evenemang</li>
|
||||||
|
<% end %>
|
||||||
|
<%= if @brutal_legends_count > 0 do %>
|
||||||
|
<li>
|
||||||
|
<%= @brutal_legends_count %>
|
||||||
|
<%= if @brutal_legends_count == 1 do %>
|
||||||
|
köpt vinylskiva
|
||||||
|
<% else %>
|
||||||
|
köpta vinylskivor
|
||||||
|
<% end %>
|
||||||
|
till Brütal Legend-samlingen
|
||||||
</li>
|
</li>
|
||||||
</ol>
|
<% end %>
|
||||||
<h1><%= @page_title %></h1>
|
</ul>
|
||||||
<ul>
|
<%= for {month, items} <- @timeline do %>
|
||||||
<li><a href={"/" <> to_string(@year - 1)}>Tillbaka till <%= @year - 1 %></a></li>
|
<section id={"m" <> month}>
|
||||||
<li><a href={"/" <> to_string(@year + 1)}>Framåt till <%= @year + 1 %></a></li>
|
<h2><%= month_name(month) <> ", " <> to_string(@year) %></h2>
|
||||||
</ul>
|
<%= for item = %{t: t} <- items do %>
|
||||||
<ul>
|
<%= if t == :articles do %>
|
||||||
<%= if @link_count > 0 do %>
|
<article class="article" vocab="https://schema.org/" typeof="Article">
|
||||||
<li><%= @link_count %> länkar värda att uppmärksamma och kommentera</li>
|
<h3 property="name">
|
||||||
<% end %>
|
<a href={"/" <> item["slug"]}>
|
||||||
<%= if @article_count > 0 do %>
|
<%= item["title"] %>
|
||||||
<li><%= @article_count %> inlägg i webbloggen</li>
|
</a>
|
||||||
<% end %>
|
</h3>
|
||||||
<%= if @event_count > 0 do %>
|
<time property="datePublished"><%= item["pubDate"] %></time>
|
||||||
<li><%= @event_count %> besökta evenemang</li>
|
</article>
|
||||||
<% end %>
|
<% end %>
|
||||||
<%= if @brutal_legends_count > 0 do %>
|
<%= if t == :events do %>
|
||||||
<li>
|
<article class="event" vocab="https://schema.org/" typeof="Event">
|
||||||
<%= @brutal_legends_count %>
|
<h3>
|
||||||
<%= if @brutal_legends_count == 1 do %>
|
<a property="name" href={"/" <> item["slug"]}>
|
||||||
köpt vinylskiva
|
<%= item["title"] %>
|
||||||
<% else %>
|
</a>
|
||||||
köpta vinylskivor
|
</h3>
|
||||||
<% end %>
|
<p property="description"><%= item["lead"] %></p>
|
||||||
till Brütal Legend-samlingen
|
<%= if item["poster"] do %>
|
||||||
</li>
|
|
||||||
<% end %>
|
|
||||||
</ul>
|
|
||||||
<%= for {month, items} <- @timeline do %>
|
|
||||||
<section id={"m" <> month}>
|
|
||||||
<h2><%= month_name(month) <> ", " <> to_string(@year) %></h2>
|
|
||||||
<%= for item = %{t: t} <- items do %>
|
|
||||||
<article>
|
|
||||||
<%= if t == :articles do %>
|
|
||||||
<h3>
|
|
||||||
<a href={"/" <> item["slug"]}>
|
|
||||||
<%= item["title"] %>
|
|
||||||
</a>
|
|
||||||
</h3>
|
|
||||||
<% end %>
|
|
||||||
<%= if t == :events do %>
|
|
||||||
<h3>
|
|
||||||
<a href={"/" <> item["slug"]}>
|
|
||||||
<%= item["title"] %>
|
|
||||||
</a>
|
|
||||||
</h3>
|
|
||||||
<p><%= item["lead"] %></p>
|
|
||||||
<img
|
<img
|
||||||
|
property="thumbnail"
|
||||||
src={ "https://n.madr.se/assets/" <> item["poster"] <> "?key=poster"}
|
src={ "https://n.madr.se/assets/" <> item["poster"] <> "?key=poster"}
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
alt="Affisch"
|
alt="Affisch"
|
||||||
width="200"
|
width="200"
|
||||||
/>
|
/>
|
||||||
<% end %>
|
<% end %>
|
||||||
<%= if t == :links do %>
|
</article>
|
||||||
<h3>
|
<% end %>
|
||||||
<%= item["title"] %>
|
<%= if t == :links do %>
|
||||||
</h3>
|
<article vocab="https://schema.org/" typeof="WebContent Review" class="bookmark">
|
||||||
<p><%= raw(Earmark.as_html!(item["contents"])) %></p>
|
<h3>
|
||||||
Källa:
|
<span property="name"><%= item["title"] %></span>
|
||||||
<a href={"/" <> item["source"]}>
|
<a class="permalink" href={"/" <> item["slug"]} title="Permalänk">#</a>
|
||||||
<%= item["h1"] %>
|
</h3>
|
||||||
|
<div property="reviewBody">
|
||||||
|
<%= item["contents"] |> Earmark.as_html!() |> raw %>
|
||||||
|
</div>
|
||||||
|
<div class="source">
|
||||||
|
Källa: <a href={item["source"]} rel="external"><%= item["h1"] %></a>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
<% end %>
|
||||||
|
<%= if t == :albums do %>
|
||||||
|
<article class="album" vocab="https://schema.org/" typeof="MusicAlbum">
|
||||||
|
<h3>
|
||||||
|
<span property="byArtist"><%= item["artist"] %></span>
|
||||||
|
- <%= Enum.map(item["songs"], fn %{"title" => name} -> name end) |> csl() %>,
|
||||||
|
från <span property="name"><%= item["album"] %></span>
|
||||||
|
(<span property="copyrightYear"><%= item["year"] %></span>)
|
||||||
|
<a
|
||||||
|
class="permalink"
|
||||||
|
href={"/" <> to_string(item["year"]) <> "/brutal-legend-" <> item["externalId"]}
|
||||||
|
>
|
||||||
|
#
|
||||||
</a>
|
</a>
|
||||||
<% end %>
|
</h3>
|
||||||
<%= if t == :albums do %>
|
<ul>
|
||||||
<h3>
|
<%= for song <- item["songs"] do %>
|
||||||
<%= item["artist"] <>
|
<li><%= song["artist"]["name"] <> " - " <> song["title"] %></li>
|
||||||
" - " <> item["album"] <> " (" <> to_string(item["year"]) <> ")" %>
|
|
||||||
<a
|
|
||||||
class="permalink"
|
|
||||||
href={"/" <> to_string(@year) <> "/brutal-legend-" <> item["externalId"]}
|
|
||||||
>
|
|
||||||
#
|
|
||||||
</a>
|
|
||||||
</h3>
|
|
||||||
<%= if item["contents"] do %>
|
|
||||||
<p><%= raw(Earmark.as_html!(item["contents"])) %></p>
|
|
||||||
<% end %>
|
<% end %>
|
||||||
<ul>
|
</ul>
|
||||||
<%= for song <- item["songs"] do %>
|
<%= if item["cover"] do %>
|
||||||
<li><%= song["artist"]["name"] <> " - " <> song["title"] %></li>
|
|
||||||
<% end %>
|
|
||||||
</ul>
|
|
||||||
<img
|
<img
|
||||||
|
property="thumbnail"
|
||||||
src={"https://n.madr.se/assets/" <> item["cover"] <> "?key=rectangular"}
|
src={"https://n.madr.se/assets/" <> item["cover"] <> "?key=rectangular"}
|
||||||
alt="Skivomslag"
|
alt="Skivomslag"
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
|
|
@ -99,6 +103,6 @@
|
||||||
<% end %>
|
<% end %>
|
||||||
</article>
|
</article>
|
||||||
<% end %>
|
<% end %>
|
||||||
</section>
|
<% end %>
|
||||||
<% end %>
|
</section>
|
||||||
</header>
|
<% end %>
|
||||||
|
|
|
||||||
|
|
@ -1,33 +1,15 @@
|
||||||
<article class="article">
|
<article class="article" vocab="https://schema.org/" typeof="Article">
|
||||||
<header>
|
<h1 property="name"><%= @heading %></h1>
|
||||||
<ol class="breadcrumbs" itemscope itemtype="https://schema.org/BreadcrumbList">
|
|
||||||
<li itemprop="itemListElement" itemscope itemtype="https://schema.org/ListItem">
|
|
||||||
<a href="/" rel="home">
|
|
||||||
<span itemprop="name">madr.se</span>
|
|
||||||
</a>
|
|
||||||
<meta itemprop="position" content="1" />
|
|
||||||
</li>
|
|
||||||
<li itemprop="itemListElement" itemscope itemtype="https://schema.org/ListItem">
|
|
||||||
<a href="/webblogg">
|
|
||||||
<span itemprop="name">Webblogg</span>
|
|
||||||
</a>
|
|
||||||
<meta itemprop="position" content="2" />
|
|
||||||
</li>
|
|
||||||
<li itemprop="itemListElement" itemscope itemtype="https://schema.org/ListItem">
|
|
||||||
<a href={"/" <> @year}>
|
|
||||||
<span itemprop="name"><%= @year %></span>
|
|
||||||
</a>
|
|
||||||
<meta itemprop="position" content="3" />
|
|
||||||
</li>
|
|
||||||
</ol>
|
|
||||||
<h1><%= @heading %></h1>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<%= raw(@contents) %>
|
<div property="articleBody">
|
||||||
|
<%= raw(@contents) %>
|
||||||
|
</div>
|
||||||
|
|
||||||
<footer>
|
<footer>
|
||||||
<p>
|
<p>
|
||||||
Publicerad <%= @published_at %> <br />och senast uppdaterad <%= @updated_at %>
|
Publicerad <time property="datePublished"><%= @published_at %></time>
|
||||||
|
av <span property="publisher">Anders Englöf Ytterström</span>, <br /> senast uppdaterad
|
||||||
|
<time property="dateModified"><%= @updated_at %></time>
|
||||||
</p>
|
</p>
|
||||||
</footer>
|
</footer>
|
||||||
</article>
|
</article>
|
||||||
|
|
|
||||||
|
|
@ -1,45 +1,38 @@
|
||||||
<article class="article">
|
<article class="event" vocab="https://schema.org/" typeof="Event Review">
|
||||||
<header>
|
<h1 property="name"><%= @heading %></h1>
|
||||||
<ol class="breadcrumbs" itemscope itemtype="https://schema.org/BreadcrumbList">
|
|
||||||
<li itemprop="itemListElement" itemscope itemtype="https://schema.org/ListItem">
|
|
||||||
<a href="/" rel="home">
|
|
||||||
<span itemprop="name">madr.se</span>
|
|
||||||
</a>
|
|
||||||
<meta itemprop="position" content="1" />
|
|
||||||
</li>
|
|
||||||
<li itemprop="itemListElement" itemscope itemtype="https://schema.org/ListItem">
|
|
||||||
<a href="/evenemang">
|
|
||||||
<span itemprop="name">Evenemang</span>
|
|
||||||
</a>
|
|
||||||
<meta itemprop="position" content="2" />
|
|
||||||
</li>
|
|
||||||
<li itemprop="itemListElement" itemscope itemtype="https://schema.org/ListItem">
|
|
||||||
<a href={"/" <> @year}>
|
|
||||||
<span itemprop="name"><%= @year %></span>
|
|
||||||
</a>
|
|
||||||
<meta itemprop="position" content="3" />
|
|
||||||
</li>
|
|
||||||
</ol>
|
|
||||||
<h1><%= @heading %></h1>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<ul>
|
<ul>
|
||||||
<li><%= @lead %></li>
|
<li property="description"><%= @lead %></li>
|
||||||
|
<li class="sr-only">
|
||||||
|
<span property="startDate"><%= @started_at %></span>–<span property="endDate"><%= @ended_at %></span>
|
||||||
|
</li>
|
||||||
<%= if opening_acts?(%{"bands" => @bands, "category" => @category}) do %>
|
<%= if opening_acts?(%{"bands" => @bands, "category" => @category}) do %>
|
||||||
<li>Huvudakt: <%= @bands |> List.first() |> Map.get("artists_id") |> Map.get("name") %></li>
|
<li>
|
||||||
<li>Förband: <%= @bands |> Enum.drop(1) |> bandlist() %></li>
|
Huvudakt:
|
||||||
|
<span property="performer">
|
||||||
|
<%= @bands |> List.first() |> Map.get("artists_id") |> Map.get("name") %>
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
<li>Förband: <%= @bands |> Enum.drop(1) |> rdfa_bandlist() |> raw %></li>
|
||||||
<% end %>
|
<% end %>
|
||||||
<%= if hilights?(%{"bands" => @bands, "category" => @category}) do %>
|
<%= if hilights?(%{"bands" => @bands, "category" => @category}) do %>
|
||||||
<li>Personliga höjdpunkter: <%= @bands |> bandlist() %></li>
|
<li>Personliga höjdpunkter: <%= @bands |> rdfa_bandlist() |> raw %></li>
|
||||||
<% end %>
|
<% end %>
|
||||||
<%= if missed?(%{"mia" => @mia, "category" => @category}) do %>
|
<%= if missed?(%{"mia" => @mia, "category" => @category}) do %>
|
||||||
<li>Band jag missade: <%= @mia |> bandlist() %></li>
|
<li>Band jag missade: <%= @mia |> bandlist() %></li>
|
||||||
<% end %>
|
<% end %>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<%= raw(@contents) %>
|
<div property="reviewBody">
|
||||||
|
<%= raw(@contents) %>
|
||||||
|
</div>
|
||||||
|
|
||||||
<%= if @poster do %>
|
<%= if @poster do %>
|
||||||
<img src={"https://n.madr.se/assets/" <> @poster} alt="affisch" loading="lazy" />
|
<img
|
||||||
|
property="image"
|
||||||
|
src={"https://n.madr.se/assets/" <> @poster <> "?key=poster"}
|
||||||
|
alt="affisch"
|
||||||
|
loading="lazy"
|
||||||
|
/>
|
||||||
<% end %>
|
<% end %>
|
||||||
</article>
|
</article>
|
||||||
|
|
|
||||||
|
|
@ -1,35 +1,16 @@
|
||||||
<article class="article">
|
<article vocab="https://schema.org/" typeof="WebContent Review" class="bookmark">
|
||||||
<header>
|
<h1 property="name"><%= @heading %></h1>
|
||||||
<ol class="breadcrumbs" itemscope itemtype="https://schema.org/BreadcrumbList">
|
<div property="reviewBody">
|
||||||
<li itemprop="itemListElement" itemscope itemtype="https://schema.org/ListItem">
|
<%= raw(@contents) %>
|
||||||
<a href="/" rel="home">
|
</div>
|
||||||
<span itemprop="name">madr.se</span>
|
<div class="source">
|
||||||
</a>
|
Källa: <a href={@url} property="url"><span property="headline"><%= @title %></span></a>
|
||||||
<meta itemprop="position" content="1" />
|
</div>
|
||||||
</li>
|
|
||||||
<li itemprop="itemListElement" itemscope itemtype="https://schema.org/ListItem">
|
|
||||||
<a href="/delningar">
|
|
||||||
<span itemprop="name">Delningar</span>
|
|
||||||
</a>
|
|
||||||
<meta itemprop="position" content="2" />
|
|
||||||
</li>
|
|
||||||
<li itemprop="itemListElement" itemscope itemtype="https://schema.org/ListItem">
|
|
||||||
<a href={"/" <> @year}>
|
|
||||||
<span itemprop="name"><%= @year %></span>
|
|
||||||
</a>
|
|
||||||
<meta itemprop="position" content="3" />
|
|
||||||
</li>
|
|
||||||
</ol>
|
|
||||||
<h1><%= @heading %></h1>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<%= raw(@contents) %>
|
|
||||||
<p>
|
|
||||||
Källa: <a href={@url} rel="external"><%= @title %></a>
|
|
||||||
</p>
|
|
||||||
<footer>
|
<footer>
|
||||||
<p>
|
<p>
|
||||||
Publicerad <%= @published_at %> <br />och senast uppdaterad <%= @updated_at %>
|
Publicerad <time property="datePublished archivedAt"><%= @published_at %></time>
|
||||||
|
av <span property="author">Anders Englöf Ytterström</span>,<br />Senast uppdaterad
|
||||||
|
<time property="dateModified"><%= @updated_at %></time>
|
||||||
</p>
|
</p>
|
||||||
</footer>
|
</footer>
|
||||||
</article>
|
</article>
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,13 @@
|
||||||
<article>
|
<article class="article" vocab="https://schema.org/" typeof="Article">
|
||||||
<header>
|
<h1 property="name"><%= @heading %></h1>
|
||||||
<ol class="breadcrumbs" itemscope itemtype="https://schema.org/BreadcrumbList">
|
|
||||||
<li itemprop="itemListElement" itemscope itemtype="https://schema.org/ListItem">
|
|
||||||
<a href="/" rel="home">
|
|
||||||
<span itemprop="name">madr.se</span>
|
|
||||||
</a>
|
|
||||||
<meta itemprop="position" content="1" />
|
|
||||||
</li>
|
|
||||||
</ol>
|
|
||||||
<h1><%= @heading %></h1>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<%= raw(@contents) %>
|
<div property="articleBody">
|
||||||
|
<%= raw(@contents) %>
|
||||||
|
</div>
|
||||||
|
|
||||||
<footer>
|
<footer>
|
||||||
<p>Senast uppdaterad <%= @updated_at %></p>
|
<p>
|
||||||
|
Senast uppdaterad <time property="dateModified"><%= @updated_at %></time>
|
||||||
|
</p>
|
||||||
</footer>
|
</footer>
|
||||||
</article>
|
</article>
|
||||||
|
|
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
<article>
|
|
||||||
<header>
|
|
||||||
<date><%= @published_at %></date>
|
|
||||||
<h1><%= @heading %></h1>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<%= raw @contents %>
|
|
||||||
|
|
||||||
<footer>
|
|
||||||
<p>Skribent: Anders Englöf Ytterström. Publicerad <%= @published_at %> och senast uppdaterad <%= @updated_at %>.</p>
|
|
||||||
</footer>
|
|
||||||
</article>
|
|
||||||
|
|
@ -2,11 +2,14 @@ defmodule Mse25Web.PageController do
|
||||||
use Mse25Web, :controller
|
use Mse25Web, :controller
|
||||||
|
|
||||||
alias Mse25.Directus
|
alias Mse25.Directus
|
||||||
|
alias Mse25.Timeline
|
||||||
|
|
||||||
|
@almost_infinity 9999
|
||||||
|
|
||||||
def home(conn, _params) do
|
def home(conn, _params) do
|
||||||
[most_recent_article, older_article] = Directus.get_articles!(limit: 2)
|
[most_recent_article, older_article] = Directus.get_articles!(limit: 2)
|
||||||
recent_event = Directus.get_events!(limit: 1)
|
recent_event = Directus.get_events!(limit: 1)
|
||||||
upcoming_events = Directus.get_events!(limit: 1, upcoming: true)
|
upcoming_events = Directus.get_events!(limit: 2, upcoming: true)
|
||||||
brutal_legends = Directus.get_albums!(limit: 1)
|
brutal_legends = Directus.get_albums!(limit: 1)
|
||||||
|
|
||||||
render(conn, :home,
|
render(conn, :home,
|
||||||
|
|
@ -20,17 +23,46 @@ defmodule Mse25Web.PageController do
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
def articles(conn, params) do
|
def search(conn, %{"q" => ""}) do
|
||||||
articles =
|
redirect(conn, to: ~p"/")
|
||||||
case params do
|
end
|
||||||
%{"q" => query_string} -> Directus.get_articles!(limit: 9999, query: query_string)
|
|
||||||
_ -> Directus.get_articles!(limit: 9999)
|
def search(conn, %{"q" => query}) do
|
||||||
|
{:ok, %{results: results, count: count}} = Timeline.search(query)
|
||||||
|
|
||||||
|
scount =
|
||||||
|
case count do
|
||||||
|
0 -> "Inga"
|
||||||
|
c -> to_string(c)
|
||||||
|
end
|
||||||
|
|
||||||
|
render(conn, :search,
|
||||||
|
q: query,
|
||||||
|
breadcrumbs: [],
|
||||||
|
page_title: scount <> " sökresultat för \"" <> query <> "\"",
|
||||||
|
results: results
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def search(conn, _params) do
|
||||||
|
redirect(conn, to: ~p"/")
|
||||||
|
end
|
||||||
|
|
||||||
|
def articles(conn, params) do
|
||||||
|
{articles, page_title} =
|
||||||
|
case params do
|
||||||
|
%{"q" => query_string} ->
|
||||||
|
{Directus.get_articles!(limit: @almost_infinity, query: query_string),
|
||||||
|
"Webblogg: \"#{query_string}\""}
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
{Directus.get_articles!(limit: @almost_infinity), "Webblogg"}
|
||||||
end
|
end
|
||||||
|> group_annually
|
|
||||||
|
|
||||||
render(conn, :articles,
|
render(conn, :articles,
|
||||||
page_title: "Webblogg",
|
page_title: page_title,
|
||||||
articles: articles,
|
breadcrumbs: [],
|
||||||
|
articles: group_annually(articles),
|
||||||
q: params["q"],
|
q: params["q"],
|
||||||
nosearch?: params["q"] == nil or params["q"] == ""
|
nosearch?: params["q"] == nil or params["q"] == ""
|
||||||
)
|
)
|
||||||
|
|
@ -41,13 +73,18 @@ defmodule Mse25Web.PageController do
|
||||||
|
|
||||||
events =
|
events =
|
||||||
case params do
|
case params do
|
||||||
%{"q" => query_string} -> Directus.get_events!(limit: 9999, query: query_string)
|
%{"q" => query_string} ->
|
||||||
_ -> Directus.get_events!(limit: 9999)
|
Directus.get_events!(limit: @almost_infinity, query: query_string)
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
Directus.get_events!(limit: @almost_infinity)
|
||||||
end
|
end
|
||||||
|> group_annually
|
|> group_annually
|
||||||
|
|
||||||
render(conn, :events,
|
render(conn, :events,
|
||||||
page_title: title,
|
page_title: title,
|
||||||
|
breadcrumbs: [],
|
||||||
|
show_interactive_event_map?: true,
|
||||||
contents: Earmark.as_html!(contents),
|
contents: Earmark.as_html!(contents),
|
||||||
events: events,
|
events: events,
|
||||||
q: params["q"],
|
q: params["q"],
|
||||||
|
|
@ -56,10 +93,11 @@ defmodule Mse25Web.PageController do
|
||||||
end
|
end
|
||||||
|
|
||||||
def links(conn, _params) do
|
def links(conn, _params) do
|
||||||
links = Directus.get_links!(limit: 9999) |> group_by_date
|
links = Directus.get_links!(limit: @almost_infinity) |> group_by_date
|
||||||
|
|
||||||
render(conn, :links,
|
render(conn, :links,
|
||||||
page_title: "Delningar",
|
page_title: "Delningar",
|
||||||
|
breadcrumbs: [],
|
||||||
links: links
|
links: links
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,4 @@
|
||||||
defmodule Mse25Web.PageHTML do
|
defmodule Mse25Web.PageHTML do
|
||||||
@moduledoc """
|
|
||||||
This module contains pages rendered by PageController.
|
|
||||||
|
|
||||||
See the `page_html` directory for all templates available.
|
|
||||||
"""
|
|
||||||
use Mse25Web, :html
|
use Mse25Web, :html
|
||||||
import Mse25.EventHelpers
|
import Mse25.EventHelpers
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,49 +1,41 @@
|
||||||
<header>
|
<h1>
|
||||||
<ol class="breadcrumbs" itemscope itemtype="https://schema.org/BreadcrumbList">
|
<%= @page_title %>
|
||||||
<li itemprop="itemListElement" itemscope itemtype="https://schema.org/ListItem">
|
</h1>
|
||||||
<a href="/" rel="home">
|
<p>
|
||||||
<span itemprop="name">madr.se</span>
|
Inlägg skrivna sedan 2006.
|
||||||
</a>
|
<%= if @nosearch? do %>
|
||||||
<meta itemprop="position" content="1" />
|
Gå direkt till:
|
||||||
</li>
|
|
||||||
</ol>
|
|
||||||
<h1>Webblogg</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>
|
|
||||||
<%= for {year, articles} <- @articles do %>
|
|
||||||
<section id={"y" <> year}>
|
|
||||||
<h2 class="sticky"><%= year %></h2>
|
|
||||||
<div class="articles">
|
|
||||||
<%= for article <- articles do %>
|
|
||||||
<article>
|
|
||||||
<h2>
|
|
||||||
<a href={"/" <> article["slug"]}><%= article["title"] %></a>
|
|
||||||
</h2>
|
|
||||||
<date><%= article["pubDate"] %></date>
|
|
||||||
</article>
|
|
||||||
<% end %>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
<% end %>
|
<% end %>
|
||||||
</header>
|
</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 %>
|
||||||
|
|
|
||||||
|
|
@ -1,72 +1,70 @@
|
||||||
<header>
|
<h1><%= @page_title %></h1>
|
||||||
<ol class="breadcrumbs" itemscope itemtype="https://schema.org/BreadcrumbList">
|
|
||||||
<li itemprop="itemListElement" itemscope itemtype="https://schema.org/ListItem">
|
<%= raw(@contents) %>
|
||||||
<a href="/" rel="home">
|
<section id="map">
|
||||||
<span itemprop="name">madr.se</span>
|
<h2>Geografisk utspridning</h2>
|
||||||
</a>
|
<figure>
|
||||||
<meta itemprop="position" content="1" />
|
<div id="leaflet" class="interactive-map"></div>
|
||||||
|
</figure>
|
||||||
|
</section>
|
||||||
|
<p>
|
||||||
|
<%= if @nosearch? do %>
|
||||||
|
Gå direkt till:
|
||||||
|
<% end %>
|
||||||
|
</p>
|
||||||
|
<ul class="months">
|
||||||
|
<%= for {year, events} <- @events do %>
|
||||||
|
<li>
|
||||||
|
<a href={"#y" <> year}><%= year %></a> (<%= Enum.count(events) %>)
|
||||||
</li>
|
</li>
|
||||||
</ol>
|
<% end %>
|
||||||
<h1><%= @page_title %></h1>
|
</ul>
|
||||||
<%= raw(@contents) %>
|
<form method="get" action="/evenemang">
|
||||||
<p>
|
<p>
|
||||||
<%= if @nosearch? do %>
|
<%= if @nosearch? do %>
|
||||||
Gå direkt till:
|
Eller
|
||||||
<% end %>
|
<% 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>
|
</p>
|
||||||
<ul class="months">
|
</form>
|
||||||
<%= for {year, events} <- @events do %>
|
<%= for {year, events} <- @events do %>
|
||||||
<li>
|
<section id={"y" <> year}>
|
||||||
<a href={"#y" <> year}><%= year %></a> (<%= Enum.count(events) %>)
|
<h2 class="sticky"><%= year %></h2>
|
||||||
</li>
|
<div class="events">
|
||||||
<% end %>
|
<%= for event <- events do %>
|
||||||
</ul>
|
<article class="event" vocab="https://schema.org/" typeof="Event">
|
||||||
<form method="get" action="/evenemang">
|
<h2>
|
||||||
<p>
|
<a property="name" href={"/" <> event["slug"]}><%= event["title"] %></a>
|
||||||
<%= if @nosearch? do %>
|
</h2>
|
||||||
Eller
|
<p property="description"><%= event["lead"] %></p>
|
||||||
|
<%= if hilights?(event) do %>
|
||||||
|
<p>
|
||||||
|
Personliga höjdpunkter: <%= rdfa_bandlist(event["bands"]) |> raw %>
|
||||||
|
</p>
|
||||||
|
<% end %>
|
||||||
|
<%= if missed?(event) do %>
|
||||||
|
<p>
|
||||||
|
Band jag missade: <%= bandlist(event["mia"]) %>
|
||||||
|
</p>
|
||||||
|
<% end %>
|
||||||
|
<%= if opening_acts?(event) do %>
|
||||||
|
<p>
|
||||||
|
Förband: <%= event["bands"] |> Enum.drop(1) |> rdfa_bandlist() |> raw %>
|
||||||
|
</p>
|
||||||
|
<% end %>
|
||||||
|
<%= if event["poster"] do %>
|
||||||
|
<img
|
||||||
|
property="thumbnail"
|
||||||
|
loading="lazy"
|
||||||
|
src={"https://n.madr.se/assets/" <> event["poster"] <> "?key=poster"}
|
||||||
|
alt="affisch"
|
||||||
|
width="150"
|
||||||
|
/>
|
||||||
|
<% end %>
|
||||||
|
</article>
|
||||||
<% end %>
|
<% end %>
|
||||||
<label for="q">sök innehåll</label>:
|
</div>
|
||||||
<input type="search" value={@q} name="q" id="q" size="7" />
|
</section>
|
||||||
<button>Sök</button>
|
<% end %>
|
||||||
</p>
|
|
||||||
</form>
|
|
||||||
<%= for {year, events} <- @events do %>
|
|
||||||
<section id={"y" <> year}>
|
|
||||||
<h2 class="sticky"><%= year %></h2>
|
|
||||||
<div class="events">
|
|
||||||
<%= for event <- events do %>
|
|
||||||
<article>
|
|
||||||
<h2>
|
|
||||||
<a href={"/" <> event["slug"]}><%= event["title"] %></a>
|
|
||||||
</h2>
|
|
||||||
<p><%= event["lead"] %></p>
|
|
||||||
<%= if hilights?(event) do %>
|
|
||||||
<p>
|
|
||||||
Personliga höjdpunkter: <%= bandlist(event["bands"]) %>
|
|
||||||
</p>
|
|
||||||
<% end %>
|
|
||||||
<%= if missed?(event) do %>
|
|
||||||
<p>
|
|
||||||
Band jag missade: <%= bandlist(event["mia"]) %>
|
|
||||||
</p>
|
|
||||||
<% end %>
|
|
||||||
<%= if opening_acts?(event) do %>
|
|
||||||
<p>
|
|
||||||
Förband: <%= event["bands"] |> Enum.drop(1) |> bandlist() %>
|
|
||||||
</p>
|
|
||||||
<% end %>
|
|
||||||
<%= if event["poster"] do %>
|
|
||||||
<img
|
|
||||||
loading="lazy"
|
|
||||||
src={"https://n.madr.se/assets/" <> event["poster"]}
|
|
||||||
alt="affisch"
|
|
||||||
width="150"
|
|
||||||
/>
|
|
||||||
<% end %>
|
|
||||||
</article>
|
|
||||||
<% end %>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
<% end %>
|
|
||||||
</header>
|
|
||||||
|
|
|
||||||
|
|
@ -1,67 +1,134 @@
|
||||||
<main class="landing">
|
<main class="landing">
|
||||||
<img src={~p"/images/aey.svg"} width="300" alt="Anders Englöf Ytterström" />
|
<img src={~p"/images/aey.svg"} width="120" alt="Anders Englöf Ytterström" />
|
||||||
<form metod="get" action="/search">
|
<h1 class="home-h1">Anders Englöf Ytterström</h1>
|
||||||
<label for="q">Sök innehåll</label>: <input size="9" type="search" id="q" name="q" />
|
<ul class="tree">
|
||||||
<button>Sök</button>
|
<li class="article">
|
||||||
</form>
|
<span></span>
|
||||||
<div class="tree">
|
|
||||||
<div>
|
|
||||||
Senast skrivet (<date><%= @recent_article["pubDate"] %></date>):<br />
|
|
||||||
<a href={"/" <> @recent_article["slug"]}>
|
<a href={"/" <> @recent_article["slug"]}>
|
||||||
<%= @recent_article["title"] %>
|
<%= @recent_article["title"] %>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
<small><time><%= @recent_article["pubDate"] %></time></small>
|
||||||
<div>
|
</li>
|
||||||
Dessförinnan (<date><%= @older_article["pubDate"] %></date>):<br />
|
<li class="article">
|
||||||
|
<span></span>
|
||||||
<a href={"/" <> @older_article["slug"]}>
|
<a href={"/" <> @older_article["slug"]}>
|
||||||
<%= @older_article["title"] %>
|
<%= @older_article["title"] %>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
<small><time><%= @older_article["pubDate"] %></time></small>
|
||||||
<div>
|
</li>
|
||||||
<a href="/webblogg">Webbloggen</a>
|
<li class="page">
|
||||||
</div>
|
<span></span>
|
||||||
<%= for event <- @upcoming do %>
|
<a href="/webblogg" class="list-link">Alla Webbloggens inlägg</a>
|
||||||
<div>
|
</li>
|
||||||
Kommande: <a href={event["slug"]}><%= event["title"] %><br /><%= event["lead"] %></a>
|
|
||||||
</div>
|
|
||||||
<% end %>
|
|
||||||
<%= for event <- @recent_event do %>
|
<%= for event <- @recent_event do %>
|
||||||
<div>
|
<li class="events">
|
||||||
Upplevt: <a href={event["slug"]}><%= event["title"] %><br /><%= event["lead"] %></a>
|
<%= if event["poster"] do %>
|
||||||
</div>
|
<img
|
||||||
|
src={"https://n.madr.se/assets/" <> event["poster"] <> "?key=thumbnail"}
|
||||||
|
alt="affisch"
|
||||||
|
loading="lazy"
|
||||||
|
/>
|
||||||
|
<% else %>
|
||||||
|
<span></span>
|
||||||
|
<% end %>
|
||||||
|
<a href={event["slug"]} title={event["lead"]}>
|
||||||
|
<%= event["title"] %>
|
||||||
|
</a>
|
||||||
|
<small><time><%= event["started_at"] %></time></small>
|
||||||
|
</li>
|
||||||
<% end %>
|
<% end %>
|
||||||
<div>
|
<%= for event <- @upcoming do %>
|
||||||
<a href="/evenemang">Evenemangstidslinje</a>
|
<li class="events">
|
||||||
</div>
|
<%= if event["poster"] do %>
|
||||||
<div>
|
<img
|
||||||
Värt att uppmärksamma:
|
src={"https://n.madr.se/assets/" <> event["poster"] <> "?key=thumbnail"}
|
||||||
<a href="/delningar">
|
alt="affisch"
|
||||||
|
loading="lazy"
|
||||||
|
/>
|
||||||
|
<% else %>
|
||||||
|
<span></span>
|
||||||
|
<% end %>
|
||||||
|
<%= event["title"] %>
|
||||||
|
<small><time><%= event["started_at"] %></time></small>
|
||||||
|
</li>
|
||||||
|
<% end %>
|
||||||
|
<li class="feed events page">
|
||||||
|
<span></span>
|
||||||
|
<a href="/evenemang" class="list-link">Evenemangstidslinje</a>
|
||||||
|
<span></span>
|
||||||
|
</li>
|
||||||
|
<li class="feed events ics">
|
||||||
|
<span></span>
|
||||||
|
<a href="/kommande-evenemang.ics" class="feed-link">Kommande evenemang</a>
|
||||||
|
<small>.ics</small>
|
||||||
|
</li>
|
||||||
|
<li class="feed links">
|
||||||
|
<span></span>
|
||||||
|
<a href="/delningar" class="list-link">
|
||||||
Delningar
|
Delningar
|
||||||
</a>
|
</a>
|
||||||
</div>
|
<span></span>
|
||||||
|
</li>
|
||||||
<%= for legend <- @brutal_legends do %>
|
<%= for legend <- @brutal_legends do %>
|
||||||
<div>
|
<li class="album">
|
||||||
Införskaffat (<%= legend["purchased_at"] %>):<br />
|
<%= if legend["cover"] do %>
|
||||||
<a href={legend["externalId"]}>
|
<img
|
||||||
|
src={"https://n.madr.se/assets/" <> legend["cover"] <> "?key=thumbnail"}
|
||||||
|
alt="skivomslag"
|
||||||
|
loading="lazy"
|
||||||
|
/>
|
||||||
|
<% else %>
|
||||||
|
<span></span>
|
||||||
|
<% end %>
|
||||||
|
<a href={"/" <> legend["purchase_year"] <> "/brutal-legend-" <> legend["externalId"]}>
|
||||||
<%= legend["artist"] %> - <%= legend["album"] %> (<%= legend["year"] %>)
|
<%= legend["artist"] %> - <%= legend["album"] %> (<%= legend["year"] %>)
|
||||||
</a>
|
</a>
|
||||||
</div>
|
<span></span>
|
||||||
|
</li>
|
||||||
<% end %>
|
<% end %>
|
||||||
<div>
|
<li class="page">
|
||||||
|
<span></span>
|
||||||
<a href="/vad-jag-gor">
|
<a href="/vad-jag-gor">
|
||||||
Vad jag gör
|
Vad jag gör
|
||||||
</a>
|
</a>
|
||||||
</div>
|
<span></span>
|
||||||
<div>
|
</li>
|
||||||
Mer om:
|
<li class="page">
|
||||||
|
<span></span>
|
||||||
<a href="/om">
|
<a href="/om">
|
||||||
Anders, 39, Hårdrockare
|
Anders, 39, Hårdrockare
|
||||||
</a>
|
</a>
|
||||||
</div>
|
<span></span>
|
||||||
<div>
|
</li>
|
||||||
<a href="/colophon">
|
<li class="feed rss">
|
||||||
Kontakt & Kolofon
|
<span></span>
|
||||||
|
<a href="/prenumerera.xml" class="feed-link">Prenumerera</a>
|
||||||
|
<small>.rss</small>
|
||||||
|
</li>
|
||||||
|
<li class="">
|
||||||
|
<span></span>
|
||||||
|
<a clasS="feed-link" href={~p"/aey.vcf"}>
|
||||||
|
Kontakt
|
||||||
</a>
|
</a>
|
||||||
</div>
|
<small>.vcf</small>
|
||||||
</div>
|
</li>
|
||||||
|
</ul>
|
||||||
|
<form method="get" action="/sok" class="home-search">
|
||||||
|
<label for="q">Sök innehåll</label>: <input size="9" type="search" id="q" name="q" />
|
||||||
|
<button>Sök</button>
|
||||||
|
</form>
|
||||||
|
<ul class="profiles">
|
||||||
|
<li><a href="/cv/anders-englof-ytterstrom.html">CV</a></li>
|
||||||
|
<li><a href="https://github.com/madr">Github</a></li>
|
||||||
|
<li>
|
||||||
|
<a href="https://www.discogs.com/user/madrse/collection?limit=250&sort=artist&sort_order=asc&layout=big">
|
||||||
|
Discogs
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="/colophon">
|
||||||
|
Kolofon
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
</main>
|
</main>
|
||||||
|
|
|
||||||
|
|
@ -1,73 +1,65 @@
|
||||||
<header>
|
<h1>Delningar</h1>
|
||||||
<ol class="breadcrumbs" itemscope itemtype="https://schema.org/BreadcrumbList">
|
<p>
|
||||||
<li itemprop="itemListElement" itemscope itemtype="https://schema.org/ListItem">
|
Länkar som är värda att uppmärksammas och lämna åsikt om.
|
||||||
<a href="/" rel="home">
|
</p>
|
||||||
<span itemprop="name">madr.se</span>
|
<%= for {date, links} <- @links do %>
|
||||||
</a>
|
<section id={"d" <> date}>
|
||||||
<meta itemprop="position" content="1" />
|
<div class="links">
|
||||||
</li>
|
<h2>
|
||||||
</ol>
|
<%= date
|
||||||
<h1>Delningar</h1>
|
|> Date.from_iso8601!()
|
||||||
<p>
|
|> Calendar.strftime(
|
||||||
Länkar som är värda att uppmärksammas och lämna åsikt om.
|
"%A, %d %B %Y",
|
||||||
</p>
|
month_names: fn m ->
|
||||||
<%= for {date, links} <- @links do %>
|
Enum.at(
|
||||||
<section id={"d" <> date}>
|
[
|
||||||
<div class="links">
|
"januari",
|
||||||
<h2>
|
"februari",
|
||||||
<%= date
|
"mars",
|
||||||
|> Date.from_iso8601!()
|
"april",
|
||||||
|> Calendar.strftime(
|
"maj",
|
||||||
"%A, %d %B %Y",
|
"juni",
|
||||||
month_names: fn m ->
|
"juli",
|
||||||
Enum.at(
|
"augusti",
|
||||||
[
|
"september",
|
||||||
"januari",
|
"oktober",
|
||||||
"februari",
|
"november",
|
||||||
"mars",
|
"december"
|
||||||
"april",
|
],
|
||||||
"maj",
|
m - 1
|
||||||
"juni",
|
)
|
||||||
"juli",
|
end,
|
||||||
"augusti",
|
day_of_week_names: fn d ->
|
||||||
"september",
|
Enum.at(
|
||||||
"oktober",
|
[
|
||||||
"november",
|
"måndag",
|
||||||
"december"
|
"tisdag",
|
||||||
],
|
"onsdag",
|
||||||
m - 1
|
"torsdag",
|
||||||
)
|
"fredag",
|
||||||
end,
|
"lördag",
|
||||||
day_of_week_names: fn d ->
|
"söndag"
|
||||||
Enum.at(
|
],
|
||||||
[
|
d - 1
|
||||||
"måndag",
|
)
|
||||||
"tisdag",
|
end
|
||||||
"onsdag",
|
)
|
||||||
"torsdag",
|
|> String.replace(~r/ 0/, " ") %>
|
||||||
"fredag",
|
</h2>
|
||||||
"lördag",
|
<%= for link <- links do %>
|
||||||
"söndag"
|
<article vocab="https://schema.org/" typeof="WebContent Review" class="bookmark">
|
||||||
],
|
<h3>
|
||||||
d - 1
|
<span property="name"><%= link["title"] %></span>
|
||||||
)
|
<a class="permalink" href={"/" <> link["slug"]} title="Permalänk">#</a>
|
||||||
end
|
</h3>
|
||||||
)
|
<div property="reviewBody">
|
||||||
|> String.replace(~r/ 0/, " ") %>
|
|
||||||
</h2>
|
|
||||||
<%= for link <- links do %>
|
|
||||||
<article>
|
|
||||||
<h3>
|
|
||||||
<%= link["title"] %>
|
|
||||||
<a class="permalink" href={"/" <> link["slug"]} title="Permalänk">#</a>
|
|
||||||
</h3>
|
|
||||||
<%= link["contents"] |> Earmark.as_html!() |> raw %>
|
<%= link["contents"] |> Earmark.as_html!() |> raw %>
|
||||||
<div class="source">
|
</div>
|
||||||
Källa: <a href={link["source"]} rel="external"><%= link["h1"] %></a>
|
<div class="source">
|
||||||
</div>
|
Källa: <a href={link["source"]} rel="external"><%= link["h1"] %></a>
|
||||||
</article>
|
</div>
|
||||||
<% end %>
|
</article>
|
||||||
</div>
|
<% end %>
|
||||||
</section>
|
</div>
|
||||||
<% end %>
|
</section>
|
||||||
</header>
|
<% end %>
|
||||||
|
|
|
||||||
71
lib/mse25_web/controllers/page_html/search.html.heex
Normal file
|
|
@ -0,0 +1,71 @@
|
||||||
|
<form metod="get" action="/sok">
|
||||||
|
<label for="q">Sök innehåll</label>: <input size="9" type="search" id="q" name="q" value={@q} />
|
||||||
|
<button>Sök</button>
|
||||||
|
</form>
|
||||||
|
<h1><%= @page_title %></h1>
|
||||||
|
<%= for item = %{t: t} <- @results do %>
|
||||||
|
<article>
|
||||||
|
<%= if t == :articles do %>
|
||||||
|
<h3>
|
||||||
|
<a href={"/" <> item["slug"]}>
|
||||||
|
<%= item["title"] %>
|
||||||
|
</a>
|
||||||
|
</h3>
|
||||||
|
<% end %>
|
||||||
|
<%= if t == :events do %>
|
||||||
|
<h3>
|
||||||
|
<a href={"/" <> item["slug"]}>
|
||||||
|
<%= item["title"] %>
|
||||||
|
</a>
|
||||||
|
</h3>
|
||||||
|
<p><%= item["lead"] %></p>
|
||||||
|
<%= if item["poster"] do %>
|
||||||
|
<img
|
||||||
|
src={ "https://n.madr.se/assets/" <> item["poster"] <> "?key=poster"}
|
||||||
|
loading="lazy"
|
||||||
|
alt="Affisch"
|
||||||
|
width="200"
|
||||||
|
/>
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
|
<%= if t == :links do %>
|
||||||
|
<h3>
|
||||||
|
<%= item["title"] %>
|
||||||
|
</h3>
|
||||||
|
<p><%= raw(Earmark.as_html!(item["contents"])) %></p>
|
||||||
|
Källa:
|
||||||
|
<a href={"/" <> item["source"]}>
|
||||||
|
<%= item["h1"] %>
|
||||||
|
</a>
|
||||||
|
<% end %>
|
||||||
|
<%= if t == :albums do %>
|
||||||
|
<h3>
|
||||||
|
<%= item["artist"] <>
|
||||||
|
" - " <> item["album"] <> " (" <> to_string(item["year"]) <> ")" %>
|
||||||
|
<a
|
||||||
|
class="permalink"
|
||||||
|
href={"/" <> to_string(@year) <> "/brutal-legend-" <> item["externalId"]}
|
||||||
|
>
|
||||||
|
#
|
||||||
|
</a>
|
||||||
|
</h3>
|
||||||
|
<%= if item["contents"] do %>
|
||||||
|
<p><%= raw(Earmark.as_html!(item["contents"])) %></p>
|
||||||
|
<% end %>
|
||||||
|
<ul>
|
||||||
|
<%= for song <- item["songs"] do %>
|
||||||
|
<li><%= song["artist"]["name"] <> " - " <> song["title"] %></li>
|
||||||
|
<% end %>
|
||||||
|
</ul>
|
||||||
|
<%= if item["cover"] do %>
|
||||||
|
<img
|
||||||
|
src={"https://n.madr.se/assets/" <> item["cover"] <> "?key=rectangular"}
|
||||||
|
alt="Skivomslag"
|
||||||
|
loading="lazy"
|
||||||
|
height="75"
|
||||||
|
width="75"
|
||||||
|
/>
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
|
</article>
|
||||||
|
<% end %>
|
||||||
|
|
@ -4,28 +4,39 @@ defmodule Mse25Web.Router do
|
||||||
pipeline :browser do
|
pipeline :browser do
|
||||||
plug :accepts, ["html"]
|
plug :accepts, ["html"]
|
||||||
plug :fetch_session
|
plug :fetch_session
|
||||||
plug :fetch_live_flash
|
|
||||||
plug :put_root_layout, html: {Mse25Web.Layouts, :root}
|
plug :put_root_layout, html: {Mse25Web.Layouts, :root}
|
||||||
plug :protect_from_forgery
|
plug :protect_from_forgery
|
||||||
plug :put_secure_browser_headers
|
plug :put_secure_browser_headers
|
||||||
end
|
end
|
||||||
|
|
||||||
|
pipeline :scripts do
|
||||||
|
plug :accepts, ["js"]
|
||||||
|
plug :put_secure_browser_headers
|
||||||
|
end
|
||||||
|
|
||||||
pipeline :api do
|
pipeline :api do
|
||||||
plug :accepts, ["json"]
|
plug :accepts, ["json"]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
scope "/", Mse25Web do
|
||||||
|
pipe_through :scripts
|
||||||
|
|
||||||
|
get "/event-map.js", FeedController, :interactive_event_map
|
||||||
|
end
|
||||||
|
|
||||||
scope "/", Mse25Web do
|
scope "/", Mse25Web do
|
||||||
pipe_through :browser
|
pipe_through :browser
|
||||||
|
|
||||||
get "/", PageController, :home
|
get "/", PageController, :home
|
||||||
|
|
||||||
get "/evenemang", PageController, :events
|
get "/evenemang", PageController, :events
|
||||||
get "/webblogg", PageController, :articles
|
get "/webblogg", PageController, :articles
|
||||||
get "/delningar", PageController, :links
|
get "/delningar", PageController, :links
|
||||||
|
get "/sok", PageController, :search
|
||||||
|
|
||||||
# get "/kommande-evenemang.ics", EventController, :calendar
|
get "/prenumerera.xml", FeedController, :feed
|
||||||
# get "/event-map.js", EventController, :interactive_map
|
get "/albums.json", FeedController, :albums
|
||||||
# get "/prenumerera.xml", TimelineController, :feed
|
get "/events.json", FeedController, :events
|
||||||
|
get "/kommande-evenemang.ics", FeedController, :calendar
|
||||||
|
|
||||||
get "/*path", ItemController, :index
|
get "/*path", ItemController, :index
|
||||||
end
|
end
|
||||||
|
|
|
||||||
12
priv/static/aey.vcf
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
BEGIN:VCARD
|
||||||
|
VERSION:4.0
|
||||||
|
N:Englöf Ytterström;Anders;;Mr.;
|
||||||
|
FN:Anders Englöf Ytterström
|
||||||
|
ORG:PlaymakerAI
|
||||||
|
TITLE:Webbutvecklare/hårdrockare
|
||||||
|
PHOTO;MEDIATYPE=image/gif:https://madr.se/mugshot.jpg
|
||||||
|
TEL;TYPE=home,voice;VALUE=uri:tel:+46-70-216-9645
|
||||||
|
EMAIL:yttan@fastmail.se
|
||||||
|
REV:20191006T195243Z
|
||||||
|
x-qq:21588891
|
||||||
|
END:VCARD
|
||||||
BIN
priv/static/android-chrome-192x192.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
priv/static/android-chrome-512x512.png
Normal file
|
After Width: | Height: | Size: 69 KiB |
BIN
priv/static/apple-touch-icon.png
Normal file
|
After Width: | Height: | Size: 21 KiB |
1
priv/static/bl/brutal.f07f1821.css
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
body{background-color:#111;color:#aaa;font-size:large;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,sans-serif;padding:0;margin:0 auto;max-width:80em}a:link,a:visited{color:#a83}a:visited{text-decoration:line-through}a:focus,a:hover{color:#fff}a:active{transform:translate(2px,2px)}header{border-bottom:3px solid #a83;padding:.5em;margin-bottom:.5em;align-items:center}@media (min-width:500px){header{display:flex;justify-content:space-between;padding:.5em;margin:0}}@media (min-width:1200px){header{margin:.5em 0}}h1{text-transform:uppercase;margin:0;font-size:1.5em}p:first-child{margin-top:0}p:last-child{margin-bottom:0}.field{background-color:#333;color:#fff;border-width:0;border-radius:5px;font-size:large;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,sans-serif;padding:.5rem 1rem;display:block;width:100%;box-sizing:border-box;margin-top:.5em}.field:hover{background-color:#444}.field:focus{background-color:#fff;color:#000}@media (min-width:500px){.field{margin-top:0}input.field{min-width:17em}}.blur{filter:blur(25px)}.visuallyhidden{position:absolute;left:-9999em}.album{display:flex;align-items:start;padding:.5em;margin:.8em 0}@media (min-width:500px){.album{flex-direction:column;margin:0}}@media (min-width:1200px){.album{flex-direction:row;align-items:center}}.album:hover{background:#333;color:#fff}.album__cover{margin:0 .5em 0 0;padding-top:7px}@media (min-width:500px){.album__cover{margin:0 0 .5em;width:100%;padding-top:0}}@media (min-width:1200px){.album__cover{margin:0 1em 0 0;width:auto;padding-top:0}}.album__cover__media{width:25vw;height:25vw;position:relative}.album__cover__media:after{position:absolute;content:"";top:0;right:0;bottom:0;left:0;background-color:#fff;z-index:10}@media (min-width:500px){.album__cover__media{width:100%;height:100%}}@media (min-width:1200px){.album__cover__media{width:10vw;height:10vw}}@media (min-width:500px){.albums{display:grid;grid-template-columns:1fr 1fr 1fr;grid-gap:.5em;text-transform:uppercase;padding-bottom:2em}}.selected-album{position:fixed;top:0;right:0;bottom:0;left:0;display:flex;align-items:center;justify-content:center}.selected-album__inner{overflow:auto;border:3px solid #a83;background:#000;padding:2em;max-height:80%;display:flex;flex-direction:column;align-items:center}.selected-album__summary{text-transform:uppercase;padding:.5em;margin-bottom:.5em}.selected-album__description{color:#fff;padding:0 .5rem 2em;margin:0 auto;max-width:40em}.selected-album__cover{display:none}@media (min-width:1200px){.selected-album__cover{display:block;width:75vh;max-width:900px;height:auto}.selected-album__media{width:100%;height:auto}}
|
||||||
1
priv/static/bl/index.html
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
<!DOCTYPE html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width"><link rel="stylesheet" href="brutal.f07f1821.css"><title>🤘 Brütal Legend 🤘</title></head><body> <div id="brutal"></div> </body><script src="src.fc45d0fd.js"></script></html>
|
||||||
129
priv/static/bl/src.fc45d0fd.js
Normal file
1
priv/static/cv/anders-englof-ytterstrom.html
Normal file
BIN
priv/static/cv/cv-anders-englof-ytterstrom.pdf
Normal file
266
priv/static/cv/cv.css
Normal file
|
|
@ -0,0 +1,266 @@
|
||||||
|
html {
|
||||||
|
font:
|
||||||
|
normal small/1.5 apple-system,
|
||||||
|
system-ui,
|
||||||
|
BlinkMacSystemFont,
|
||||||
|
Segoe UI,
|
||||||
|
Roboto,
|
||||||
|
Helvetica Neue,
|
||||||
|
Arial,
|
||||||
|
sans-serif;
|
||||||
|
background-color: var(--bg-color, #fff);
|
||||||
|
color: var(--bg-color, #444);
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
font-size: clamp(0.9em, 1.5vw, 1.4em);
|
||||||
|
}
|
||||||
|
ul {
|
||||||
|
margin: 0;
|
||||||
|
padding-left: 1em;
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
margin-bottom: 0;
|
||||||
|
font-size: 3em;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
h2 {
|
||||||
|
border-bottom: 0.3em solid #e3e3e3;
|
||||||
|
padding-bottom: 0.25em;
|
||||||
|
margin-bottom: 1em;
|
||||||
|
}
|
||||||
|
img {
|
||||||
|
display: block;
|
||||||
|
border-radius: 50%;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
figure {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
a {
|
||||||
|
color: var(--bg-color, #444);
|
||||||
|
transition:
|
||||||
|
background-color 0.3s ease-out,
|
||||||
|
border-bottom-color 0.3s ease-out;
|
||||||
|
}
|
||||||
|
p:first-child {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
a:link,
|
||||||
|
a:visited {
|
||||||
|
display: inline-block;
|
||||||
|
text-decoration: none;
|
||||||
|
border-bottom: 1px solid #bbb;
|
||||||
|
}
|
||||||
|
a:hover,
|
||||||
|
a:focus {
|
||||||
|
background: rgba(0, 0, 0, 0.05);
|
||||||
|
border-bottom-color: #000;
|
||||||
|
color: #000;
|
||||||
|
}
|
||||||
|
a:active {
|
||||||
|
transform: translate(3px, 3px);
|
||||||
|
}
|
||||||
|
[href^="tel"]::before {
|
||||||
|
content: "📞 ";
|
||||||
|
}
|
||||||
|
[href^="mailto"]::before {
|
||||||
|
content: "✉️ ";
|
||||||
|
}
|
||||||
|
[href^="https://github.com"]::before
|
||||||
|
{
|
||||||
|
content: "🐙 ";
|
||||||
|
}
|
||||||
|
[href^="https://madr"]::before
|
||||||
|
{
|
||||||
|
content: "🏠 ";
|
||||||
|
}
|
||||||
|
[href$="pdf"] {
|
||||||
|
font-size: 1.1em;
|
||||||
|
padding: 0.3em;
|
||||||
|
}
|
||||||
|
[href$="pdf"]::before {
|
||||||
|
content: "📑 ";
|
||||||
|
}
|
||||||
|
[href$="pdf"]:hover::before,
|
||||||
|
[href$="pdf"]:focus::before {
|
||||||
|
background-color: rgba(0, 255, 0, 0.2);
|
||||||
|
}
|
||||||
|
[role="doc-subtitle"] {
|
||||||
|
font-size: 1.25em;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
@media (min-width: 40em) {
|
||||||
|
.h-aside {
|
||||||
|
border-bottom: 0;
|
||||||
|
font-size: 1.4em;
|
||||||
|
margin-bottom: 0;
|
||||||
|
margin-top: 1.95em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.contact {
|
||||||
|
margin: 1em 0;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 2em;
|
||||||
|
}
|
||||||
|
@media (min-width: 40em) {
|
||||||
|
.contact {
|
||||||
|
margin: 0.5em 0 0;
|
||||||
|
grid-template-columns: repeat(4, 1fr);
|
||||||
|
gap: 2em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.contact > dd {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
.contact > dt {
|
||||||
|
position: absolute;
|
||||||
|
left: -9999em;
|
||||||
|
}
|
||||||
|
.skillset {
|
||||||
|
display: grid;
|
||||||
|
gap: 0.5em 1em;
|
||||||
|
grid-template-columns: auto 1fr;
|
||||||
|
}
|
||||||
|
.skillset > dt {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.skillset > dd {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
/*
|
||||||
|
Left for good measure, could have been gold if
|
||||||
|
the upcoming Container queries was a thing.
|
||||||
|
|
||||||
|
@media (min-width: 80em) {
|
||||||
|
.skillset {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
.skillset > dt {
|
||||||
|
transform: translateY(0.2em);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
.resume {
|
||||||
|
max-width: 70em;
|
||||||
|
margin: 0 auto;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
grid-template-areas:
|
||||||
|
"name"
|
||||||
|
"about"
|
||||||
|
"skills"
|
||||||
|
"work"
|
||||||
|
"education"
|
||||||
|
"projects"
|
||||||
|
"courses"
|
||||||
|
"personal";
|
||||||
|
gap: 0;
|
||||||
|
}
|
||||||
|
@media (min-width: 40em) {
|
||||||
|
.resume {
|
||||||
|
grid-template-columns: 2fr 1fr;
|
||||||
|
grid-template-areas:
|
||||||
|
"name name"
|
||||||
|
"skills about"
|
||||||
|
"work work"
|
||||||
|
"education courses"
|
||||||
|
"projects personal";
|
||||||
|
gap: 2em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.name {
|
||||||
|
grid-area: name;
|
||||||
|
display: flex;
|
||||||
|
gap: 1em;
|
||||||
|
place-items: center;
|
||||||
|
text-align: center;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
.photo {
|
||||||
|
grid-area: photo;
|
||||||
|
}
|
||||||
|
.work {
|
||||||
|
grid-area: work;
|
||||||
|
}
|
||||||
|
.courses {
|
||||||
|
grid-area: courses;
|
||||||
|
}
|
||||||
|
.personal {
|
||||||
|
grid-area: personal;
|
||||||
|
}
|
||||||
|
.about {
|
||||||
|
grid-area: about;
|
||||||
|
background: #f1f1f1;
|
||||||
|
border-radius: 0.2em;
|
||||||
|
padding: 1em;
|
||||||
|
color: #111;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.education {
|
||||||
|
grid-area: education;
|
||||||
|
}
|
||||||
|
.projects {
|
||||||
|
grid-area: projects;
|
||||||
|
}
|
||||||
|
.skills {
|
||||||
|
grid-area: skills;
|
||||||
|
}
|
||||||
|
|
||||||
|
.event {
|
||||||
|
display: grid;
|
||||||
|
gap: 0.5em 0.5em;
|
||||||
|
margin: 2em 0;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.event--aside {
|
||||||
|
margin: 1em 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.event__title {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
.event__position {
|
||||||
|
font-variant: small-caps;
|
||||||
|
}
|
||||||
|
.event__content {
|
||||||
|
line-height: 1.5;
|
||||||
|
padding-top: 0.5em;
|
||||||
|
grid-column: 1 / 3;
|
||||||
|
}
|
||||||
|
.event__aside {
|
||||||
|
text-align: right;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
@page {
|
||||||
|
padding: 2cm 0;
|
||||||
|
margin: 0;
|
||||||
|
size: A4 portrait;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media print {
|
||||||
|
body {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
[href$="pdf"] {
|
||||||
|
display: none;
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
border: none !important;
|
||||||
|
color: #000 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.education,
|
||||||
|
.projects,
|
||||||
|
.work {
|
||||||
|
page-break-before: always;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Before Width: | Height: | Size: 152 B After Width: | Height: | Size: 15 KiB |