├── content
├── posty.txt
└── homepage.md
├── theme
├── script.js
├── assets
│ ├── favicon.ico
│ ├── favicon.png
│ ├── custom
│ │ ├── style
│ │ │ ├── header.css
│ │ │ ├── footer.css
│ │ │ ├── pagination.css
│ │ │ ├── homepage.css
│ │ │ ├── tag-page.css
│ │ │ ├── skip-link.css
│ │ │ ├── tags-page.css
│ │ │ ├── base.css
│ │ │ ├── debug.css
│ │ │ ├── graph.css
│ │ │ ├── reset.css
│ │ │ ├── layout.css
│ │ │ ├── fonts.css
│ │ │ ├── content.css
│ │ │ ├── search.css
│ │ │ ├── overlay-pagination.css
│ │ │ ├── links.css
│ │ │ ├── logo.css
│ │ │ └── tags-list.css
│ │ ├── script
│ │ │ ├── main.js
│ │ │ ├── modes.js
│ │ │ ├── views
│ │ │ │ ├── index.js
│ │ │ │ ├── search.js
│ │ │ │ ├── start.js
│ │ │ │ ├── tag.js
│ │ │ │ └── homepage.js
│ │ │ ├── components
│ │ │ │ ├── header.js
│ │ │ │ ├── intro.js
│ │ │ │ ├── activity-chart.js
│ │ │ │ ├── content.js
│ │ │ │ ├── pagination.js
│ │ │ │ ├── search-form.js
│ │ │ │ ├── tags-list.js
│ │ │ │ ├── canvas.js
│ │ │ │ └── chart-pagination.js
│ │ │ ├── finder.js
│ │ │ ├── links.js
│ │ │ ├── intros.js
│ │ │ ├── view-resources.js
│ │ │ ├── router.js
│ │ │ ├── helpers.js
│ │ │ ├── controller.js
│ │ │ └── vendor
│ │ │ │ ├── scrollbooster.min.js
│ │ │ │ └── kicss.js
│ │ └── fonts
│ │ │ └── playfair-display
│ │ │ ├── nuFiD-vYSZviVYUb_rj3ij__anPXDTzYgEM86xQ.woff2
│ │ │ ├── nuFiD-vYSZviVYUb_rj3ij__anPXDTLYgEM86xRbPQ.woff2
│ │ │ └── OFL.txt
│ ├── favicon.svg
│ └── logo.svg
├── templates
│ ├── components
│ │ ├── skip-link.hbs
│ │ ├── header.hbs
│ │ ├── tags.hbs
│ │ ├── footer.hbs
│ │ ├── graph.hbs
│ │ ├── links.hbs
│ │ └── logo.hbs
│ ├── layouts
│ │ └── grid.hbs
│ ├── base
│ │ ├── base-script.hbs
│ │ ├── features.hbs
│ │ ├── custom-head.hbs
│ │ ├── custom-script.hbs
│ │ ├── index.hbs
│ │ └── custom-style.hbs
│ ├── pages
│ │ ├── homepage
│ │ │ └── basic
│ │ │ │ ├── content.hbs
│ │ │ │ └── index.hbs
│ │ └── tags
│ │ │ ├── index.hbs
│ │ │ └── tag.hbs
│ ├── features
│ │ └── rss
│ │ │ └── index.hbs
│ └── helpers.js
├── style.css
└── theme-settings.css
├── .gitignore
├── src
├── scripts
│ ├── build.js
│ ├── start.js
│ └── publish-latest-link.js
├── addLink.js
├── publish.js
├── git.js
├── writ
│ ├── index.js
│ ├── prepare.js
│ └── minifyPostsJSON.js
├── channels
│ ├── tumblr.js
│ └── twitter.js
├── lib
│ └── Channel.js
└── helpers.js
├── .env.example
├── settings.json
├── package.json
├── .github
└── workflows
│ ├── deploy.yml
│ └── publish.yml
├── misc
├── commit-timestamp-for-date-published.js
├── stats-03-07-2020.md
├── router.js
└── setup.md
├── readme.md
└── add
/content/posty.txt:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/theme/script.js:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .env
3 | .DS_Store
4 | site
5 | _notes
6 |
--------------------------------------------------------------------------------
/src/scripts/build.js:
--------------------------------------------------------------------------------
1 | require('../writ').build({ rootDirectory: '.' })
2 |
--------------------------------------------------------------------------------
/src/scripts/start.js:
--------------------------------------------------------------------------------
1 | require('../writ').start({ rootDirectory: '.' })
2 |
--------------------------------------------------------------------------------
/theme/assets/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/scriptype/feed/HEAD/theme/assets/favicon.ico
--------------------------------------------------------------------------------
/theme/assets/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/scriptype/feed/HEAD/theme/assets/favicon.png
--------------------------------------------------------------------------------
/theme/templates/components/skip-link.hbs:
--------------------------------------------------------------------------------
1 | Jump to links
2 |
--------------------------------------------------------------------------------
/theme/assets/custom/style/header.css:
--------------------------------------------------------------------------------
1 | .header {
2 | perspective: var(--header-perspective);
3 | }
4 |
--------------------------------------------------------------------------------
/theme/assets/custom/script/main.js:
--------------------------------------------------------------------------------
1 | import Controller from './controller.js'
2 |
3 | Controller().start()
4 |
--------------------------------------------------------------------------------
/theme/templates/layouts/grid.hbs:
--------------------------------------------------------------------------------
1 |
2 | {{>@partial-block}}
3 |
4 |
--------------------------------------------------------------------------------
/theme/templates/components/header.hbs:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/theme/templates/base/base-script.hbs:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/theme/assets/custom/style/footer.css:
--------------------------------------------------------------------------------
1 | .footer {
2 | font-size: 1em;
3 | padding: 2em 0 4em;
4 | color: var(--footer-fg);
5 | text-align: center;
6 | }
7 |
--------------------------------------------------------------------------------
/theme/templates/base/features.hbs:
--------------------------------------------------------------------------------
1 | {{#if (isEnabled "rss")}}
2 |
3 | {{/if}}
4 |
--------------------------------------------------------------------------------
/theme/assets/custom/script/modes.js:
--------------------------------------------------------------------------------
1 | export const hydration = 'enhance statically rendered elements'
2 | export const full = 'fully re-render existing elements'
3 |
4 | export default {
5 | hydration,
6 | full
7 | }
8 |
--------------------------------------------------------------------------------
/theme/assets/custom/fonts/playfair-display/nuFiD-vYSZviVYUb_rj3ij__anPXDTzYgEM86xQ.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/scriptype/feed/HEAD/theme/assets/custom/fonts/playfair-display/nuFiD-vYSZviVYUb_rj3ij__anPXDTzYgEM86xQ.woff2
--------------------------------------------------------------------------------
/theme/assets/custom/fonts/playfair-display/nuFiD-vYSZviVYUb_rj3ij__anPXDTLYgEM86xRbPQ.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/scriptype/feed/HEAD/theme/assets/custom/fonts/playfair-display/nuFiD-vYSZviVYUb_rj3ij__anPXDTLYgEM86xRbPQ.woff2
--------------------------------------------------------------------------------
/theme/assets/custom/script/views/index.js:
--------------------------------------------------------------------------------
1 | export { default as Start } from './start.js'
2 | export { default as Homepage } from './homepage.js'
3 | export { default as Tag } from './tag.js'
4 | export { default as Search } from './search.js'
5 |
--------------------------------------------------------------------------------
/.env.example:
--------------------------------------------------------------------------------
1 | TUMBLR_CONSUMER_KEY=xxx
2 | TUMBLR_CONSUMER_SECRET=xxx
3 | TUMBLR_REQUEST_TOKEN=xxx
4 | TUMBLR_REQUEST_TOKEN_SECRET=xxx
5 | TWITTER_CONSUMER_KEY=xxx
6 | TWITTER_CONSUMER_SECRET=xxx
7 | TWITTER_ACCESS_TOKEN_KEY=xxx
8 | TWITTER_ACCESS_TOKEN_SECRET=xxx
9 |
--------------------------------------------------------------------------------
/src/addLink.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs/promises')
2 | const links = require('../links')
3 |
4 | module.exports = async ({ outputPath, link }) => {
5 | const content = JSON.stringify(links.concat(link), null, 2)
6 | return fs.writeFile(outputPath, content)
7 | }
8 |
--------------------------------------------------------------------------------
/theme/templates/base/custom-head.hbs:
--------------------------------------------------------------------------------
1 | {{#if (isTagsPage)}}
2 |
3 |
4 |
5 | {{/if}}
6 |
--------------------------------------------------------------------------------
/theme/assets/custom/style/pagination.css:
--------------------------------------------------------------------------------
1 | .pagination {
2 | display: flex;
3 | flex-direction: column;
4 | align-items: center;
5 | }
6 |
7 | .pagination-buttons {
8 | display: flex;
9 | gap: 2em;
10 | justify-content: center;
11 | margin: 2em 0 1em;
12 | }
13 |
--------------------------------------------------------------------------------
/theme/templates/pages/homepage/basic/content.hbs:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {{> @partial-block }}
5 |
6 |
7 | {{>components/graph}}
8 | {{>components/links}}
9 |
10 |
--------------------------------------------------------------------------------
/theme/templates/pages/homepage/basic/index.hbs:
--------------------------------------------------------------------------------
1 | {{#>base page="homepage"}}
2 | {{#>layouts/grid}}
3 | {{>components/skip-link}}
4 | {{>components/header}}
5 | {{>components/tags tags=(featuredTags)}}
6 | {{>pages/homepage/basic/content}}
7 | {{>components/footer}}
8 | {{/layouts/grid}}
9 | {{/base}}
10 |
--------------------------------------------------------------------------------
/src/publish.js:
--------------------------------------------------------------------------------
1 | require('dotenv').config()
2 | const twitter = require('./channels/twitter')
3 | const tumblr = require('./channels/tumblr')
4 |
5 | module.exports = (entry) => {
6 | return Promise.all([
7 | twitter.publish(entry),
8 | tumblr.publish(entry)
9 | ]).catch(e => {
10 | console.log('error publishing', e)
11 | })
12 | }
13 |
--------------------------------------------------------------------------------
/theme/templates/base/custom-script.hbs:
--------------------------------------------------------------------------------
1 | {{#if (not (isTagsPage))}}
2 |
3 |
4 | {{/if}}
5 |
6 | {{#if (hasCustomScript)}}
7 |
8 | {{/if}}
9 |
--------------------------------------------------------------------------------
/content/homepage.md:
--------------------------------------------------------------------------------
1 | Welcome to my feed!
2 |
3 |
4 | Follow it on Twitter, Tumblr or via RSS.
5 |
6 |
7 | Total links: {{allPosts.length}}
8 |
--------------------------------------------------------------------------------
/theme/assets/custom/script/components/header.js:
--------------------------------------------------------------------------------
1 | import { query } from '../helpers.js'
2 |
3 | const findLogo = () => query('.logo-container')
4 |
5 | const render = ({ onClickLogo }) => {
6 | findLogo().addEventListener('click', event => {
7 | event.preventDefault()
8 | onClickLogo()
9 | })
10 | }
11 |
12 | const blurLogo = () => {
13 | findLogo().blur()
14 | }
15 |
16 | export default {
17 | render,
18 | blur: blurLogo
19 | }
20 |
--------------------------------------------------------------------------------
/theme/assets/custom/style/homepage.css:
--------------------------------------------------------------------------------
1 | @import url("skip-link.css");
2 |
3 | .homepage-content {
4 | text-align: center;
5 | }
6 |
7 | .homepage-title {
8 | font-size: 2em;
9 | font-weight: 700;
10 | letter-spacing: -0.04em;
11 | word-spacing: 0.05em;
12 | }
13 |
14 | .homepage-social-links {
15 | margin-bottom: 1em;
16 | }
17 |
18 | .homepage-links-count {
19 | font-size: 1.25em;
20 | color: var(--link-count-color);
21 | }
22 |
--------------------------------------------------------------------------------
/theme/assets/custom/style/tag-page.css:
--------------------------------------------------------------------------------
1 | @import url("skip-link.css");
2 |
3 | .tag-header {
4 | font-size: 2.5em;
5 | font-weight: 700;
6 | letter-spacing: -0.04em;
7 | word-spacing: 0.05em;
8 | text-align: center;
9 | }
10 |
11 | .tag-header-post-count {
12 | display: block;
13 | color: var(--link-count-color);
14 | font-size: .5em;
15 | font-weight: initial;
16 | word-spacing: initial;
17 | letter-spacing: initial;
18 | }
19 |
--------------------------------------------------------------------------------
/theme/style.css:
--------------------------------------------------------------------------------
1 | @import url("style/reset.css");
2 | @import url("style/fonts.css");
3 | @import url("style/base.css");
4 | @import url("style/layout.css");
5 | @import url("style/header.css");
6 | @import url("style/logo.css");
7 | @import url("style/tags-list.css");
8 | @import url("style/content.css");
9 | @import url("style/graph.css");
10 | @import url("style/overlay-pagination.css");
11 | @import url("style/links.css");
12 | @import url("style/footer.css");
13 | @import url("style/pagination.css");
14 |
--------------------------------------------------------------------------------
/theme/templates/pages/tags/index.hbs:
--------------------------------------------------------------------------------
1 | {{#>base page="tags"}}
2 |
3 | {{>components/logo}}
4 | {{#if tags.length}}
5 | {{#each tags as |tag|}}
6 | #{{tag.tag}}
7 | {{/each}}
8 | {{else}}
9 | {{lookup 'tagsNotFoundHeader'}}
10 | {{/if}}
11 |
12 | 💛
13 | {{/base}}
14 |
--------------------------------------------------------------------------------
/theme/assets/custom/style/skip-link.css:
--------------------------------------------------------------------------------
1 | .skip-link {
2 | position: absolute;
3 | top: 0;
4 | left: 0;
5 | transform: translateY(-125%);
6 | transform-origin: center top;
7 | opacity: .5;
8 | padding: 0.3em 1.6em 0.5em 0.8em;
9 | color: white;
10 | background: royalblue;
11 | font-size: 1.5em;
12 | z-index: 1;
13 | text-decoration: none;
14 | border-radius: 0 0 4em 0;
15 | transition: all .2s;
16 | }
17 |
18 | .skip-link:focus {
19 | opacity: 1;
20 | transform: translateY(0);
21 | }
22 |
--------------------------------------------------------------------------------
/src/git.js:
--------------------------------------------------------------------------------
1 | const { execute } = require('./helpers')
2 |
3 | const Git = {
4 | commit({ message, paths }) {
5 | return execute({
6 | cmd: `\\
7 | git pull && \\
8 | git add ${paths.join(' ')} && \\
9 | git commit -m "${message}"
10 | `,
11 | outStream: process.stdout,
12 | errStream: process.stderr
13 | })
14 | },
15 |
16 | push() {
17 | return execute({
18 | cmd: 'git push',
19 | outStream: process.stdout,
20 | errStream: process.stderr
21 | })
22 | }
23 | }
24 |
25 | module.exports = Git
26 |
--------------------------------------------------------------------------------
/theme/templates/pages/tags/tag.hbs:
--------------------------------------------------------------------------------
1 | {{#>base page="tag"}}
2 | {{#>layouts/grid}}
3 | {{>components/skip-link}}
4 | {{>components/header}}
5 | {{>components/tags tags=(featuredTags)}}
6 |
7 |
8 |
12 |
13 | {{>components/graph}}
14 | {{>components/links}}
15 |
16 | {{>components/footer}}
17 | {{/layouts/grid}}
18 | {{/base}}
19 |
--------------------------------------------------------------------------------
/src/scripts/publish-latest-link.js:
--------------------------------------------------------------------------------
1 | const { readFile } = require('fs/promises')
2 | const path = require('path')
3 | const { last } = require('../helpers')
4 | const publish = require('../publish')
5 |
6 | const publishLatestLink = async () => {
7 | const linksPath = path.join(__dirname, '..', '..', 'links.json')
8 | let linksJSON
9 | try {
10 | linksJSON = await readFile(linksPath, { encoding: 'utf-8' })
11 | } catch (e) {
12 | return console.log('Error reading links.json', e)
13 | }
14 | const lastLink = last(JSON.parse(linksJSON))
15 | return publish(lastLink)
16 | }
17 |
18 | publishLatestLink()
19 |
--------------------------------------------------------------------------------
/src/writ/index.js:
--------------------------------------------------------------------------------
1 | const prepare = require('./prepare')
2 | const minifyPostsJSON = require('./minifyPostsJSON')
3 |
4 | const run = async ({ links, rootDirectory, mode, debug }) => {
5 | const writ = prepare(links)
6 |
7 | await writ[mode]({
8 | rootDirectory: rootDirectory,
9 | cli: true,
10 | debug,
11 | onFinish({ settings }) {
12 | return minifyPostsJSON(settings)
13 | }
14 | })
15 | }
16 |
17 | module.exports = {
18 | build: (options) => run({
19 | ...options,
20 | mode: 'build'
21 | }),
22 |
23 | start: (options) => run({
24 | ...options,
25 | mode: 'start'
26 | })
27 | }
28 |
--------------------------------------------------------------------------------
/theme/templates/components/tags.hbs:
--------------------------------------------------------------------------------
1 |
17 |
--------------------------------------------------------------------------------
/theme/assets/custom/style/tags-page.css:
--------------------------------------------------------------------------------
1 | .all-tags-listing {
2 | background: #222;
3 | display: flex;
4 | flex-wrap: wrap;
5 | gap: 0.5em;
6 | justify-content: center;
7 | align-items: center;
8 | padding: 0 3.6rem;
9 | }
10 |
11 | .all-tags-listing-item {
12 | font-size: calc(1em + 10em * var(--importance));
13 | color: hsl(
14 | var(--i),
15 | 80%,
16 | calc(70% - 20% * var(--importance))
17 | );
18 | }
19 |
20 | .all-tags-listing-end-message {
21 | padding: 3em;
22 | font-size: 2em;
23 | background: #222;
24 | color: #eee;
25 | text-align: center;
26 | }
27 |
--------------------------------------------------------------------------------
/theme/templates/components/footer.hbs:
--------------------------------------------------------------------------------
1 |
12 |
--------------------------------------------------------------------------------
/src/channels/tumblr.js:
--------------------------------------------------------------------------------
1 | const tumblr = require('tumblr.js')
2 | const Channel = require('../lib/Channel')
3 |
4 | const client = tumblr.createClient({
5 | consumer_key: process.env.TUMBLR_CONSUMER_KEY,
6 | consumer_secret: process.env.TUMBLR_CONSUMER_SECRET,
7 | token: process.env.TUMBLR_REQUEST_TOKEN,
8 | token_secret: process.env.TUMBLR_REQUEST_TOKEN_SECRET
9 | })
10 |
11 | client.returnPromises()
12 |
13 | const postLink = item =>
14 | client.createLinkPost('readsfeed', {
15 | tags: item.tags.join(','),
16 | title: item.title,
17 | url: item.url
18 | })
19 |
20 | module.exports = new Channel({
21 | name: 'tumblr',
22 | method: postLink
23 | })
24 |
--------------------------------------------------------------------------------
/theme/assets/custom/style/base.css:
--------------------------------------------------------------------------------
1 | * {
2 | box-sizing: border-box;
3 | }
4 |
5 | html {
6 | scroll-behavior: smooth;
7 | }
8 |
9 | body {
10 | font: 100 normal 16px/1.5 "Playfair Display", times, serif;
11 | background: var(--bg);
12 | color: var(--fg);
13 | }
14 |
15 | img {
16 | vertical-align: middle;
17 | }
18 |
19 | a {
20 | color: var(--primary-color);
21 | }
22 |
23 | strong {
24 | font-weight: bold;
25 | }
26 |
27 | button {
28 | padding: 0;
29 | margin: 0;
30 | border: none;
31 | background: none;
32 | vertical-align: middle;
33 | appearance: none;
34 | -webkit-appearance: none;
35 | font: inherit;
36 | }
37 |
--------------------------------------------------------------------------------
/src/writ/prepare.js:
--------------------------------------------------------------------------------
1 | const writ = require('writ-cms')
2 |
3 | const extendPostAsLink = (post, link) => {
4 | return {
5 | ...post,
6 | publishDatePrototype: {
7 | value: new Date(link.datePublished),
8 | checkCache: false
9 | },
10 | type: 'link',
11 | permalink: link.url,
12 | ...link
13 | }
14 | }
15 |
16 | module.exports = (_links) => {
17 | const links = _links || require('../../links.json')
18 |
19 | writ.useContentModel((value) => {
20 | const samplePost = value.posts[0]
21 | return {
22 | ...value,
23 | posts: links.map(link => extendPostAsLink(samplePost, link))
24 | }
25 | })
26 |
27 | return writ
28 | }
29 |
--------------------------------------------------------------------------------
/theme/assets/custom/style/debug.css:
--------------------------------------------------------------------------------
1 | :root {
2 | --debug-v-rythm: 0;
3 | --debug-v-rythm-offset: 3.8px;
4 | --debug-v-rythm-height: 0.7em;
5 | }
6 |
7 | body::after {
8 | content: "";
9 | display: block;
10 | width: calc(var(--debug-v-rythm) * 100%);
11 | height: calc(var(--debug-v-rythm) * 100%);
12 | position: fixed;
13 | top: var(--debug-v-rythm-offset);
14 | left: 0;
15 | z-index: 9999;
16 | background:
17 | repeating-linear-gradient(
18 | to bottom,
19 | transparent,
20 | transparent var(--debug-v-rythm-height),
21 | black calc(var(--debug-v-rythm-height) + 1px)
22 | );
23 | pointer-events: none;
24 | }
25 |
--------------------------------------------------------------------------------
/theme/templates/components/graph.hbs:
--------------------------------------------------------------------------------
1 |
17 |
--------------------------------------------------------------------------------
/src/channels/twitter.js:
--------------------------------------------------------------------------------
1 | const { TwitterApi } = require('twitter-api-v2')
2 | const Channel = require('../lib/Channel')
3 |
4 | const client = new TwitterApi({
5 | appKey: process.env.TWITTER_CONSUMER_KEY,
6 | appSecret: process.env.TWITTER_CONSUMER_SECRET,
7 | accessToken: process.env.TWITTER_ACCESS_TOKEN_KEY,
8 | accessSecret: process.env.TWITTER_ACCESS_TOKEN_SECRET
9 | })
10 |
11 | const statusTemplate = link => {
12 | const title = link.isTweet ? link.retweetQuote : link.title
13 | return [title, link.url].filter(Boolean).join(': ')
14 | }
15 |
16 | const tweet = item => {
17 | return client.v2.tweet(statusTemplate(item))
18 | }
19 |
20 | module.exports = new Channel({
21 | name: 'twitter',
22 | method: tweet
23 | })
24 |
--------------------------------------------------------------------------------
/theme/templates/base/index.hbs:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | {{pageTitle}}
10 | {{>base/custom-head}}
11 | {{>base/features}}
12 | {{>base/custom-style}}
13 |
14 |
15 |
16 | {{>@partial-block}}
17 | {{>base/base-script}}
18 | {{>base/custom-script}}
19 |
20 |
21 |
--------------------------------------------------------------------------------
/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "theme": "zen",
3 | "url": "https://enes.in/feed",
4 | "title": "Reads Feed",
5 | "description": "A collection of links to blog posts, talks and opinions on art, design and technology",
6 | "permalinkPrefix": "/feed",
7 | "revisionHistory": "off",
8 | "rss": "on",
9 | "exportDirectory": "site",
10 | "featuredTags": [
11 | "web",
12 | "software",
13 | "design",
14 | "accessibility",
15 | "ux",
16 | "ai",
17 | "art",
18 | "science",
19 | "environment",
20 | "nature",
21 | "philosophy",
22 | "life",
23 | "privacy",
24 | "video",
25 | "git",
26 | "colors",
27 | "geography",
28 | "javascript",
29 | "css",
30 | "security",
31 | "podcast",
32 | "animation"
33 | ]
34 | }
35 |
--------------------------------------------------------------------------------
/theme/assets/custom/script/components/intro.js:
--------------------------------------------------------------------------------
1 | import { debounce, region, transition } from '../helpers.js'
2 |
3 | const render = debounce(async (template, data) => {
4 | const $el = region('intro')
5 | const { html, transitionSettings } = template(data)
6 | const $newEl = $el.cloneNode(true)
7 | $newEl.innerHTML = html
8 | await transition({
9 | target: $el,
10 | className: 'exiting',
11 | duration: (transitionSettings || {}).exit || 500,
12 | crossFade: 2/3
13 | })
14 | $el.parentElement.prepend($newEl)
15 | await transition({
16 | target: $newEl,
17 | className: 'entering',
18 | duration: (transitionSettings || {}).enter || 500,
19 | crossFade: 2/3
20 | })
21 | $el.remove()
22 | }, 500)
23 |
24 | export default {
25 | render
26 | }
27 |
--------------------------------------------------------------------------------
/src/writ/minifyPostsJSON.js:
--------------------------------------------------------------------------------
1 | const { readFile, writeFile } = require('fs/promises')
2 | const { join } = require('path')
3 |
4 | const minify = async ({ exportDirectory }) => {
5 | const postsJSONFile = await readFile(
6 | join(exportDirectory, 'posts.json'),
7 | { encoding: 'utf-8' }
8 | )
9 |
10 | const postsJSON = JSON.parse(postsJSONFile)
11 |
12 | const minifiedPostsJSON = postsJSON.map(post => ({
13 | title: post.title,
14 | datePublished: post.datePublished,
15 | tags: post.tags,
16 | url: post.url
17 | }))
18 |
19 | const minifiedPostsJSONFileContent = JSON.stringify(minifiedPostsJSON)
20 |
21 | await writeFile(
22 | join(exportDirectory, 'posts.min.json'),
23 | minifiedPostsJSONFileContent
24 | )
25 | }
26 |
27 | module.exports = minify
28 |
--------------------------------------------------------------------------------
/theme/templates/base/custom-style.hbs:
--------------------------------------------------------------------------------
1 | {{#if (isHomePage)}}
2 |
3 | {{/if}}
4 |
5 | {{#if (isTagPage)}}
6 |
7 | {{/if}}
8 |
9 | {{#if (isTagsPage)}}
10 |
11 | {{/if}}
12 |
13 | {{#if (isStartMode)}}
14 |
15 | {{/if}}
16 |
17 | {{#if (hasThemeSettings)}}
18 |
19 | {{/if}}
20 |
21 | {{#if (hasCustomStyle)}}
22 |
23 | {{/if}}
24 |
--------------------------------------------------------------------------------
/theme/assets/custom/script/finder.js:
--------------------------------------------------------------------------------
1 | export default class Finder {
2 | constructor({ entries, searchIn }) {
3 | this.entries = entries
4 | this.itemIndexFn = searchIn
5 | this.index = this.makeIndex()
6 | }
7 |
8 | makeIndex() {
9 | return this.entries.map((entry, index) => ({
10 | content: this.itemIndexFn.call(entry).join('\n'),
11 | index
12 | }))
13 | }
14 |
15 | find(query) {
16 | const escapedQuery = query.replace(/\[/g, '\\[')
17 | const matches = this.index.filter(({ content }) => {
18 | return content.match(new RegExp(escapedQuery, 'gsi'))
19 | })
20 |
21 | return this.entries
22 | .map((entry, i) => {
23 | const isMatching = matches.find(({ index }) => index === i)
24 | return isMatching && entry
25 | })
26 | .filter(Boolean)
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/theme/assets/custom/script/views/search.js:
--------------------------------------------------------------------------------
1 | import Modes from '../modes.js'
2 | import Intros from '../intros.js'
3 | import Intro from '../components/intro.js'
4 | import SearchForm from '../components/search-form.js'
5 | import ActivityChart from '../components/activity-chart.js'
6 | import Pagination from '../components/pagination.js'
7 | import Content from '../components/content.js'
8 |
9 | const render = ({ links, searchQuery, }) => {
10 | Intro.render(Intros.search, {
11 | searchQuery,
12 | links
13 | })
14 |
15 | SearchForm.setInputValue(searchQuery)
16 |
17 | Content.render({
18 | links
19 | })
20 |
21 | ActivityChart.render({
22 | readonly: true,
23 | mode: Modes.full,
24 | links,
25 | pages: [links]
26 | })
27 |
28 | Pagination.remove()
29 | }
30 |
31 | export default {
32 | render
33 | }
34 |
--------------------------------------------------------------------------------
/theme/assets/custom/style/graph.css:
--------------------------------------------------------------------------------
1 | .graph-container {
2 | margin: 3em 0 4em;
3 | overflow: auto;
4 | }
5 |
6 | .graph-container.has-scroll {
7 | cursor: grab;
8 | }
9 |
10 | .graph-container.has-scroll.faked-active-state {
11 | cursor: grabbing;
12 | }
13 |
14 | .graph-container.exiting {
15 | animation: disappear var(--transition-duration) forwards;
16 | }
17 |
18 | @keyframes disappear {
19 | 99% {
20 | transform: scaleY(1);
21 | }
22 | to {
23 | opacity: 0;
24 | transform: scaleY(0);
25 | }
26 | }
27 |
28 | .graph-container:has([open]) {
29 | margin: 3em 0 0;
30 | }
31 |
32 | .js-enhanced .graph-container {
33 | margin: 1em 0;
34 | }
35 |
36 | .graph {
37 | height: 100px;
38 | }
39 |
40 | .canvas-container {
41 | width: min-content;
42 | position: relative;
43 | }
44 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "feed",
3 | "version": "3.1.0",
4 | "description": "",
5 | "main": "index.js",
6 | "dependencies": {
7 | "axios": "^1.6.8",
8 | "cheerio": "^1.0.0-rc.3",
9 | "dotenv": "^8.2.0",
10 | "inquirer": "^7.0.0",
11 | "lodash": "^4.17.11",
12 | "tumblr.js": "^2.0.2",
13 | "twitter-api-v2": "^1.14.3",
14 | "writ-cms": "github:scriptype/writ-cms#v0.29.2"
15 | },
16 | "scripts": {
17 | "start": "node ./src/scripts/start",
18 | "build": "node ./src/scripts/build",
19 | "publish-latest-link": "node src/scripts/publish-latest-link.js"
20 | },
21 | "repository": {
22 | "type": "git",
23 | "url": "git+https://github.com/scriptype/feed.git"
24 | },
25 | "keywords": [],
26 | "author": "",
27 | "license": "ISC",
28 | "bugs": {
29 | "url": "https://github.com/scriptype/feed/issues"
30 | },
31 | "homepage": "https://github.com/scriptype/feed#readme"
32 | }
33 |
--------------------------------------------------------------------------------
/theme/assets/custom/script/links.js:
--------------------------------------------------------------------------------
1 | import Finder from './finder.js'
2 | import { fetchPrefix } from './helpers.js'
3 |
4 | let links = []
5 | let finderInstance
6 |
7 | const fetchLinks = (fileName) => {
8 | links = fetch(`${fetchPrefix}/${fileName}`).then(r => r.json())
9 | return links
10 | }
11 |
12 | const all = async () => {
13 | return await links
14 | }
15 |
16 | const findByTag = async (tag) => {
17 | return (await links).filter((link) => {
18 | return !!link.tags.find(t => t.tag === tag)
19 | })
20 | }
21 |
22 | const search = async (query) => {
23 | finderInstance = finderInstance || new Finder({
24 | entries: await links,
25 | searchIn() {
26 | return [
27 | this.title,
28 | this.url,
29 | this.tags.map(({ tag }) => tag).join(' ')
30 | ]
31 | }
32 | })
33 |
34 | return finderInstance.find(query)
35 | }
36 |
37 | export default {
38 | fetch: fetchLinks,
39 | all,
40 | findByTag,
41 | search
42 | }
43 |
--------------------------------------------------------------------------------
/theme/templates/components/links.hbs:
--------------------------------------------------------------------------------
1 |
2 | {{#each posts as |link|}}
3 |
16 | {{/each}}
17 |
18 | {{#with pagination}}
19 |
29 | {{/with}}
30 |
--------------------------------------------------------------------------------
/.github/workflows/deploy.yml:
--------------------------------------------------------------------------------
1 | name: Deploy site to github pages
2 |
3 | on:
4 | push:
5 | branches: [master, main]
6 |
7 | permissions:
8 | contents: read
9 | pages: write
10 | id-token: write
11 |
12 | concurrency:
13 | group: "pages"
14 | cancel-in-progress: false
15 |
16 | jobs:
17 | deploy:
18 | environment:
19 | name: github-pages
20 | url: ${{ steps.deployment.outputs.page_url }}
21 | runs-on: ubuntu-latest
22 | steps:
23 | - name: Checkout
24 | uses: actions/checkout@v4
25 |
26 | - name: Setup Pages
27 | uses: actions/configure-pages@v4
28 |
29 | - name: Install Node.js
30 | uses: actions/setup-node@v3
31 | with:
32 | node-version: '16'
33 |
34 | - name: Install dependencies
35 | run: npm i
36 |
37 | - name: Build
38 | run: npm run build
39 |
40 | - name: Upload artifact
41 | uses: actions/upload-pages-artifact@v3
42 | with:
43 | path: 'site'
44 |
45 | - name: Deploy to GitHub Pages
46 | id: deployment
47 | uses: actions/deploy-pages@v4
48 |
--------------------------------------------------------------------------------
/theme/assets/custom/script/intros.js:
--------------------------------------------------------------------------------
1 | const homepage = ({ links }) => ({
2 | transitionSettings: {
3 | enter: 800
4 | },
5 | html: `
6 |
7 |
Welcome to my feed!
8 |
9 |
10 | Follow it on Twitter, Tumblr or via RSS.
11 |
12 |
13 |
Total links: ${links.length}
14 |
15 | `
16 | })
17 |
18 | const tag = ({ tag, links }) => ({
19 | html: `
20 |
24 | `
25 | })
26 |
27 | const search = ({ searchQuery, links }) => ({
28 | html: `
29 |
30 | “${searchQuery}”
31 | ${links.length} links
32 |
33 | `
34 | })
35 |
36 | export default {
37 | homepage,
38 | tag,
39 | search
40 | }
41 |
--------------------------------------------------------------------------------
/theme/assets/custom/script/view-resources.js:
--------------------------------------------------------------------------------
1 | import { fetchPrefix } from './helpers.js'
2 |
3 | export default class ViewResources {
4 | constructor({ pathPrefix = fetchPrefix + '/assets/custom/', stylesheets, scripts }) {
5 | this.pathPrefix = pathPrefix
6 | this.stylesheets = stylesheets
7 | this.scripts = scripts
8 | }
9 |
10 | load() {
11 | for (let id in this.stylesheets) {
12 | if (!this.stylesheets.hasOwnProperty(id)) {
13 | continue
14 | }
15 | if (document.getElementById(id)) {
16 | continue
17 | }
18 | const link = document.createElement('link')
19 | link.rel = 'stylesheet'
20 | link.id = id
21 | link.href = this.pathPrefix + this.stylesheets[id]
22 | document.head.appendChild(link)
23 | }
24 |
25 | for (let id in this.scripts) {
26 | if (!this.scripts.hasOwnProperty(id)) {
27 | continue
28 | }
29 | if (document.getElementById(id)) {
30 | continue
31 | }
32 | const script = document.createElement('script')
33 | script.id = id
34 | script.src = this.pathPrefix + this.scripts[id]
35 | document.body.appendChild(script)
36 | }
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/lib/Channel.js:
--------------------------------------------------------------------------------
1 | module.exports = class Channel {
2 | constructor({ name, method, waitBetween = 1000 }) {
3 | this.name = name
4 | this.method = method
5 | this.waitBetween = waitBetween
6 | return this
7 | }
8 |
9 | log(message, ...args) {
10 | console.log(`log ${this.name}: ${message}`, ...args)
11 | }
12 |
13 | error(message, ...args) {
14 | console.error(`error ${this.name}: ${message}`, ...args)
15 | }
16 |
17 | logNotStarting(...args) {
18 | this.log(...['nothing to publish', ...args])
19 | }
20 |
21 | logStart(...args) {
22 | this.log(...['publishing', ...args])
23 | }
24 |
25 | logSuccess(...args) {
26 | this.log(...['published', ...args])
27 | }
28 |
29 | logError(...args) {
30 | this.error(...['couldnt publish', ...args])
31 | }
32 |
33 | wait(timeout) {
34 | return new Promise(resolve => {
35 | setTimeout(() => resolve(), timeout)
36 | })
37 | }
38 |
39 | async publish(item) {
40 | if (!item) {
41 | return this.logNotStarting()
42 | }
43 | try {
44 | this.logStart()
45 | await this.method(item)
46 | this.logSuccess()
47 | } catch (e) {
48 | this.logError(e)
49 | }
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/theme/assets/custom/script/router.js:
--------------------------------------------------------------------------------
1 | import { fetchPrefix } from './helpers.js'
2 |
3 | const getPaginationPart = (pageNumber) => {
4 | return pageNumber > 1 ? `/page/${pageNumber}` : '/'
5 | }
6 |
7 | export default {
8 | debug: false,
9 |
10 | logState(...msg) {
11 | if (this.debug) {
12 | console.info(...msg)
13 | }
14 | },
15 |
16 | start(state, onGoBack, options) {
17 | window.history.replaceState(state, '', document.location.href)
18 | window.addEventListener("popstate", onGoBack)
19 | this.debug = options && options.debug
20 | this.logState('Router.start', state)
21 | },
22 |
23 | homepage(state) {
24 | const path = getPaginationPart(state.pageNumber)
25 | window.history.pushState(state, '', `${fetchPrefix}${path}`)
26 | this.logState('Router.homepage', state)
27 | },
28 |
29 | tag(state) {
30 | const paginationPart = getPaginationPart(state.pageNumber)
31 | window.history.pushState(state, '', `${fetchPrefix}/tags/${state.tag}${paginationPart}`)
32 | this.logState('Router.tag', state)
33 | },
34 |
35 | search(state) {
36 | window.history.pushState(state, '', `?search=${state.searchQuery}`)
37 | this.logState('Router.search', state)
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/.github/workflows/publish.yml:
--------------------------------------------------------------------------------
1 | name: Publish latest link
2 |
3 | on:
4 | push:
5 | branches: [master, main]
6 |
7 | jobs:
8 | publish:
9 | runs-on: ubuntu-latest
10 | if: contains(github.event.head_commit.message, '[publish]')
11 | steps:
12 | - name: Checkout
13 | uses: actions/checkout@v4
14 |
15 | - name: Install Node.js
16 | uses: actions/setup-node@v3
17 | with:
18 | node-version: '16'
19 |
20 | - name: Install dependencies
21 | run: npm i
22 |
23 | - name: Run publish script
24 | env:
25 | TUMBLR_CONSUMER_KEY: ${{ secrets.TUMBLR_CONSUMER_KEY }}
26 | TUMBLR_CONSUMER_SECRET: ${{ secrets.TUMBLR_CONSUMER_SECRET }}
27 | TUMBLR_REQUEST_TOKEN: ${{ secrets.TUMBLR_REQUEST_TOKEN }}
28 | TUMBLR_REQUEST_TOKEN_SECRET: ${{ secrets.TUMBLR_REQUEST_TOKEN_SECRET }}
29 | TWITTER_API_VERSION: ${{ secrets.TWITTER_API_VERSION }}
30 | TWITTER_CONSUMER_KEY: ${{ secrets.TWITTER_CONSUMER_KEY }}
31 | TWITTER_CONSUMER_SECRET: ${{ secrets.TWITTER_CONSUMER_SECRET }}
32 | TWITTER_ACCESS_TOKEN_KEY: ${{ secrets.TWITTER_ACCESS_TOKEN_KEY }}
33 | TWITTER_ACCESS_TOKEN_SECRET: ${{ secrets.TWITTER_ACCESS_TOKEN_SECRET }}
34 | run: npm run publish-latest-link
35 |
--------------------------------------------------------------------------------
/theme/assets/custom/style/reset.css:
--------------------------------------------------------------------------------
1 | /* http://meyerweb.com/eric/tools/css/reset/
2 | v2.0 | 20110126
3 | License: none (public domain)
4 | */
5 |
6 | html, body, div, span, applet, object, iframe,
7 | h1, h2, h3, h4, h5, h6, p, blockquote, pre,
8 | a, abbr, acronym, address, big, cite, code,
9 | del, dfn, em, img, ins, kbd, q, s, samp,
10 | small, strike, strong, sub, sup, tt, var,
11 | b, u, i, center,
12 | dl, dt, dd, ol, ul, li,
13 | fieldset, form, label, legend,
14 | table, caption, tbody, tfoot, thead, tr, th, td,
15 | article, aside, canvas, details, embed,
16 | figure, figcaption, footer, header, hgroup,
17 | menu, nav, output, ruby, section, summary,
18 | time, mark, audio, video {
19 | margin: 0;
20 | padding: 0;
21 | border: 0;
22 | font-size: 100%;
23 | font: inherit;
24 | vertical-align: baseline;
25 | }
26 | /* HTML5 display-role reset for older browsers */
27 | article, aside, details, figcaption, figure,
28 | footer, header, hgroup, menu, nav, section {
29 | display: block;
30 | }
31 | body {
32 | line-height: 1;
33 | }
34 | ol, ul {
35 | list-style: none;
36 | }
37 | blockquote, q {
38 | quotes: none;
39 | }
40 | blockquote:before, blockquote:after,
41 | q:before, q:after {
42 | content: '';
43 | content: none;
44 | }
45 | table {
46 | border-collapse: collapse;
47 | border-spacing: 0;
48 | }
49 |
--------------------------------------------------------------------------------
/theme/assets/custom/style/layout.css:
--------------------------------------------------------------------------------
1 | .app {
2 | display: grid;
3 | grid-template-columns: 190px min-content;
4 | justify-content: center;
5 | }
6 |
7 | .header {
8 | place-self: end;
9 | grid-row: 1;
10 | }
11 |
12 | #search-form-container {
13 | height: 2em;
14 | margin-bottom: 2em;
15 | }
16 |
17 | .js-enabled #search-form-container {
18 | height: auto;
19 | margin-bottom: 0;
20 | }
21 |
22 | .tags {
23 | width: min-content;
24 | place-self: end;
25 | grid-column: 1;
26 | margin-right: 3em;
27 | }
28 |
29 | .content {
30 | width: var(--content-width);
31 | grid-row: 1 / span 3;
32 | }
33 |
34 | .footer {
35 | width: var(--content-width);
36 | grid-column: 2;
37 | }
38 |
39 | @media (max-width: 640px) {
40 | :root {
41 | --content-width: 100vw;
42 | }
43 | }
44 |
45 | @media (max-width: 900px) {
46 | .app {
47 | grid-template-columns: 1fr;
48 | justify-items: center;
49 | }
50 |
51 | .header,
52 | .tags {
53 | place-self: center;
54 | }
55 |
56 | .tags {
57 | position: fixed;
58 | bottom: 0;
59 | right: 0;
60 | z-index: 1;
61 | width: 100%;
62 | margin-right: 0;
63 | }
64 |
65 | .content, .footer {
66 | grid-column: 1;
67 | }
68 |
69 | .content {
70 | grid-row: 3;
71 | }
72 |
73 | .footer {
74 | grid-row: 4;
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/theme/templates/features/rss/index.hbs:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {{ feed.title }}
5 | {{ feed.link }}
6 | {{ feed.description }}
7 | {{ settings.language }}
8 |
9 | {{ settings.site.title }}
10 | {{ feed.iconUrl }}
11 | {{ settings.site.url }}
12 |
13 | {{#each categories as |category|}}
14 | {{ category.name }}
15 | {{/each}}
16 | {{ feed.lastBuildDate }}
17 | All rights reserved 2024, {{ settings.site.title }}
18 | https://www.rssboard.org/rss-specification
19 | writ-cms
20 |
21 | {{#each posts as |post|}}
22 | -
23 |
24 | {{ permalink }}
25 | {{ permalink }}
26 | {{ publishDateUTC }}
27 |
28 |
29 | {{/each}}
30 |
31 |
32 |
--------------------------------------------------------------------------------
/theme/assets/favicon.svg:
--------------------------------------------------------------------------------
1 |
2 |
16 |
--------------------------------------------------------------------------------
/theme/assets/custom/script/views/start.js:
--------------------------------------------------------------------------------
1 | import Modes from '../modes.js'
2 | import ViewResources from '../view-resources.js'
3 | import Intros from '../intros.js'
4 | import Intro from '../components/intro.js'
5 | import Header from '../components/header.js'
6 | import TagsList from '../components/tags-list.js'
7 | import SearchForm from '../components/search-form.js'
8 | import ActivityChart from '../components/activity-chart.js'
9 | import Pagination from '../components/pagination.js'
10 | import Content from '../components/content.js'
11 | import { scrollToTop, getPages, getPageLinks } from '../helpers.js'
12 |
13 | const render = async ({
14 | onClickLogo,
15 | onClickTag,
16 | onSearch,
17 | onResetSearch,
18 | onPaginate,
19 | tag,
20 | links,
21 | pageNumber
22 | }) => {
23 | const pages = getPages({ links })
24 | const pageLinks = getPageLinks({ pages, pageNumber })
25 |
26 | Header.render({ onClickLogo })
27 |
28 | TagsList.render({ onClickTag })
29 |
30 | ActivityChart.render({
31 | mode: Modes.hydration,
32 | tag,
33 | links,
34 | pages,
35 | pageNumber,
36 | onPaginate: (payload) => onPaginate({
37 | ...payload,
38 | tag,
39 | paginationType: 'ActivityChart'
40 | })
41 | })
42 |
43 | Content.render({
44 | mode: Modes.hydration,
45 | links: pageLinks
46 | })
47 |
48 | Pagination.render({
49 | mode: Modes.hydration,
50 | tag,
51 | onPaginate: onPaginate
52 | })
53 |
54 | await SearchForm.render({
55 | onSearch,
56 | onReset: onResetSearch
57 | })
58 | }
59 |
60 | export default {
61 | render
62 | }
63 |
--------------------------------------------------------------------------------
/misc/commit-timestamp-for-date-published.js:
--------------------------------------------------------------------------------
1 | /*
2 | * This is a script that was run just once to add datePublished field
3 | * to links for the first time.
4 | *
5 | * Extract UNIX timestamp of the commit that adds the link.
6 | *
7 | * Before running, install the dependency:
8 | * npm i -S isomorphic-git
9 | *
10 | * The file is kept for reference and also why not.
11 | * */
12 | const fs = require('fs')
13 | const { writeFile } = require('fs/promises')
14 | const git = require('isomorphic-git')
15 |
16 | const run = async () => {
17 | const links = require('./links.json')
18 |
19 | const logs = await git.log({
20 | fs,
21 | dir: '.'
22 | })
23 |
24 | const findMatchingLog = (link) => {
25 | const urlForRegex = link.url
26 | .replace(/\?/, '\\?')
27 | .replace(/\(/, '\\(')
28 | .replace(/\)/, '\\)')
29 | const regex = new RegExp(`Add: ${urlForRegex}(?:\n)$`)
30 | const regex2 = new RegExp(`- ${urlForRegex}`)
31 | return logs.find(log => (
32 | log.commit.message.match(regex) ||
33 | log.commit.message.match(regex2)
34 | ))
35 | }
36 |
37 | const findExceptionsLog = (links) => {
38 | return logs.find(log => (
39 | log.commit.message.match('alwaysjudgeabookbyitscover')
40 | ))
41 | }
42 |
43 | const linksWithDates = links.map(link => {
44 | const matchingLog = findMatchingLog(link) || findExceptionsLog(link)
45 | return {
46 | ...link,
47 | datePublished: matchingLog.commit.committer.timestamp * 1000
48 | }
49 | })
50 |
51 | await writeFile('./links.json', JSON.stringify(linksWithDates, null, 2))
52 | }
53 |
54 | run()
55 |
--------------------------------------------------------------------------------
/theme/assets/custom/style/fonts.css:
--------------------------------------------------------------------------------
1 | /* latin-ext */
2 | @font-face {
3 | font-family: 'Playfair Display';
4 | font-style: normal;
5 | font-weight: 400;
6 | font-display: swap;
7 | src: url(../fonts/playfair-display/nuFiD-vYSZviVYUb_rj3ij__anPXDTLYgEM86xRbPQ.woff2) format('woff2');
8 | unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
9 | }
10 | /* latin */
11 | @font-face {
12 | font-family: 'Playfair Display';
13 | font-style: normal;
14 | font-weight: 400;
15 | font-display: swap;
16 | src: url(../fonts/playfair-display/nuFiD-vYSZviVYUb_rj3ij__anPXDTzYgEM86xQ.woff2) format('woff2');
17 | unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
18 | }
19 | /* latin-ext */
20 | @font-face {
21 | font-family: 'Playfair Display';
22 | font-style: normal;
23 | font-weight: 700;
24 | font-display: swap;
25 | src: url(../fonts/playfair-display/nuFiD-vYSZviVYUb_rj3ij__anPXDTLYgEM86xRbPQ.woff2) format('woff2');
26 | unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
27 | }
28 | /* latin */
29 | @font-face {
30 | font-family: 'Playfair Display';
31 | font-style: normal;
32 | font-weight: 700;
33 | font-display: swap;
34 | src: url(../fonts/playfair-display/nuFiD-vYSZviVYUb_rj3ij__anPXDTzYgEM86xQ.woff2) format('woff2');
35 | unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
36 | }
37 |
--------------------------------------------------------------------------------
/theme/assets/custom/script/views/tag.js:
--------------------------------------------------------------------------------
1 | import Modes from '../modes.js'
2 | import ViewResources from '../view-resources.js'
3 | import Intros from '../intros.js'
4 | import Intro from '../components/intro.js'
5 | import SearchForm from '../components/search-form.js'
6 | import ActivityChart from '../components/activity-chart.js'
7 | import Pagination from '../components/pagination.js'
8 | import Content from '../components/content.js'
9 | import { scrollToTop, getPages, getPageLinks } from '../helpers.js'
10 |
11 | const resources = new ViewResources({
12 | stylesheets: {
13 | 'tag-stylesheet': 'style/tag-page.css'
14 | }
15 | })
16 |
17 | const render = ({
18 | tag,
19 | links,
20 | pageNumber,
21 | navigatedFrom,
22 | onPaginate
23 | }) => {
24 | const pages = getPages({ links })
25 | const pageLinks = getPageLinks({ pages, pageNumber })
26 |
27 | resources.load()
28 |
29 | Intro.render(Intros.tag, {
30 | links,
31 | tag
32 | })
33 |
34 | SearchForm.setInputValue('')
35 |
36 | if (navigatedFrom.pagination === 'ActivityChart') {
37 | ActivityChart.updateCurrentPage({
38 | pageNumber
39 | })
40 | } else {
41 | ActivityChart.render({
42 | mode: Modes.full,
43 | tag,
44 | links,
45 | pages,
46 | pageNumber,
47 | onPaginate: (payload) => onPaginate({
48 | ...payload,
49 | tag,
50 | paginationType: 'ActivityChart'
51 | })
52 | })
53 | }
54 |
55 | Content.render({
56 | links: pageLinks
57 | })
58 |
59 | Pagination.render({
60 | mode: Modes.full,
61 | tag,
62 | pageNumber,
63 | totalPages: pages.length,
64 | onPaginate
65 | })
66 |
67 | if (navigatedFrom.pagination !== 'ActivityChart') {
68 | scrollToTop()
69 | }
70 | }
71 |
72 | export default {
73 | render
74 | }
75 |
--------------------------------------------------------------------------------
/theme/assets/custom/style/content.css:
--------------------------------------------------------------------------------
1 | .content {
2 | position: relative;
3 | padding: 150px 0 1em;
4 | background: var(--content-bg-color);
5 | border-radius: 3px;
6 | border-bottom: 1px solid var(--content-border-color);
7 | box-shadow: -0.6em 0 2em #00000009;
8 | }
9 |
10 | @media (max-width: 900px) {
11 | .content {
12 | box-shadow: none;
13 | }
14 | }
15 |
16 | [data-region-id="intro"] {
17 | position: absolute;
18 | top: 0;
19 | left: 0;
20 | width: 100%;
21 | padding-top: 2em;
22 | }
23 |
24 | [data-region-id="intro"].entering {
25 | animation: enter-intro var(--transition-duration) forwards;
26 | }
27 |
28 | [data-region-id="intro"].exiting {
29 | animation: exit-intro var(--transition-duration) forwards;
30 | animation-timing-function: cubic-bezier(.66,-0.97,.27,1.55);
31 | }
32 |
33 | @keyframes enter-intro {
34 | from {
35 | opacity: 0;
36 | transform: scale(1.025);
37 | }
38 | to {
39 | opacity: 1;
40 | transform: translateY(1);
41 | }
42 | }
43 |
44 | @keyframes exit-intro {
45 | to {
46 | opacity: 0;
47 | transform: scale(1.05);
48 | }
49 | }
50 |
51 | @media (prefers-color-scheme: dark) {
52 | .content::after {
53 | content: "";
54 | position: absolute;
55 | top: 0;
56 | left: 0;
57 | width: 2dvmin;
58 | height: 50dvmin;
59 | translate: 0 0;
60 | background: radial-gradient(
61 | ellipse 10% 50% at left center,
62 | #fdd3,
63 | transparent
64 | );
65 | opacity: 0;
66 | transition: all .4s var(--logo-bezier);
67 | pointer-events: none;
68 | }
69 |
70 | .header:has(.logo-container:hover, .logo-container:focus-within) ~ .content::after {
71 | opacity: 1;
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/theme/assets/custom/script/views/homepage.js:
--------------------------------------------------------------------------------
1 | import Modes from '../modes.js'
2 | import ViewResources from '../view-resources.js'
3 | import Intros from '../intros.js'
4 | import Intro from '../components/intro.js'
5 | import Header from '../components/header.js'
6 | import SearchForm from '../components/search-form.js'
7 | import ActivityChart from '../components/activity-chart.js'
8 | import Pagination from '../components/pagination.js'
9 | import Content from '../components/content.js'
10 | import { scrollToTop, getPages, getPageLinks } from '../helpers.js'
11 |
12 | const resources = new ViewResources({
13 | stylesheets: {
14 | 'homepage-stylesheet': 'style/homepage.css'
15 | }
16 | })
17 |
18 | const render = ({
19 | links,
20 | pageNumber,
21 | navigatedFrom,
22 | onPaginate
23 | }) => {
24 | const pages = getPages({ links })
25 | const pageLinks = getPageLinks({ pages, pageNumber })
26 |
27 | resources.load()
28 |
29 | SearchForm.setInputValue('')
30 |
31 | Intro.render(Intros.homepage, {
32 | links
33 | })
34 |
35 | if (navigatedFrom.pagination === 'ActivityChart') {
36 | ActivityChart.updateCurrentPage({
37 | pageNumber
38 | })
39 | } else {
40 | ActivityChart.render({
41 | mode: Modes.full,
42 | links,
43 | pages,
44 | pageNumber,
45 | onPaginate: (payload) => onPaginate({
46 | ...payload,
47 | paginationType: 'ActivityChart'
48 | })
49 | })
50 | }
51 |
52 | Content.render({
53 | links: pageLinks
54 | })
55 |
56 | Pagination.render({
57 | mode: Modes.full,
58 | pageNumber,
59 | totalPages: pages.length,
60 | onPaginate
61 | })
62 |
63 | if (navigatedFrom.pagination !== 'ActivityChart') {
64 | scrollToTop()
65 | }
66 |
67 | Header.blur()
68 | }
69 |
70 | export default {
71 | render
72 | }
73 |
--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | 
4 |
5 | My collection of links to blog posts, talks and opinions on art, design and technology.
6 | Website | RSS | Twitter | Tumblr
7 | Set up your own POSSE feed
8 |
9 |
10 |
11 | ## The website
12 |
13 | The website is built with my static site generator [Writ-CMS](https://github.com/scriptype/writ-cms).
14 |
15 |
16 |
17 | ### Features
18 |
19 | - Pogressively enhanced SPA (pure CSS + JS) based on statically generated html
20 | - RSS feed
21 | - JSON Search with RegExp support
22 | - Paginated activity graph
23 | - Tag pages
24 | - Check the lighthouse scores below
25 |
26 |
27 |
28 |
29 |
30 | ### Lighthouse scores
31 |
32 | #### Desktop
33 |
34 |
35 |
36 |
37 |
38 | #### Mobile
39 |
40 |
41 |
42 |
43 | ## Licence
44 |
45 | ```
46 | DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
47 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
48 |
49 | 0. You just DO WHAT THE FUCK YOU WANT TO.
50 | ```
51 |
--------------------------------------------------------------------------------
/src/helpers.js:
--------------------------------------------------------------------------------
1 | const { rm, mkdir } = require('fs/promises')
2 | const cp = require('child_process')
3 | const axios = require('axios')
4 | const cheerio = require('cheerio')
5 |
6 | // Get the last item of an array
7 | const last = array => array[array.length - 1]
8 |
9 | // Returns a reversed copy array, without mutating it.
10 | const reverseNew = array => Object.assign([], array).reverse()
11 |
12 | // Split an array into parts using a limit.
13 | const splitByLimit = (array, limit) =>
14 | array.reduce((result, item, index) => {
15 | if (index % limit === 0) {
16 | return result.concat([[item]])
17 | }
18 | return Object.assign([], result, {
19 | [result.length - 1]: last(result).concat(item)
20 | })
21 | }, [])
22 |
23 | const flatten = arr => [].concat(...arr)
24 |
25 | // Remove and re-create a folder.
26 | const cleanFolder = async (folderPath) => {
27 | await rm(folderPath, { recursive: true })
28 | await mkdir(folderPath)
29 | return Promise.resolve()
30 | }
31 |
32 | // Execute a command and supply output stream, error stream and accept env.
33 | const execute = options => new Promise((resolve, reject) => {
34 | const { cmd, outStream, errStream, env } = options
35 | const ps = cp.exec(cmd, { env })
36 | let errData = ''
37 | ps.stdout.pipe(outStream)
38 | ps.stderr.pipe(errStream)
39 | ps.stderr.on('data', data => {
40 | errData += data
41 | })
42 | ps.on('close', code => {
43 | if (code === 0) {
44 | resolve()
45 | } else {
46 | reject(new Error(`Error executing ${cmd}. Code: ${code}, Error: ${errData}.`))
47 | }
48 | })
49 | })
50 |
51 | const loadPage = async (url) => {
52 | const { data: html } = await axios.get(url)
53 | return cheerio.load(html)
54 | }
55 |
56 | const scrapePageTitle = async (url) => {
57 | const $ = await loadPage(url)
58 | return $('title').text()
59 | }
60 |
61 | const isTweet = url => {
62 | return /twitter.com\/.+\/status\/\d+$/.test(url)
63 | }
64 |
65 | const parseTags = (str) => {
66 | return str
67 | .split(',')
68 | .map(tag => tag.trim())
69 | .filter(tag => tag.length)
70 | }
71 |
72 | module.exports = {
73 | last,
74 | reverseNew,
75 | flatten,
76 | splitByLimit,
77 | cleanFolder,
78 | execute,
79 | loadPage,
80 | scrapePageTitle,
81 | isTweet,
82 | parseTags
83 | }
84 |
--------------------------------------------------------------------------------
/theme/assets/custom/style/search.css:
--------------------------------------------------------------------------------
1 | .feat-search {
2 | --transition-duration: .25s;
3 | position: relative;
4 | height: 2em;
5 | width: 160px;
6 | margin: 0 3em 2em auto;
7 | }
8 |
9 | .feat-search-input {
10 | position: absolute;
11 | top: 0;
12 | left: 0;
13 | width: 100%;
14 | height: 100%;
15 | padding-right: 30px;
16 | border-radius: 5px;
17 | border: 1px solid var(--search-input-border-color);
18 | background: var(--search-input-bg);
19 | color: var(--search-input-color);
20 | box-shadow: inset 0 0px 2em #0001
21 | }
22 |
23 | .feat-search-button {
24 | position: absolute;
25 | top: 50%;
26 | right: 50%;
27 | transform: translate(50%, -50%);
28 | line-height: 1;
29 | color: var(--search-input-label-color);
30 | cursor: pointer;
31 | pointer-events: none;
32 | transition: all var(--transition-duration);
33 | }
34 |
35 | .feat-search-button-text {
36 | display: inline-block;
37 | font-family: helvetica, arial, sans-serif;
38 | font-size: .9em;
39 | transition: all calc(var(--transition-duration));
40 | }
41 |
42 | .feat-search-icon {
43 | width: 18px;
44 | aspect-ratio: 1;
45 | vertical-align: middle;
46 | fill: currentColor;
47 | }
48 |
49 | .feat-search.has-query .feat-search-input,
50 | .feat-search:focus-within .feat-search-input {
51 | box-shadow: none;
52 | }
53 |
54 | .feat-search.has-query .feat-search-button,
55 | .feat-search:focus-within .feat-search-button {
56 | transform: translate(0, -50%) scale(1.1);
57 | right: 2px;
58 | pointer-events: initial;
59 | }
60 |
61 | .feat-search.has-query .feat-search-button-text,
62 | .feat-search:focus-within .feat-search-button-text {
63 | overflow: hidden;
64 | width: 0;
65 | opacity: 0;
66 | }
67 |
68 | .feat-search-results-message {
69 | font-size: 2.5em;
70 | font-weight: 700;
71 | letter-spacing: -0.04em;
72 | word-spacing: 0.05em;
73 | text-align: center;
74 | }
75 |
76 | .feat-search-results-count {
77 | display: block;
78 | margin-bottom: 1em;
79 | color: var(--link-count-color);
80 | font-size: .5em;
81 | font-weight: initial;
82 | word-spacing: initial;
83 | letter-spacing: initial;
84 | }
85 |
86 | @media (prefers-color-scheme: dark) {
87 | .feat-search-input {
88 | transition: all .4s var(--logo-bezier);
89 | }
90 |
91 | :is(.logo-container:hover, .logo-container:focus-within) + #search-form-container .feat-search-input {
92 | border-bottom-color: #685852;
93 | border-top-color: #383832;
94 | box-shadow: inset 0 .75dvmin 2dvmin #0009;
95 | }
96 | }
97 |
--------------------------------------------------------------------------------
/theme/assets/custom/style/overlay-pagination.css:
--------------------------------------------------------------------------------
1 | #overlay-pagination-toggle {
2 | position: sticky;
3 | width: var(--content-width);
4 | text-align: center;
5 | color: royalblue;
6 | opacity: 0;
7 | animation: appear .2s .4s forwards;
8 | }
9 |
10 | #overlay-pagination-container:not([open]) #overlay-pagination-toggle:focus {
11 | box-shadow: inset 0 0 1px 2px royalblue;
12 | }
13 |
14 | @keyframes appear {
15 | to {
16 | opacity: 1;
17 | }
18 | }
19 |
20 | .js-enhanced #overlay-pagination-toggle {
21 | position: absolute;
22 | bottom: 200vh;
23 | z-index: 1;
24 | font-size: 0;
25 | text-align: initial;
26 | width: auto;
27 | }
28 |
29 | .js-enhanced #overlay-pagination-toggle:focus {
30 | font-size: .9em;
31 | height: 1.5em;
32 | top: 0;
33 | bottom: auto;
34 | box-shadow: none;
35 | left: var(--graph-left);
36 | }
37 |
38 | .overlay-pagination {
39 | display: flex;
40 | padding: .5em 0 .5em;
41 | }
42 |
43 | .js-enhanced .overlay-pagination {
44 | position: absolute;
45 | inset: 0;
46 | flex-direction: row-reverse;
47 | padding: 0;
48 | }
49 |
50 | .overlay-pagination-item {
51 | --width: max-content;
52 | width: var(--width);
53 | padding: .75em .5em;
54 | }
55 |
56 | .js-enhanced .overlay-pagination-item {
57 | padding: 0;
58 | }
59 |
60 | .overlay-pagination-item:has(.is-current) {
61 | background: var(--tag-badge-color);
62 | border-radius: .8em;
63 | }
64 |
65 | .js-enhanced .overlay-pagination-item:has(.is-current) {
66 | border-radius: 0;
67 | background: none;
68 | }
69 |
70 | .overlay-pagination-link {
71 | display: block;
72 | width: 100%;
73 | height: 100%;
74 | }
75 |
76 | .js-enhanced .overlay-pagination-link {
77 | /* The quickest way to get mix-blend-mode working in safari */
78 | -webkit-backdrop-filter: blur();
79 | }
80 |
81 | .js-enhanced .overlay-pagination-link {
82 | text-indent: -2000vw;
83 | }
84 |
85 | .js-enhanced .overlay-pagination-link:hover,
86 | .js-enhanced .overlay-pagination-link:focus-visible {
87 | background: var(--chart-hover-page-bg);
88 | mix-blend-mode: var(--chart-hover-blend);
89 | }
90 |
91 | .overlay-pagination-link.faked-active-state {
92 | cursor: grabbing;
93 | }
94 |
95 | .overlay-pagination-link.is-current {
96 | background: var(--tag-badge-color);
97 | }
98 |
99 | .js-enhanced .overlay-pagination-link.is-current {
100 | background: linear-gradient(
101 | to top,
102 | transparent,
103 | var(--chart-active-page) 30%,
104 | var(--chart-active-page) 70%,
105 | transparent
106 | );
107 | mix-blend-mode: multiply;
108 | box-shadow: 0 0 0 1px #ca33;
109 | }
110 |
--------------------------------------------------------------------------------
/misc/stats-03-07-2020.md:
--------------------------------------------------------------------------------
1 | # 3 July 2020 Statistics
2 |
3 | ## Number of links
4 |
5 | ```sh
6 | cat public/data/all.json | jq "." | grep { | wc -l
7 | ```
8 |
9 | 400
10 |
11 | ## Twitter followers
12 |
13 | 84
14 |
15 | ## How many unique tags
16 |
17 | ```sh
18 | cat public/data/all.json | jq ".[].tags" | tr -d [],# | grep "\w" | sort | uniq -c | sort | wc -l
19 |
20 | 491
21 | ```
22 |
23 | ## Top 20 most common tags
24 |
25 | ```sh
26 | cat public/data/all.json | jq ".[].tags" | tr -d [],# | grep "\w" | sort | uniq -c | sort -r | head -n 20
27 |
28 | # And manually:
29 | - Add number of #türkçe (1) to türkçe (35)
30 | - Add number of JavaScript (1) to javascript (26)
31 | - Add number of front-end (3) to frontend (8)
32 |
33 | 36 "türkçe"
34 | 28 "web"
35 | 27 "javascript"
36 | 27 "css"
37 | 25 "programming"
38 | 22 "accessibility"
39 | 20 "design"
40 | 19 "security"
41 | 17 "software"
42 | 13 "science"
43 | 12 "react"
44 | 12 "art"
45 | 11 "frontend"
46 | 11 "environment"
47 | 10 "climate"
48 | 9 "development"
49 | 8 "privacy"
50 | 8 "life"
51 | 8 "learning"
52 | 8 "ai"
53 | ```
54 |
55 | ## Top 20 most common non-programming tags
56 |
57 | ```sh
58 | cat public/data/all.json | jq ".[].tags" | tr -d [],# | grep "\w" | egrep -v '(web|css|javascript|programming|software|react|frontend|ai|development|algorithms|html|vue|unix)' |sort | uniq -c | sort -r | head -n 20
59 |
60 | 36 "türkçe"
61 | 22 "accessibility"
62 | 20 "design"
63 | 19 "security"
64 | 13 "science"
65 | 12 "art"
66 | 11 "environment"
67 | 10 "climate"
68 | 8 "privacy"
69 | 8 "life"
70 | 8 "learning"
71 | 7 "finland"
72 | 7 "astronomy"
73 | 7 "animation"
74 | 6 "tutorial"
75 | 6 "photography"
76 | 6 "google"
77 | 6 "change"
78 | 5 "video"
79 | 5 "story"
80 | ```
81 |
82 | ## Number of tags that are used only once
83 |
84 | ```sh
85 | cat public/data/all.json | jq ".[].tags" | tr -d [],# | grep "\w" | sort | uniq -c | sort | grep " 1 " | wc -l
86 |
87 | 356
88 | ```
89 |
90 | ## Interesting tags that are used only once
91 |
92 | - butthole
93 | - y
94 | - fake
95 | - PCG, CTS, SDLC
96 | - Modules
97 | - Upernavik
98 |
99 | ## Most common tags that are used along with #türkçe (Turkish tags, basically)
100 |
101 | ```sh
102 | cat public/data/all.json | jq -c ".[].tags" | tr -d []# | grep "\w" | grep türkçe | tr ',' '\n' | egrep -v türkçe | sort | uniq -c | sort -r | head -n 10
103 |
104 | 2 "yazılım" (Turkish for software)
105 | 2 "video"
106 | 2 "tasarım" (Turkish for design)
107 | 2 "http"
108 | 2 "deprem" (Turkish for earthquake)
109 |
110 | # After these, there are 88 more tags used only once
111 | ```
112 |
113 | ## Interesting tags used along with #türkçe
114 |
115 | - çürüme (Turkish for corruption)
116 | - yozlaşma (Turkish for degeneration, retrogression)
117 | - nah (A popular hand gesture that has a similar meaning to 🖕)
118 | - adamasmaca (Turkish for Hangman [game])
119 | - dil (Turkish for tongue, language)
120 |
--------------------------------------------------------------------------------
/theme/assets/custom/script/components/activity-chart.js:
--------------------------------------------------------------------------------
1 | import Color from "../vendor/Color.js"
2 | import Modes from '../modes.js'
3 | import { query, transition } from '../helpers.js'
4 | import Canvas from './canvas.js'
5 | import ChartPagination from './chart-pagination.js'
6 | const { min, max } = Math
7 |
8 | const DAY = 1000 * 60 * 60 * 24
9 |
10 | const findScrollContainer = () => query('.graph-container')
11 | const findCanvasContainer = () => query('.canvas-container')
12 | const findCanvas = (container) => query('canvas', container)
13 |
14 | const darkMode = window.matchMedia("(prefers-color-scheme:dark)").matches
15 |
16 | const colors = darkMode ? [
17 | new Color('#a6a6e0'),
18 | new Color('#ffeaac')
19 | ] : [
20 | new Color('#ffeaac'),
21 | new Color('#a6a6e0')
22 | ]
23 |
24 | const renderCanvas = ({ data, dayScale, startDate }) => {
25 | const canvas = Canvas({
26 | height: 260,
27 | data: data.map(p => p.datePublished),
28 | dayScale,
29 | lineWidth: 6,
30 | resolution: 1/12,
31 | startDate,
32 | yearMarkPosition: 'bottom',
33 | yearMarkFont: '30px sans-serif',
34 | padding: {
35 | top: 20,
36 | right: 0,
37 | bottom: 80,
38 | left: 0
39 | },
40 | colors
41 | })
42 |
43 | const canvasContainer = findCanvasContainer()
44 | const existingCanvas = findCanvas(canvasContainer)
45 | if (existingCanvas) {
46 | existingCanvas.remove()
47 | }
48 | if (data.length) {
49 | const $canvas = canvas.draw()
50 | canvasContainer.prepend($canvas)
51 | }
52 | }
53 |
54 | const rerender = () => {
55 | return `
56 |
64 | `
65 | }
66 |
67 | const render = async ({ readonly, mode, tag, links, pages, pageNumber, startDate = new Date(1532221014000), onPaginate }) => {
68 | if (mode === Modes.full) {
69 | findScrollContainer().outerHTML = rerender()
70 | }
71 |
72 | const totalDaysElapsed = (Date.now() - startDate) / DAY
73 | const minDayScale = 1665 / totalDaysElapsed
74 | const maxDayScale = 2
75 | const dayScale = min(maxDayScale, max(minDayScale, links.length / 200))
76 |
77 | renderCanvas({
78 | data: links,
79 | dayScale,
80 | startDate
81 | })
82 |
83 | ChartPagination.render({
84 | readonly,
85 | mode,
86 | tag,
87 | pages,
88 | pageNumber,
89 | dayScale,
90 | onPaginate
91 | })
92 | }
93 |
94 | const updateCurrentPage = ({ pageNumber }) => {
95 | return ChartPagination.updateCurrentPage({ pageNumber })
96 | }
97 |
98 | const remove = async () => {
99 | const container = findScrollContainer()
100 | await transition({
101 | target: container,
102 | className: 'exiting',
103 | duration: 300
104 | })
105 | }
106 |
107 | export default {
108 | render,
109 | updateCurrentPage,
110 | remove
111 | }
112 |
--------------------------------------------------------------------------------
/theme/assets/custom/script/components/content.js:
--------------------------------------------------------------------------------
1 | import Modes from '../modes.js'
2 | import { query, queryAll, truncate, transition } from '../helpers.js'
3 |
4 | const TITLE_CHARS_MAX = 70
5 | const URL_CHARS_MAX = 50
6 | const ENTER_CLASS = 'entering'
7 | const EXIT_CLASS = 'exiting'
8 | const findContainer = () => query('.links')
9 | const findLinkElements = () => queryAll('.link')
10 | const findLinkTitle = (linkElement) => query('.link-title', linkElement)
11 | const findLinkUrl = (linkElement) => query('.link-url', linkElement)
12 | const findLinkTags = (linkElement) => query('.link-tags', linkElement)
13 |
14 | let sampleLinkContainer = null
15 |
16 | const linkTitleInnerHTMLTemplate = (link) => {
17 | return `${truncate(link.title, TITLE_CHARS_MAX, true)}
`
18 | }
19 |
20 | const linkTitleTemplate = (link) => {
21 | return `
22 |
23 | ${linkTitleInnerHTMLTemplate(link)}
24 |
25 | `
26 | }
27 |
28 | const linkTagTemplate = ({ tag, permalink }) => {
29 | return `
30 |
31 | #${tag}
32 |
33 | `
34 | }
35 |
36 | const exitCurrentLinks = () => {
37 | return Promise.all(
38 | Array.from(findLinkElements()).map(linkElement => {
39 | if (linkElement.classList.contains(ENTER_CLASS)) {
40 | linkElement.classList.remove(ENTER_CLASS)
41 | }
42 | return transition({
43 | target: linkElement,
44 | className: EXIT_CLASS,
45 | duration: 250
46 | })
47 | })
48 | )
49 | }
50 |
51 | const enterNewLinks = (links) => {
52 | const container = findContainer()
53 | const fragment = document.createDocumentFragment()
54 | links.forEach((link, i) => {
55 | const linkElement = sampleLinkContainer.cloneNode(true)
56 | linkElement.classList.remove(EXIT_CLASS)
57 | transition({
58 | target: linkElement,
59 | className: ENTER_CLASS,
60 | duration: 300
61 | })
62 | linkElement.style.setProperty('--i', i)
63 | const linkTitle = findLinkTitle(linkElement)
64 | const linkUrl = findLinkUrl(linkElement)
65 | const linkTags = findLinkTags(linkElement)
66 | linkTitle.outerHTML = linkTitleTemplate(link)
67 | linkUrl.textContent = truncate(link.url, URL_CHARS_MAX, true)
68 | linkTags.innerHTML = link.tags.map(linkTagTemplate).join('')
69 | fragment.appendChild(linkElement)
70 | })
71 | container.innerHTML = ''
72 | container.appendChild(fragment)
73 | }
74 |
75 | const hydrate = (links) => {
76 | findLinkElements().forEach((linkElement, i) => {
77 | const link = links[i]
78 | const linkTitle = findLinkTitle(linkElement)
79 | linkTitle.outerHTML = linkTitleTemplate(link)
80 |
81 | const linkUrl = findLinkUrl(linkElement)
82 | linkUrl.textContent = truncate(link.url, URL_CHARS_MAX, true)
83 | })
84 | }
85 |
86 | const render = async ({ mode, links }) => {
87 | if (mode === Modes.hydration) {
88 | return hydrate(links)
89 | }
90 | sampleLinkContainer = (sampleLinkContainer || findLinkElements()[0]).cloneNode(true)
91 | await exitCurrentLinks()
92 | enterNewLinks(links)
93 | }
94 |
95 | export default {
96 | render
97 | }
98 |
--------------------------------------------------------------------------------
/add:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 | require('dotenv').config()
3 | const path = require('path')
4 | const inquirer = require('inquirer')
5 | const links = require('./links')
6 | const { parseTags, scrapePageTitle, isTweet } = require('./src/helpers')
7 | const addLink = require('./src/addLink')
8 | const writ = require('./src/writ')
9 | const git = require('./src/git')
10 | const publish = require('./src/publish')
11 |
12 | let linkIsTweet
13 |
14 | const questions = [
15 | {
16 | type: 'input',
17 | name: 'url',
18 | message: 'URL',
19 | async validate(value) {
20 | if (!value) {
21 | return 'Url is required'
22 | }
23 |
24 | const url = value
25 | linkIsTweet = isTweet(url)
26 |
27 | const alreadyAdded = links.find(link =>
28 | link.url === url || (
29 | link.url.match(/\/$/) && link.url.slice(0, -1) === url
30 | )
31 | )
32 | if (alreadyAdded) {
33 | return 'This link already exists.'
34 | }
35 |
36 | return true
37 | },
38 | filter: String
39 | },
40 | {
41 | type: 'input',
42 | name: 'title',
43 | message: 'Title',
44 | async default({ url }) {
45 | try {
46 | return await scrapePageTitle(url)
47 | } catch (e) {
48 | console.log("Couldn't scrape title, enter manually", e)
49 | return undefined
50 | }
51 | },
52 | validate(value) {
53 | return !!value || 'Title is required'
54 | },
55 | filter: String
56 | },
57 | {
58 | type: 'input',
59 | name: 'RTQuote',
60 | message: 'Retweet quote',
61 | filter: String,
62 | when: () => linkIsTweet
63 | },
64 | {
65 | type: 'input',
66 | name: 'tags',
67 | message: 'Tags (comma separated)',
68 | filter: String
69 | },
70 | {
71 | type: 'confirm',
72 | name: 'isConfirmed',
73 | message: 'Sure?',
74 | default: true
75 | },
76 | {
77 | type: 'confirm',
78 | name: 'shouldPublish',
79 | message: 'Publish now?',
80 | default: true
81 | }
82 | ]
83 |
84 | console.log('Add a new link')
85 |
86 | inquirer.prompt(questions).then(async answers => {
87 | const tags = parseTags(answers.tags)
88 |
89 | const twitterOptions = linkIsTweet ? {
90 | isTweet: true,
91 | retweetQuote: answers.RTQuote
92 | } : {}
93 |
94 | const link = {
95 | url: answers.url,
96 | title: answers.title,
97 | tags,
98 | datePublished: Date.now(),
99 | ...twitterOptions
100 | }
101 |
102 | if (!answers.isConfirmed) {
103 | return console.log('OK, link is not added.')
104 | }
105 |
106 | await addLink({
107 | outputPath: './links.json',
108 | link
109 | })
110 |
111 | await writ.build({
112 | links: links.concat(link),
113 | rootDirectory: '.'
114 | })
115 |
116 | console.log('Added link:', JSON.stringify(link, null, 2))
117 |
118 | // Commit msg with this mark will trigger gh-action publish workflow
119 | const messagePrefix = answers.shouldPublish ? '[publish] ' : ''
120 | await git.commit({
121 | message: `${messagePrefix}Add: ${link.url}`,
122 | paths: ['links.json']
123 | })
124 |
125 | if (answers.shouldPublish) {
126 | await git.push()
127 | }
128 | })
129 |
--------------------------------------------------------------------------------
/misc/router.js:
--------------------------------------------------------------------------------
1 | /*
2 | * During SPA transition, I first thought I would be using a full-fledged router
3 | * and reactively render the app. Not that this router is robust at all. But it
4 | * would do the job.
5 | *
6 | * Soon I realized that's not what I want. I didn't want to run cycles
7 | * unnecessarily re-rendering everything on every route. Maybe I could still
8 | * use this to imperatively manage things, but, at the time, it didn't seem like
9 | * it would have obvious advantages compared to a simpler solution.
10 | *
11 | * Simpler solution being: controller injects callbacks into components
12 | *
13 | * To keep things simpler, I decided to abandon this for now. And implemented
14 | * a dead-simple history wrapper.
15 | *
16 | * If I hadn't, here's how I would be using this router:
17 | *
18 | * const Router = createRouter()
19 | *
20 | * Router
21 | * .on('/', Controller.homepage)
22 | * .on('/page/:pageNumber', Controller.homepage)
23 | * .on('/tags/:tag', Controller.tag)
24 | * .on('/tags/:tag/:pageNumber', Controller.tag)
25 | * .on('#search?q=:query', Controller.search) // This wouldn't work
26 | * .start()
27 | */
28 |
29 | import { fetchPrefix } from './helpers.js'
30 |
31 | const createRouter = () => {
32 | const routes = {}
33 |
34 | const getRouteParts = (route) => {
35 | return route
36 | .replace(/^\//, '')
37 | .replace(/\/$/, '')
38 | .split('/')
39 | }
40 |
41 | const checkMatch = (actualRoute) => {
42 | let routeParams
43 | let matchingRoute
44 | for (const route in routes) {
45 | if (!routes.hasOwnProperty(route)) {
46 | continue
47 | }
48 | const parts = [
49 | getRouteParts(route),
50 | getRouteParts(actualRoute)
51 | ]
52 | if (parts[0].length !== parts[1].length) {
53 | continue
54 | }
55 | routeParams = parts[1].reduce((acc, part, i) => {
56 | if (!part.startsWith(':') && parts[0][i] === part) {
57 | return acc
58 | }
59 | if (parts[0][i].startsWith(':')) {
60 | return {
61 | ...acc,
62 | [parts[0][i].replace(/^:/, '')]: part
63 | }
64 | }
65 | return false
66 | }, {})
67 | if (routeParams) {
68 | matchingRoute = route
69 | break
70 | }
71 | }
72 | return {
73 | route: matchingRoute,
74 | params: routeParams
75 | }
76 | }
77 |
78 | return {
79 | start(initialState = {}) {
80 | window.history.replaceState(initialState, '', document.location.href)
81 | window.addEventListener("popstate", (event) => {
82 | console.log('router popstate event', event)
83 | const route = document.location.pathname.replace(new RegExp('^' + fetchPrefix), '')
84 | const match = checkMatch(route)
85 | if (match) {
86 | console.log('routing to', match.route)
87 | this.navigate(route, event.state)
88 | }
89 | })
90 | console.log('router start')
91 | return this
92 | },
93 |
94 | on(route, handler) {
95 | routes[route] = handler
96 | return this
97 | },
98 |
99 | navigate(route, state) {
100 | window.history.pushState(state, '', route)
101 | const match = checkMatch(route)
102 | if (!match) {
103 | console.info('Unknown route', route)
104 | }
105 | const handler = routes[match.route]
106 | handler(state || match.params)
107 | }
108 | }
109 | }
110 |
111 | export default createRouter
112 |
--------------------------------------------------------------------------------
/theme/assets/custom/script/components/pagination.js:
--------------------------------------------------------------------------------
1 | import Modes from '../modes.js'
2 | import { query, fixLeadingSlashes } from '../helpers.js'
3 |
4 | const NEWER_PAGE_LINK_ID = 'newer-page-link'
5 | const OLDER_PAGE_LINK_ID = 'older-page-link'
6 |
7 | const findContainer = () => query('.pagination-buttons')
8 | const findNewerPageLink = (container) => query(`#${NEWER_PAGE_LINK_ID}`, container)
9 | const findOlderPageLink = (container) => query(`#${OLDER_PAGE_LINK_ID}`, container)
10 |
11 | const getBaseUrl = (tag) => {
12 | const urlParts = [window.permalinkPrefix]
13 | if (tag) {
14 | urlParts.push('tags', tag)
15 | }
16 | return fixLeadingSlashes(urlParts.filter(Boolean).join('/'))
17 | }
18 |
19 | const getPageNumbers = (pageNumber, totalPages) => {
20 | return {
21 | newer: pageNumber ? (pageNumber >= 2 && pageNumber - 1) : undefined,
22 | older: (pageNumber < totalPages) && pageNumber + (pageNumber === 0 ? 2 : 1)
23 | }
24 | }
25 |
26 | const getPageNumberFromLink = (link) => {
27 | const match = link.href.match(/page\/(\d+)/)
28 | if (match) {
29 | return parseInt(match[1], 10)
30 | }
31 | return 0
32 | }
33 |
34 | const containerTemplate = ({ baseUrl, pageNumbers }) => {
35 | const newerPageUrl = pageNumbers.newer === 1 ?
36 | baseUrl :
37 | fixLeadingSlashes(`${baseUrl}/page/${pageNumbers.newer}`)
38 |
39 | const olderPageUrl = fixLeadingSlashes(`${baseUrl}/page/${pageNumbers.older}`)
40 |
41 | return `
42 |
46 | `
47 | }
48 |
49 | const renderLinks = ({ tag, pageNumbers }) => {
50 | const baseUrl = getBaseUrl(tag)
51 |
52 | const container = findContainer()
53 | container.innerHTML = containerTemplate({
54 | baseUrl,
55 | pageNumbers
56 | })
57 | return container
58 | }
59 |
60 | const attachClickHandlers = ({ container, onClick }) => {
61 | const links = [
62 | findNewerPageLink(container),
63 | findOlderPageLink(container)
64 | ]
65 |
66 | links.forEach((link, i) => {
67 | if (!link) {
68 | return
69 | }
70 | link.addEventListener('click', event => {
71 | event.preventDefault()
72 | const pageNumber = getPageNumberFromLink(link)
73 | onClick({
74 | pageNumber: getPageNumberFromLink(link)
75 | })
76 | })
77 | })
78 | }
79 |
80 | const softRender = ({ tag, onPaginate }) => {
81 | const linksContainer = findContainer()
82 |
83 | attachClickHandlers({
84 | container: linksContainer,
85 | onClick: (payload) => onPaginate({
86 | ...payload,
87 | tag
88 | })
89 | })
90 | }
91 |
92 | const fullRender = ({ tag, pageNumber, totalPages, onPaginate }) => {
93 | const pageNumbers = getPageNumbers(pageNumber || 1, totalPages)
94 |
95 | const linksContainer = renderLinks({
96 | tag,
97 | pageNumbers
98 | })
99 |
100 | attachClickHandlers({
101 | container: linksContainer,
102 | onClick: (payload) => onPaginate({
103 | ...payload,
104 | tag
105 | })
106 | })
107 | }
108 |
109 | const render = ({ mode, ...rest }) => {
110 | if (mode === Modes.hydration) {
111 | return softRender(rest)
112 | }
113 | return fullRender(rest)
114 | }
115 |
116 | const remove = () => {
117 | findContainer().innerHTML = ''
118 | }
119 |
120 | export default {
121 | render,
122 | remove
123 | }
124 |
--------------------------------------------------------------------------------
/theme/assets/custom/script/helpers.js:
--------------------------------------------------------------------------------
1 | export const truncate = (text, limit, ellipsis) => {
2 | return text.length > limit ?
3 | `${ text.slice(0, limit).trim() }${ ellipsis ? '...' : '' }` :
4 | text
5 | }
6 |
7 | export const region = (id, parent = document) => {
8 | return parent.querySelector(`[data-region-id="${id}"]`)
9 | }
10 |
11 | export const transition = ({ target, className, duration, crossFade }) => {
12 | target.classList.add(className)
13 | target.style.setProperty('--transition-duration', duration + 'ms')
14 | if (crossFade) {
15 | setTimeout(() => {
16 | target.classList.remove(className)
17 | }, duration)
18 | return new Promise(r => {
19 | setTimeout(r, duration - duration * crossFade)
20 | })
21 | }
22 | return new Promise(r => {
23 | setTimeout(() => {
24 | target.classList.remove(className)
25 | r()
26 | }, duration)
27 | })
28 | }
29 |
30 | export const debounce = (fn, timeout) => {
31 | let waiting
32 | return (...args) => {
33 | if (waiting) {
34 | return
35 | }
36 | waiting = true
37 | setTimeout(() => waiting = false, timeout)
38 | return fn(...args)
39 | }
40 | }
41 |
42 | export const fetchPrefix = window.permalinkPrefix === '/' ?
43 | '' :
44 | window.permalinkPrefix
45 |
46 | export const fetchHTML = (path) => {
47 | const url = [fetchPrefix, path.replace(/^\//, '')].join('/')
48 | return fetch(url).then(r => r.text())
49 | }
50 |
51 | export const HTMLToDOM = (html) => {
52 | const el = document.createElement('div')
53 | el.innerHTML = html
54 | return el.firstElementChild
55 | }
56 |
57 | export const last = (arr) => arr[arr.length - 1]
58 |
59 | export const chunk = (array, size) => {
60 | return array.reduce((result, item, index) => {
61 | if (index % size === 0) {
62 | return result.concat([[item]])
63 | }
64 | return Object.assign([], result, {
65 | [result.length - 1]: last(result).concat(item)
66 | })
67 | }, [])
68 | }
69 |
70 | export const query = (selector, parent = document) => {
71 | return parent.querySelector(selector)
72 | }
73 |
74 | export const queryAll = (selector, parent = document) => {
75 | return parent.querySelectorAll(selector)
76 | }
77 |
78 | export const fixLeadingSlashes = (str) => str.replace(/^\/\//, '/')
79 |
80 | export const route = (url, state) => {
81 | window.history.pushState(state, '', `${fetchPrefix}${url}`)
82 | }
83 |
84 | export const getTagFromUrl = () => {
85 | const urlParts = window.location.pathname
86 | .replace(window.permalinkPrefix, '')
87 | .split('/')
88 | .filter(Boolean)
89 | let tag
90 | if (urlParts.includes('tags')) {
91 | tag = urlParts[1]
92 | }
93 | return tag
94 | }
95 |
96 | export const getPageNumberFromUrl = () => {
97 | const match = document.location.pathname.match(/page\/(\d+)/)
98 | return match && Number(match[1])
99 | }
100 |
101 | export const getSearchQueryFromUrl = () => {
102 | const match = document.location.search.match(/\?search=(.+)/)
103 | return match && decodeURI(match[1])
104 | }
105 |
106 | export const scrollToTop = () => {
107 | document.documentElement.scrollTop = 0
108 | }
109 |
110 | export const getPages = ({ links, linksPerPage = 15 }) => {
111 | return chunk(links, linksPerPage)
112 | }
113 |
114 | export const getPageLinks = ({ links, pages, pageNumber, linksPerPage = 15 }) => {
115 | const _pages = pages || getPages({ links, linksPerPage })
116 |
117 | return pageNumber > 1 ?
118 | _pages[pageNumber - 1] :
119 | _pages[0]
120 | }
121 |
--------------------------------------------------------------------------------
/theme/templates/helpers.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | pageTitle() {
3 | if (this.page === 'post') {
4 | return `${this.post.title} / ${this.settings.site.title}`
5 | }
6 | if (this.page === 'subpage') {
7 | return `${this.subpage.title} / ${this.settings.site.title}`
8 | }
9 | if (this.page === 'category') {
10 | return `${this.category.name} / ${this.settings.site.title}`
11 | }
12 | if (this.page === 'tag') {
13 | return `#${this.tag.tag} / ${this.settings.site.title}`
14 | }
15 | if (this.page === 'homepage' && this.homepage.title) {
16 | return `${this.homepage.title} / ${this.settings.site.title}`
17 | }
18 | return `${this.settings.site.title}`
19 | },
20 |
21 | pageDescription() {
22 | if (this.page === 'tags') {
23 | return 'What have I been up to since 2018?'
24 | }
25 | return this.settings.site.description
26 | },
27 |
28 | assetsPath() {
29 | const { permalinkPrefix, assetsDirectory } = this.settings
30 | const prefix = permalinkPrefix === '/' ? '' : permalinkPrefix
31 | return prefix + '/' + assetsDirectory
32 | },
33 |
34 | featuredTags() {
35 | const { featuredTags } = this.settings
36 | return this.tags.filter((tag) => featuredTags.includes(tag.tag))
37 | },
38 |
39 | tagImportance(allTags, tag) {
40 | return tag.posts.length / allTags[0].posts.length
41 | },
42 |
43 | truncate (text, limit, ellipsis) {
44 | return text.length > limit ?
45 | `${ text.slice(0, limit).trim() }${ ellipsis ? '...' : '' }` :
46 | text
47 | },
48 |
49 | region(name) {
50 | return ` data-region-id="${name}" `
51 | },
52 |
53 | serialize(obj) {
54 | return JSON.stringify(obj)
55 | },
56 |
57 | pluck(arr, fieldName) {
58 | return arr.map((obj) => obj[fieldName])
59 | },
60 |
61 | seeMore() {
62 | return ''
63 | },
64 |
65 | map(...keyValues) {
66 | return keyValues.reduce((result, keyOrValue, index) => {
67 | if (index % 2 > 0) {
68 | return result
69 | }
70 | return {
71 | ...result,
72 | [keyOrValue]: keyValues[index + 1]
73 | }
74 | }, {})
75 | },
76 |
77 | mention(permalink, options) {
78 | const pattern = new RegExp('^(|\/)' + permalink)
79 | const entry = [
80 | this.homepage,
81 | ...this.posts,
82 | ...this.categories,
83 | ...this.subpages
84 | ].find(e => pattern.test(e.permalink))
85 | if (options.fn) {
86 | return options.fn(entry)
87 | }
88 | return `${entry.title}`
89 | },
90 |
91 | filterPostsByType(type) {
92 | return this.posts.filter(p => p.type === type)
93 | },
94 |
95 | is(value1, value2) {
96 | return value1 === value2
97 | },
98 |
99 | isNot(value1, value2) {
100 | return value1 !== value2
101 | },
102 |
103 | not(value) {
104 | return !value
105 | },
106 |
107 | isEnabled(featureName) {
108 | return this.settings[featureName] !== 'off'
109 | },
110 |
111 | isStartMode() {
112 | return this.settings.mode === 'start'
113 | },
114 |
115 | isBuildMode() {
116 | return this.settings.mode === 'build'
117 | },
118 |
119 | isPostPage() {
120 | return this.page === 'post'
121 | },
122 |
123 | isSubPage() {
124 | return this.page === 'subpage'
125 | },
126 |
127 | isHomePage() {
128 | return this.page === 'homepage'
129 | },
130 |
131 | isCategoryPage() {
132 | return this.page === 'category'
133 | },
134 |
135 | isTagPage() {
136 | return this.page === 'tag'
137 | },
138 |
139 | isTagsPage() {
140 | return this.page === 'tags'
141 | }
142 | }
143 |
--------------------------------------------------------------------------------
/theme/assets/custom/script/components/search-form.js:
--------------------------------------------------------------------------------
1 | import Dictionary from '../../../common/dictionary.js'
2 | import { query } from '../helpers.js'
3 |
4 | const findContainer = () => query('#search-form-container')
5 | const findForm = () => query('.feat-search')
6 | const findInput = () => query('#feat-search')
7 |
8 | const loadResources = () => {
9 | return new Promise(resolve => {
10 | const stylesheet = document.createElement('link')
11 | stylesheet.rel = 'stylesheet'
12 | stylesheet.href = `${window.assetsPath}/custom/style/search.css`
13 | stylesheet.onload = resolve
14 | document.head.appendChild(stylesheet)
15 | })
16 | }
17 |
18 | const searchIconTemplate = () => `
19 |
24 | `
25 |
26 | const searchFormTemplate = () => `
27 |
44 | `
45 |
46 | const renderSearchForm = () => {
47 | const container = findContainer()
48 | container.innerHTML = `
49 | ${searchFormTemplate()}
50 | ${searchIconTemplate()}
51 | `
52 | return container.querySelector('form')
53 | }
54 |
55 | const addFormStateClassNames = () => {
56 | const form = findForm()
57 | const input = findInput()
58 | form.classList.toggle('has-query', input.value.trim())
59 | }
60 |
61 | const attachEventListeners = ({ searchForm, onSearch, onReset }) => {
62 | const searchInput = searchForm.querySelector('input[type="search"]')
63 | addFormStateClassNames()
64 | searchInput.addEventListener('change', () => {
65 | addFormStateClassNames()
66 | })
67 | searchForm.addEventListener('submit', event => {
68 | event.preventDefault()
69 | const formData = new FormData(event.target)
70 | const query = formData.get('search')
71 | if (!query.trim()) {
72 | return onReset()
73 | }
74 | onSearch({ query })
75 | })
76 | }
77 |
78 | const render = async ({ onSearch, onReset }) => {
79 | try {
80 | await loadResources()
81 | } catch (e) {
82 | console.log('failed loading resources for search-form', e)
83 | }
84 | return new Promise(resolve => {
85 | Dictionary.ready(async () => {
86 | const searchForm = renderSearchForm()
87 | attachEventListeners({
88 | searchForm,
89 | onSearch,
90 | onReset
91 | })
92 | resolve(searchForm)
93 | })
94 | })
95 | }
96 |
97 | const empty = () => {
98 | findInput().value = ''
99 | addFormStateClassNames()
100 | }
101 |
102 | const setInputValue = (value) => {
103 | findInput().value = value
104 | addFormStateClassNames()
105 | }
106 |
107 | export default {
108 | render,
109 | empty,
110 | setInputValue
111 | }
112 |
--------------------------------------------------------------------------------
/theme/assets/custom/script/components/tags-list.js:
--------------------------------------------------------------------------------
1 | import { query, queryAll } from '../helpers.js'
2 |
3 | import { reportScroll } from "../vendor/kicss.js"
4 |
5 | const TAG_LINK_CLASS = 'tags-list-item-link'
6 | const findOutsideRegions = () => queryAll('header.header, main.content, footer.footer')
7 | const findContainer = () => query('.tags-list-container')
8 | const findToggle = (container = findContainer()) => query('.tags-list-toggle-label', container)
9 | const findList = (container) => query('.tags-list', container)
10 | const findTagLinks = (list) => queryAll('.' + TAG_LINK_CLASS, list)
11 | const isScreenSmall = () => window.innerWidth <= 900
12 |
13 | const findClosestTagLink = (element, recursion) => {
14 | if (element.classList.contains(TAG_LINK_CLASS)) {
15 | return element
16 | }
17 | if (recursion < 1) {
18 | return undefined
19 | }
20 | return findClosestTagLink(element.parentElement, recursion - 1)
21 | }
22 |
23 | const toggleLabels = {
24 | clickToOpen: 'Show topics',
25 | clickToClose: 'Hide topics'
26 | }
27 |
28 | const autoExpand = () => {
29 | if (isScreenSmall() && findContainer().hasAttribute('open')) {
30 | collapse()
31 | }
32 | if (!isScreenSmall()) {
33 | expand()
34 | }
35 | }
36 |
37 | const collapse = () => {
38 | const container = findContainer()
39 | const outsideRegions = findOutsideRegions()
40 | const toggleBtn = findToggle(container)
41 | container.removeAttribute('open')
42 | toggleBtn.textContent = toggleLabels.clickToOpen
43 | document.body.classList.remove('tags-list-is-open')
44 | outsideRegions.forEach(region => {
45 | region.removeAttribute('inert')
46 | })
47 | }
48 |
49 | const expand = () => {
50 | const container = findContainer()
51 | const outsideRegions = findOutsideRegions()
52 | const toggleBtn = findToggle(container)
53 | container.setAttribute('open', true)
54 | toggleBtn.textContent = toggleLabels.clickToClose
55 | toggleBtn.textContent = toggleLabels.clickToClose
56 | document.body.classList.add('tags-list-is-open')
57 | if (isScreenSmall()) {
58 | outsideRegions.forEach(region => region.inert = true)
59 | }
60 | }
61 |
62 | const attachToggleClickHandler = () => {
63 | const toggleBtn = findToggle()
64 |
65 | toggleBtn.addEventListener('click', () => {
66 | // Immediately changing toggle text causes mobile safari not to expand the
67 | // details element at all.
68 | setTimeout(() => {
69 | if (toggleBtn.textContent === toggleLabels.clickToClose) {
70 | collapse()
71 | } else {
72 | expand()
73 | }
74 | }, 200)
75 | })
76 | }
77 |
78 | const attachTagClickHandler = (cb) => {
79 | const container = findContainer()
80 | const list = findList(container)
81 | list.addEventListener('click', event => {
82 | const closestLink = findClosestTagLink(event.target, 2)
83 | if (closestLink) {
84 | event.preventDefault()
85 | cb({
86 | tag: closestLink.dataset.tag
87 | })
88 | if (isScreenSmall()) {
89 | collapse()
90 | }
91 | }
92 | })
93 | }
94 |
95 | const attachResizeHandler = () => {
96 | window.addEventListener('resize', autoExpand)
97 | window.addEventListener('orientationchange', autoExpand)
98 | }
99 |
100 | const attachScrollHandler = () => {
101 | const container = findContainer()
102 | const list = findList(container)
103 | list.addEventListener('scroll', reportScroll('--tags-list-scroll'))
104 | }
105 |
106 |
107 | const render = ({ onClickTag }) => {
108 | attachToggleClickHandler()
109 | attachTagClickHandler(onClickTag)
110 | attachResizeHandler()
111 | attachScrollHandler()
112 | autoExpand()
113 | }
114 |
115 | export default {
116 | render
117 | }
118 |
--------------------------------------------------------------------------------
/theme/assets/custom/style/links.css:
--------------------------------------------------------------------------------
1 | .links {
2 | perspective: 500vh;
3 | }
4 |
5 | .link {
6 | --enter-rotate-x: 30deg;
7 | --enter-rotate-y: 0deg;
8 | --enter-shift-x: 0%;
9 | --enter-shift-y: 4%;
10 | --enter-scale: .95, .95;
11 | --enter-duration: .2s;
12 | --enter-stagger: 50ms;
13 | --exit-shift-x: 0%;
14 | --exit-shift-y: 2%;
15 | --exit-scale: .97, .97;
16 | --exit-duration: .15s;
17 | --exit-stagger: 30ms;
18 | position: relative;
19 | display: flex;
20 | flex-direction: column;
21 | justify-content: center;
22 | padding: .8em 2em 1.1em;
23 | font-size: 1.8em;
24 | font-weight: 100;
25 | transition: all .3s;
26 | }
27 |
28 | @media (max-width: 640px) {
29 | .link {
30 | padding: .7em 1em 1em;
31 | }
32 | }
33 |
34 | .link:hover,
35 | .link:focus-within {
36 | background: var(--link-hover-bg);
37 | }
38 |
39 | .link:active {
40 | background: var(--link-active-bg);
41 | box-shadow: none;
42 | }
43 |
44 | .link.entering,
45 | .link.exiting {
46 | transform-origin: top center;
47 | transform-style: preserve-3d;
48 | animation-fill-mode: forwards;
49 | }
50 |
51 | .link.entering {
52 | opacity: 0;
53 | transform:
54 | translate(
55 | calc(var(--enter-shift-x) * -1),
56 | calc(var(--enter-shift-y) * -1)
57 | )
58 | scale(var(--enter-scale))
59 | rotateX(calc(var(--enter-rotate-x) * -1))
60 | rotateY(calc(var(--enter-rotate-y) * -1));
61 | animation-name: link-enter;
62 | animation-duration: var(--enter-duration);
63 | animation-delay: calc(var(--i) * var(--enter-stagger));
64 | }
65 |
66 | @keyframes link-enter {
67 | to {
68 | transform:
69 | translate(0, 0)
70 | scale(1, 1)
71 | rotateX(0)
72 | rotateY(0);
73 | opacity: 1;
74 | }
75 | }
76 |
77 | .link.exiting {
78 | transform-origin: bottom center;
79 | animation-name: link-exit;
80 | animation-duration: var(--exit-duration);
81 | animation-delay: calc(var(--i) * var(--exit-stagger));
82 | }
83 |
84 | @keyframes link-exit {
85 | to {
86 | transform:
87 | translate(
88 | var(--exit-shift-x),
89 | var(--exit-shift-y)
90 | )
91 | scale(var(--exit-scale));
92 | opacity: 0;
93 | }
94 | }
95 |
96 | .link-title {
97 | color: var(--link-color);
98 | text-decoration: none;
99 | }
100 |
101 | .link-title::after {
102 | content: "";
103 | display: block;
104 | position: absolute;
105 | inset: 0;
106 | }
107 |
108 | .link-title {
109 | line-height: 1.4;
110 | }
111 |
112 | .link-url {
113 | margin-top: .25rem;
114 | font-size: .6em;
115 | max-width: 100%;
116 | overflow: hidden;
117 | text-overflow: ellipsis;
118 | color: var(--primary-color);
119 | }
120 |
121 | .link-tags {
122 | display: flex;
123 | flex-wrap: wrap;
124 | margin-top: .35rem;
125 | margin-bottom: -.2em;
126 | }
127 |
128 | .link-tag-container {
129 | font-size: 0.45em;
130 | margin-top: 0.3em;
131 | margin-bottom: 0.25em;
132 | }
133 |
134 | .link-tag {
135 | position: relative;
136 | padding: 0.3em .5em .25em 0;
137 | letter-spacing: .07em;
138 | text-transform: uppercase;
139 | text-decoration: none;
140 | color: var(--link-tag-color);
141 | box-shadow: 1px 20px 0 -15px #aaaa;
142 | transition: all .3s;
143 | }
144 |
145 | .link-tag:hover,
146 | .link-tag:focus {
147 | color: var(--link-tag-hover-color);
148 | box-shadow: 1px 11.5px 0 -10px currentColor;
149 | text-shadow: 0 0 1px var(--link-tag-hover-stroke-color);
150 | }
151 |
--------------------------------------------------------------------------------
/theme/assets/custom/style/logo.css:
--------------------------------------------------------------------------------
1 | .logo-container {
2 | display: block;
3 | width: 256px;
4 | height: 256px;
5 | background: var(--logo-bg);
6 | border-radius: 100%;
7 | border-bottom: 1px solid #ddd;
8 | overflow: hidden;
9 | transform: scale(.75, .75);
10 | transition: all .4s var(--logo-bezier);
11 | box-shadow: var(--logo-box-shadow);
12 | }
13 |
14 | .shine {
15 | content: "";
16 | position: absolute;
17 | top: 0;
18 | left: 0;
19 | width: 200%;
20 | height: 200%;
21 | border-radius: 100%;
22 | background: rgba(255, 255, 255, .5);
23 | opacity: 0;
24 | transform:
25 | rotate(-45deg)
26 | translateY(-110%);
27 | transition: inherit;
28 | }
29 |
30 | .logo-container:hover,
31 | .logo-container:focus {
32 | background: var(--logo-hover-bg);
33 | transform: var(--logo-hover-transform);
34 | box-shadow: var(--logo-hover-box-shadow);
35 | transform-style: preserve-3d;
36 | }
37 |
38 | .logo-container:hover .shine,
39 | .logo-container:focus .shine {
40 | opacity: var(--logo-hover-shine-opacity);
41 | transform:
42 | rotate(-45deg)
43 | translateY(-85%)
44 | }
45 |
46 | .logo-container:focus {
47 | outline: none;
48 | }
49 |
50 | .logo-container:active {
51 | background: #fff;
52 | transform: scale(.75, .75);
53 | }
54 |
55 | .logo-container:active .shine {
56 | opacity: 0;
57 | transform:
58 | rotate(-45deg)
59 | translateY(-110%)
60 | }
61 |
62 | .logo {
63 | --color: var(--logo-fg);
64 | position: absolute;
65 | max-width: 100%;
66 | border-radius: inherit;
67 | transform:
68 | scale(0.756)
69 | translate(.5%, -58.125%);
70 | height: 2200px;
71 | overflow: visible;
72 | stroke: var(--color);
73 | stroke-width: 1.5px;
74 | }
75 |
76 | @media (prefers-color-scheme: dark) {
77 | .logo-container {
78 | perspective: 500px;
79 | }
80 |
81 | .shine {
82 | top: auto;
83 | bottom: 0;
84 | left: 50%;
85 | width: 105%;
86 | height: 100%;
87 | transform-style: preserve-3d;
88 | transform-origin: center 60%;
89 | translate: -50% 5%;
90 | clip-path: polygon( 0 50%, 100% 50%, 100% 100%, 0 100%);
91 | background: linear-gradient(
92 | to bottom,
93 | transparent 40%,
94 | #fdee
95 | );
96 | background-size: 100% 300%;
97 | background-position: top center;
98 | }
99 |
100 | .logo-container:hover,
101 | .logo-container:focus {
102 | background: var(--logo-hover-bg);
103 | transform: scale(.75, .75);
104 | box-shadow: var(--logo-box-shadow);
105 | transform-style: preserve-3d;
106 | border-bottom: 30px solid #fed;
107 | }
108 |
109 | .logo-container:hover .shine,
110 | .logo-container:focus .shine {
111 | transform: none;
112 | opacity: 1;
113 | rotate: x 45deg;
114 | background-position: center 30%;
115 | }
116 |
117 | .logo-container:active .shine {
118 | transform: none;
119 | }
120 |
121 | .logo-bottom-part {
122 | transform-style: preserve-3d;
123 | transform-origin: 0% center;
124 | transition: all .4s cubic-bezier(0.68, -0.55, 0.27, 1.55);
125 | }
126 |
127 | :nth-child(1 of .logo) .logo-bottom-part {
128 | opacity: 0;
129 | }
130 |
131 | :nth-child(2 of .logo) .logo-top-part {
132 | opacity: 0;
133 | }
134 |
135 | .logo-container:hover :nth-child(2 of .logo) .logo-bottom-part,
136 | .logo-container:focus :nth-child(2 of .logo) .logo-bottom-part {
137 | background: var(--logo-hover-bg);
138 | rotate: x 45deg;
139 | border-bottom: 30px solid #fed;
140 | }
141 | }
142 |
--------------------------------------------------------------------------------
/theme/assets/custom/script/components/canvas.js:
--------------------------------------------------------------------------------
1 | const { floor, max } = Math
2 |
3 | const DAY = 1000 * 60 * 60 * 24
4 |
5 | const createCanvas = () => {
6 | const canvas = document.createElement('canvas')
7 | canvas.classList.add('graph')
8 | canvas.id = 'graph'
9 | return canvas
10 | }
11 |
12 | export default (options) => {
13 | const {
14 | data,
15 | height,
16 | dayScale,
17 | resolution,
18 | colors,
19 | lineWidth,
20 | padding,
21 | startDate,
22 | yearMarkPosition,
23 | yearMarkFont
24 | } = options
25 |
26 | const totalDaysElapsed = (Date.now() - startDate) / DAY
27 | const width = totalDaysElapsed * dayScale
28 | const outlierScale = 0.8
29 | const innerWidth = width - (padding.left + padding.right)
30 | const innerHeight = height - (padding.top + padding.bottom)
31 |
32 | const getHeightMap = ({ timeResolution }) => {
33 | const heightMap = []
34 | let tallestPoint = 0
35 | for (let i = 0; i < data.length - 1; i ++) {
36 | const timeElapsed = data[i] - data[data.length - 1]
37 | let dataPoint = floor(timeElapsed / timeResolution)
38 | heightMap[dataPoint] = (heightMap[dataPoint] || 0) + 1
39 | tallestPoint = max(tallestPoint, heightMap[dataPoint])
40 | }
41 | const timeElapsedUntilNow = Date.now() - data[data.length - 1]
42 | const imaginaryDataPoint = timeElapsedUntilNow * Math.max(1.15, Math.min(50, 20 / data.length))
43 | const lastDataPoint = floor(imaginaryDataPoint / timeResolution)
44 | heightMap[lastDataPoint] = heightMap[lastDataPoint] || 0
45 | for (let i = 0; i < heightMap.length; i++) {
46 | heightMap[i] = heightMap[i] || 0
47 | if (heightMap[i] === tallestPoint) {
48 | heightMap[i] *= outlierScale
49 | tallestPoint *= outlierScale
50 | }
51 | }
52 | return {
53 | heightMap,
54 | tallestPoint
55 | }
56 | }
57 |
58 | const drawLine = ({ ctx, heightMap, tallestPoint }) => {
59 | const gradient = colors[0].range(colors[1], {
60 | space: "lch",
61 | outputSpace: "srgb"
62 | })
63 | ctx.beginPath()
64 | for (let i = 0; i < heightMap.length - 1; i++) {
65 | const progress = i / (heightMap.length - 1)
66 | const x = padding.left + i / resolution
67 | const y = padding.top + innerHeight - (heightMap[i] / tallestPoint) * innerHeight
68 | ctx.lineCap = 'round'
69 | ctx.lineWidth = lineWidth
70 | ctx.lineTo(x, y)
71 | ctx.strokeStyle = gradient(progress)
72 | ctx.stroke()
73 | ctx.closePath()
74 | ctx.beginPath()
75 | ctx.moveTo(x, y)
76 | }
77 | ctx.closePath()
78 | }
79 |
80 | const drawYearMarks = ({ ctx }) => {
81 | let firstVisibleYear = startDate.getFullYear() + 1
82 | const firstVisibleYearDate = new Date(0)
83 | firstVisibleYearDate.setYear(firstVisibleYear)
84 | const timeToFirstYear = firstVisibleYearDate - startDate
85 | const startOffset = timeToFirstYear / DAY * dayScale
86 | const yearLength = floor(365 * dayScale)
87 | ctx.font = yearMarkFont
88 | ctx.fillStyle = '#666'
89 | ctx.textAlign = 'center'
90 | ctx.textBaseline = 'middle'
91 | for (let i = 0; i < innerWidth + startOffset; i++) {
92 | if (i > startOffset && i % yearLength === 0) {
93 | const x = padding.left + i - startOffset
94 | const y = yearMarkPosition === 'bottom' ? height - 40 : 20
95 | ctx.fillText(firstVisibleYear, x, y)
96 | firstVisibleYear++
97 | }
98 | }
99 | }
100 |
101 | const draw = () => {
102 | const $el = createCanvas()
103 | $el.width = width
104 | $el.height = height
105 | const ctx = $el.getContext('2d')
106 | const timeResolution = DAY * (totalDaysElapsed / innerWidth / resolution)
107 | const { heightMap, tallestPoint } = getHeightMap({ timeResolution })
108 | drawLine({
109 | ctx,
110 | heightMap,
111 | tallestPoint
112 | })
113 | drawYearMarks({ ctx })
114 | return $el
115 | }
116 |
117 | return {
118 | draw
119 | }
120 | }
121 |
--------------------------------------------------------------------------------
/misc/setup.md:
--------------------------------------------------------------------------------
1 | # Set up your own [POSSE](https://indieweb.org/POSSE) feed
2 |
3 | Reads Feed helps you save and share your links/bookmarks without relying on external services.
4 |
5 | Your links are indexed and stored locally in your computer until you decide to push to GitHub.
6 |
7 | Once you push,
8 |
9 | - A static site, built with [Writ-CMS](https://github.com/scriptype/writ-cms), will be deployed to
10 | GitHub pages
11 | - The static site will enable subscription via RSS
12 | - Links will be shared on your Twitter and Tumblr accounts
13 |
14 | ## Usage overview
15 |
16 | - Add some links to a json file, by using `./add` CLI helper
17 | - To preview your feed, run `npm start`
18 | - To finalize, run `npm run build`
19 | - Make it public by pushing to GitHub (`./add` will ask doing it for you)
20 |
21 | **Note: Publishing multiple/batch links at once is not tested.**
22 |
23 | ## Install
24 |
25 | Fork this repository to your account, and then:
26 |
27 | ```sh
28 | # Clone
29 | git clone git@github.com:/feed.git
30 | cd feed
31 |
32 | # You may optionally want to clean up git history at this point
33 |
34 | # Install dependencies
35 | npm i
36 |
37 | # Reset the links
38 | rm links.json
39 | echo "[]" > links.json
40 |
41 | # Add a link
42 | ./add
43 | ```
44 |
45 | ## Social media APIs
46 |
47 | You will need to register OAUTH applications on
48 | [Twitter](https://developer.twitter.com/en/docs/basics/getting-started#get-started-app)
49 | and [Tumblr](https://www.tumblr.com/docs/en/api/v2) for social sharing to work.
50 |
51 | Then go to your repository settings and add the secrets as repository secrets:
52 |
53 |
54 |
55 | **The secrets are _secrets_, so make sure never to make them public**
56 |
57 | ## The look & feel
58 |
59 | Adding a link looks like this:
60 |
61 | https://user-images.githubusercontent.com/5516876/207561991-00259d33-9ee2-424a-9e3c-26262a3dcb4d.mov
62 |
63 | > Update: Now it will also confirm whether to push changes (and trigger publish) or not.
64 |
65 | Run `./add` in the root of the project.
66 |
67 | This will ask:
68 | - Url
69 | - Title
70 | - Retweet quote (if link is a tweet)
71 | - Tags
72 | - Whether to publish now
73 |
74 | If you choose to publish now, changes will be pushed to GitHub, triggering the workflows for social sharing and deploying to GitHub Pages.
75 |
76 | And let's see the results.
77 |
78 | ### Twitter
79 |
80 | Hashtags are omitted on Twitter to avoid the trashy/spammy look.
81 |
82 |
83 |
84 | ### Tumblr
85 |
86 |
87 |
88 | ### Static site
89 |
90 |
91 |
92 | ## Automagical retweeting
93 |
94 | If you add a tweet link, you will be asked for an optional retweet quote.
95 |
96 | https://user-images.githubusercontent.com/5516876/207567328-d1f103a6-a38d-45a4-91e8-cd42fd19945a.mov
97 |
98 | It will result in a retweet on Twitter, with the quote you entered. Elsewhere the link itself will be shared as usual.
99 |
100 |
101 |
102 | Previously, feed was able to fetch the link in a tweet and share _that_ link on Tumblr and static site.
103 | But, with the Twitter's current free API it's not possible any more. Similarly, the title can't be extracted from a tweet any more.
104 |
105 | ***
106 |
107 | I hope my instructions are clear and helpful. If you get stuck, you can always open an issue or contact me and I'll be happily helping you ✌️
108 |
109 | If you find this project helpful, please consider [supporting me](https://github.com/sponsors/scriptype) 💛
110 |
--------------------------------------------------------------------------------
/theme/assets/custom/fonts/playfair-display/OFL.txt:
--------------------------------------------------------------------------------
1 | Copyright 2005–2023 The Playfair Project Authors (https://github.com/clauseggers/Playfair)
2 |
3 | This Font Software is licensed under the SIL Open Font License, Version 1.1.
4 | This license is copied below, and is also available with a FAQ at:
5 | http://scripts.sil.org/OFL
6 |
7 |
8 | -----------------------------------------------------------
9 | SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
10 | -----------------------------------------------------------
11 |
12 | PREAMBLE
13 | The goals of the Open Font License (OFL) are to stimulate worldwide
14 | development of collaborative font projects, to support the font creation
15 | efforts of academic and linguistic communities, and to provide a free and
16 | open framework in which fonts may be shared and improved in partnership
17 | with others.
18 |
19 | The OFL allows the licensed fonts to be used, studied, modified and
20 | redistributed freely as long as they are not sold by themselves. The
21 | fonts, including any derivative works, can be bundled, embedded,
22 | redistributed and/or sold with any software provided that any reserved
23 | names are not used by derivative works. The fonts and derivatives,
24 | however, cannot be released under any other type of license. The
25 | requirement for fonts to remain under this license does not apply
26 | to any document created using the fonts or their derivatives.
27 |
28 | DEFINITIONS
29 | "Font Software" refers to the set of files released by the Copyright
30 | Holder(s) under this license and clearly marked as such. This may
31 | include source files, build scripts and documentation.
32 |
33 | "Reserved Font Name" refers to any names specified as such after the
34 | copyright statement(s).
35 |
36 | "Original Version" refers to the collection of Font Software components as
37 | distributed by the Copyright Holder(s).
38 |
39 | "Modified Version" refers to any derivative made by adding to, deleting,
40 | or substituting -- in part or in whole -- any of the components of the
41 | Original Version, by changing formats or by porting the Font Software to a
42 | new environment.
43 |
44 | "Author" refers to any designer, engineer, programmer, technical
45 | writer or other person who contributed to the Font Software.
46 |
47 | PERMISSION & CONDITIONS
48 | Permission is hereby granted, free of charge, to any person obtaining
49 | a copy of the Font Software, to use, study, copy, merge, embed, modify,
50 | redistribute, and sell modified and unmodified copies of the Font
51 | Software, subject to the following conditions:
52 |
53 | 1) Neither the Font Software nor any of its individual components,
54 | in Original or Modified Versions, may be sold by itself.
55 |
56 | 2) Original or Modified Versions of the Font Software may be bundled,
57 | redistributed and/or sold with any software, provided that each copy
58 | contains the above copyright notice and this license. These can be
59 | included either as stand-alone text files, human-readable headers or
60 | in the appropriate machine-readable metadata fields within text or
61 | binary files as long as those fields can be easily viewed by the user.
62 |
63 | 3) No Modified Version of the Font Software may use the Reserved Font
64 | Name(s) unless explicit written permission is granted by the corresponding
65 | Copyright Holder. This restriction only applies to the primary font name as
66 | presented to the users.
67 |
68 | 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
69 | Software shall not be used to promote, endorse or advertise any
70 | Modified Version, except to acknowledge the contribution(s) of the
71 | Copyright Holder(s) and the Author(s) or with their explicit written
72 | permission.
73 |
74 | 5) The Font Software, modified or unmodified, in part or in whole,
75 | must be distributed entirely under this license, and must not be
76 | distributed under any other license. The requirement for fonts to
77 | remain under this license does not apply to any document created
78 | using the Font Software.
79 |
80 | TERMINATION
81 | This license becomes null and void if any of the above conditions are
82 | not met.
83 |
84 | DISCLAIMER
85 | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
86 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
87 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
88 | OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
89 | COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
90 | INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
91 | DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
92 | FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
93 | OTHER DEALINGS IN THE FONT SOFTWARE.
94 |
--------------------------------------------------------------------------------
/theme/assets/logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
21 |
--------------------------------------------------------------------------------
/theme/theme-settings.css:
--------------------------------------------------------------------------------
1 | :root {
2 | --bg: repeating-linear-gradient(
3 | 60deg,
4 | #fcfcfc,
5 | #f4f2f0 1px,
6 | #fcfcfc 2px,
7 | #fcfcfc 3px
8 | );
9 | --fg: black;
10 | --primary-color-r: 207;
11 | --primary-color-g: 49;
12 | --primary-color-b: 33;
13 | --primary-color: rgb(
14 | var(--primary-color-r),
15 | var(--primary-color-g),
16 | var(--primary-color-b)
17 | );
18 | --footer-fg: #555;
19 |
20 | --logo-bezier: cubic-bezier(0.68, -0.55, 0.27, 1.55);
21 |
22 | --link-color: #333;
23 | --link-hover-bg: #f4f4f4;
24 | --link-active-bg: #e8e8e8;
25 | --link-count-color: #333;
26 | --link-tag-color: #555;
27 | --link-tag-hover-color: #000;
28 | --link-tag-hover-stroke-color: #0005;
29 |
30 | --tag-link-color: inherit;
31 | --tag-badge-color: #ffe633;
32 | --tag-badge-number-color: #96433f;
33 | --header-perspective: auto;
34 |
35 | --logo-bg: #fff;
36 | --logo-fg: var(--primary-color);
37 | --logo-box-shadow: auto;
38 |
39 | --logo-hover-bg: #fafafa;
40 | --logo-hover-transform: scale(.85, .85);
41 | --logo-hover-box-shadow: auto;
42 |
43 | --search-input-border-color: #c8c8c8;
44 | --search-input-bg: auto;
45 | --search-input-color: auto;
46 | --search-input-label-color: #444;
47 |
48 | --chart-hover-page-bg: linear-gradient(
49 | to top,
50 | #000,
51 | #018 30%,
52 | #018 70%,
53 | #000
54 | );
55 | --chart-hover-blend: difference;
56 | --chart-active-page: lemonchiffon;
57 | --content-bg-color: #fff;
58 | --content-border-color: #eaeaea;
59 | --content-width: 640px;
60 |
61 | --tags-list-sphere-1-bg:
62 | radial-gradient(circle at 80% 90%, #a93700 -150%, transparent 75%),
63 | radial-gradient(circle at 30% 30%, #fafae5, #ffea41 30%);
64 | --tags-list-sphere-1-glow: none;
65 | --tags-list-sphere-2-bg:
66 | radial-gradient(circle at 80% 90%, #1e0071 -60%, transparent 80%),
67 | radial-gradient(circle at 30% 20%, #7e7aff, #4b07e2 40%);
68 | --tags-list-sphere-2-glow: none;
69 | --tags-list-sphere-3-bg:
70 | radial-gradient(circle at 80% 90%, #710000 -60%, transparent 80%),
71 | radial-gradient(circle at 30% 20%, #ff7a7a, #e21207 40%);
72 | --tags-list-sphere-3-glow: none;
73 |
74 | --bounce-easing: linear(
75 | 0, 0.004, 0.016, 0.035, 0.063 9.1%, 0.141, 0.25, 0.391, 0.563, 0.765, 1,
76 | 0.891, 0.813 45.5%, 0.785, 0.766, 0.754, 0.75, 0.754, 0.766, 0.785, 0.813 63.6%, 0.891, 1 72.7%,
77 | 0.973, 0.953, 0.941, 0.938, 0.941, 0.953, 0.973, 1,
78 | 0.988, 0.984, 0.988, 1
79 | );
80 | }
81 |
82 | @media (prefers-color-scheme: dark) {
83 | :root {
84 | --bg: #181a1f;
85 | --fg: #cccac8;
86 | --primary-color-r: 191;
87 | --primary-color-g: 115;
88 | --primary-color-b: 168;
89 | --primary-color: rgb(
90 | var(--primary-color-r),
91 | var(--primary-color-g),
92 | var(--primary-color-b)
93 | );
94 | --footer-fg: inherit;
95 |
96 | --link-color: inherit;
97 | --link-hover-bg: #fff1;
98 | --link-active-bg: #aaa1;
99 | --link-count-color: #bbb;
100 | --link-tag-color: inherit;
101 | --link-tag-hover-color: #fff;
102 | --link-tag-hover-stroke-color: #fff5;
103 |
104 | --tag-link-color: inherit;
105 | --tag-badge-color: #7b2f56;
106 | --tag-badge-number-color: #edfd;
107 | --header-perspective: 400px;
108 |
109 | --logo-bg: transparent;
110 | --logo-fg: #ffe5dd;
111 | --logo-box-shadow:
112 | 0 0 4vmin #ffe5dd44,
113 | 0 .2vmin 2vmin .5vmin #ffe5dd77,
114 | 0 -.5vmin 2vmin #a6f7;
115 | --logo-hover-shine-opacity: 0;
116 |
117 | --logo-hover-bg: transparent;
118 | --logo-hover-transform:
119 | scale(.75, .75)
120 | rotateY(10deg)
121 | rotateX(-10deg);
122 | --logo-hover-box-shadow:
123 | 0 0 4vmin #ffe5dd99,
124 | 0 .2vmin 2vmin .5vmin #ffe5dd99,
125 | 0 -.5vmin 3vmin #a6f8;
126 | --logo-hover-shine-opacity: .15;
127 |
128 | --search-input-border-color: #383828;
129 | --search-input-bg: #0002;
130 | --search-input-color: white;
131 | --search-input-label-color: #aaa;
132 |
133 | --chart-hover-page-bg: linear-gradient(
134 | to top,
135 | transparent,
136 | #4a234f 30%,
137 | #4a234f 70%,
138 | transparent
139 | );
140 | --chart-hover-blend: lighten;
141 | --chart-active-page: violet;
142 | --content-bg-color: rgb(29, 30, 35);
143 | --content-border-color: #111;
144 |
145 | --tags-list-sphere-1-bg: #b59a33;
146 | --tags-list-sphere-1-glow:
147 | inset 0 0 1.5em 0em palegoldenrod, 0 0 1em 0em #fff;
148 | --tags-list-sphere-2-bg: darkslategray;
149 | --tags-list-sphere-2-glow:
150 | inset -.15em -.08em .2em 0em #87ebb7, 0 0 1em 0em #fffa;
151 | --tags-list-sphere-3-bg: darkslateblue;
152 | --tags-list-sphere-3-glow:
153 | inset -.3em 0 .35em 0em #db7070, 0 0 1em 0em #fffa
154 | }
155 | }
156 |
--------------------------------------------------------------------------------
/theme/assets/custom/script/controller.js:
--------------------------------------------------------------------------------
1 | import * as Views from './views/index.js'
2 | import Links from './links.js'
3 | import Router from './router.js'
4 | import { getTagFromUrl, getPageNumberFromUrl, getSearchQueryFromUrl } from './helpers.js'
5 |
6 | const debug = false
7 |
8 | const createController = () => {
9 | let state = {}
10 |
11 | const start = async () => {
12 | document.body.classList.add('js-enhanced')
13 |
14 | Links.fetch('posts.min.json')
15 |
16 | const searchQuery = getSearchQueryFromUrl()
17 | const tag = getTagFromUrl()
18 | const pageNumber = getPageNumberFromUrl()
19 | const links = tag ? await Links.findByTag(tag) : await Links.all()
20 |
21 | state = {
22 | tag,
23 | links,
24 | pageNumber,
25 | searchQuery
26 | }
27 |
28 | await Views.Start.render({
29 | onClickLogo: navigateHomepage,
30 | onClickTag: navigateTagPage,
31 | onPaginate: navigatePage,
32 | onSearch: (payload) => {
33 | onSearch(payload)
34 | state.searchQuery = payload.query
35 | Router.search(state)
36 | },
37 | onResetSearch,
38 | tag,
39 | links,
40 | pageNumber
41 | })
42 |
43 | if (searchQuery) {
44 | onSearch({ query: searchQuery })
45 | }
46 |
47 | Router.start(state, navigateBack, { debug })
48 | }
49 |
50 | const navigateBack = async (event) => {
51 | if (!event.state) {
52 | console.log('no')
53 | return
54 | }
55 | const { links, pageNumber, tag, searchQuery } = event.state
56 | const navigatedFrom = {
57 | differentPage: state.searchQuery || state.tag !== tag
58 | }
59 | state = {
60 | tag,
61 | links,
62 | pageNumber,
63 | searchQuery
64 | }
65 |
66 | if (searchQuery) {
67 | return onSearch({ query: searchQuery })
68 | }
69 |
70 | if (tag) {
71 | return Views.Tag.render({
72 | tag,
73 | links,
74 | pageNumber,
75 | navigatedFrom,
76 | onPaginate: navigatePage
77 | })
78 | }
79 |
80 | return Views.Homepage.render({
81 | links,
82 | pageNumber,
83 | navigatedFrom,
84 | onPaginate: navigatePage
85 | })
86 | }
87 |
88 | const navigatePage = async ({ tag, pageNumber, paginationType }) => {
89 | const links = tag ? await Links.findByTag(tag) : await Links.all()
90 | const navigatedFrom = {
91 | pagination: paginationType || true
92 | }
93 |
94 | state = {
95 | tag,
96 | links,
97 | pageNumber,
98 | searchQuery: ''
99 | }
100 |
101 | if (tag) {
102 | Views.Tag.render({
103 | tag,
104 | links,
105 | pageNumber,
106 | navigatedFrom,
107 | onPaginate: navigatePage
108 | })
109 |
110 | return Router.tag(state)
111 | }
112 |
113 | Views.Homepage.render({
114 | links,
115 | pageNumber,
116 | navigatedFrom,
117 | onPaginate: navigatePage
118 | })
119 |
120 | Router.homepage(state)
121 | }
122 |
123 | const navigateHomepage = async () => {
124 | const links = await Links.all()
125 | const navigatedFrom = {
126 | differentPage: state.searchQuery || state.tag,
127 | pagination: state.pageNumber > 0
128 | }
129 |
130 | state = {
131 | links,
132 | pageNumber: 0,
133 | searchQuery: ''
134 | }
135 |
136 | Views.Homepage.render({
137 | links,
138 | pageNumber: 0,
139 | navigatedFrom,
140 | onPaginate: navigatePage
141 | })
142 |
143 | Router.homepage(state)
144 | }
145 |
146 | const navigateTagPage = async ({ tag } = {}) => {
147 | const links = await Links.findByTag(tag)
148 | const navigatedFrom = {
149 | differentPage: state.searchQuery || state.tag !== tag,
150 | pagination: state.pageNumber > 0
151 | }
152 | state = {
153 | tag,
154 | links,
155 | pageNumber: 0,
156 | searchQuery: ''
157 | }
158 |
159 | Views.Tag.render({
160 | tag,
161 | links,
162 | pageNumber: 0,
163 | navigatedFrom,
164 | onPaginate: navigatePage
165 | })
166 |
167 | Router.tag(state)
168 | }
169 |
170 | const onSearch = async ({ query }) => {
171 | const links = await Links.search(query)
172 |
173 | Views.Search.render({
174 | links,
175 | searchQuery: query
176 | })
177 | }
178 |
179 | const onResetSearch = async () => {
180 | const tag = getTagFromUrl()
181 | const pageNumber = getPageNumberFromUrl()
182 | const links = tag ? await Links.findByTag(tag) : await Links.all()
183 | const navigatedFrom = {
184 | differentPage: false,
185 | pagination: false
186 | }
187 | state.searchQuery = ''
188 |
189 | if (tag) {
190 | Views.Tag.render({
191 | tag,
192 | links,
193 | pageNumber,
194 | navigatedFrom,
195 | onPaginate: navigatePage
196 | })
197 |
198 | return Router.tag(state)
199 | }
200 |
201 | Views.Homepage.render({
202 | links,
203 | pageNumber,
204 | navigatedFrom,
205 | onPaginate: navigatePage
206 | })
207 |
208 | return Router.homepage(state)
209 | }
210 |
211 | return {
212 | start,
213 | onSearch,
214 | onResetSearch,
215 | navigatePage,
216 | navigateHomepage,
217 | navigateTagPage
218 | }
219 | }
220 |
221 | export default createController
222 |
--------------------------------------------------------------------------------
/theme/assets/custom/style/tags-list.css:
--------------------------------------------------------------------------------
1 | .tags-list-toggle-label {
2 | position: relative;
3 | display: list-item;
4 | min-width: 190px;
5 | width: 100%;
6 | margin-bottom: 2em;
7 | padding-left: 30px;
8 | text-align: center;
9 | color: royalblue;
10 | opacity: 0;
11 | animation: appear .2s .4s forwards;
12 | }
13 |
14 | @keyframes appear {
15 | to {
16 | opacity: 1;
17 | }
18 | }
19 |
20 | @media (min-width: 901px) {
21 | .js-enhanced .tags-list-toggle-label {
22 | bottom: 200vh;
23 | font-size: 0;
24 | }
25 |
26 | .tags-list-toggle-label:focus {
27 | bottom: 0;
28 | font-size: 1em;
29 | }
30 | }
31 |
32 | .tags-list {
33 | --number-of-items: 22;
34 | display: flex;
35 | flex-direction: column;
36 | align-items: flex-end;
37 | gap: 2.3em;
38 | }
39 |
40 | .tags-list-item {
41 | --i: 0;
42 | opacity: 0;
43 | animation: tags-list-item .3s forwards;
44 | animation-delay: calc(var(--i) * 25ms);
45 | padding: 0;
46 | margin-left: 1em;
47 | white-space: nowrap;
48 | letter-spacing: 0.1em;
49 | text-transform: uppercase;
50 | }
51 |
52 | @keyframes tags-list-item {
53 | from {
54 | transform: translateX(-10%);
55 | }
56 | to {
57 | transform: translateX(0);
58 | opacity: 1;
59 | }
60 | }
61 |
62 | .tags-list-item-link {
63 | display: inline-block;
64 | position: relative;
65 | padding-right: 1.7em;
66 | text-decoration-thickness: 0.04em;
67 | text-underline-offset: 0.1em;
68 | text-decoration-color: #999;
69 | color: var(--tag-link-color);
70 | }
71 |
72 | .tag-hash {
73 | margin-right: -0.3em;
74 | opacity: 0.7;
75 | }
76 |
77 | .tag-count-badge {
78 | position: absolute;
79 | top: 50%;
80 | right: 0;
81 | transform: translateY(-45%);
82 | display: inline-flex;
83 | justify-content: center;
84 | align-items: center;
85 | width: 2em;
86 | aspect-ratio: 1;
87 | font-size: 0.7em;
88 | font-family: monospace;
89 | font-weight: 100;
90 | letter-spacing: -.04em;
91 | border-radius: 0.8em;
92 | background: var(--tag-badge-color);
93 | color: var(--tag-badge-number-color);
94 | }
95 |
96 | @media (max-width: 900px) {
97 | ::-webkit-details-marker {
98 | display: none;
99 | }
100 |
101 | body:has(.tags-list-container[open]) {
102 | overflow: hidden;
103 | }
104 |
105 | body.tags-list-is-open {
106 | overflow: hidden;
107 | }
108 |
109 | .tags-list-toggle-label {
110 | --animation-duration: 1s;
111 | display: inline-block;
112 | position: absolute;
113 | right: 1em;
114 | bottom: 1em;
115 | width: 3.5em;
116 | min-width: auto;
117 | margin: 0;
118 | text-indent: -2000vw;
119 | aspect-ratio: 1;
120 | border-radius: 50%;
121 | cursor: pointer;
122 | opacity: 1;
123 | background: var(--tags-list-sphere-1-bg);
124 | box-shadow: var(--tags-list-sphere-1-glow);
125 | animation: bounce var(--animation-duration) var(--bounce-easing) forwards;
126 | }
127 |
128 | .tags-list-toggle-label,
129 | .tags-list-toggle-label::before,
130 | .tags-list-toggle-label::after {
131 | z-index: 2;
132 | }
133 |
134 | @keyframes bounce {
135 | from {
136 | transform: translateY(-200%);
137 | }
138 | to {
139 | transform: translateY(0);
140 | }
141 | }
142 |
143 | .tags-list-toggle-label::before,
144 | .tags-list-toggle-label::after {
145 | content: "";
146 | display: inline-block;
147 | aspect-ratio: 1;
148 | border-radius: 50%;
149 | position: absolute;
150 | }
151 |
152 | .tags-list-toggle-label::after {
153 | right: 3.8em;
154 | bottom: 1.05em;
155 | width: 1.33em;
156 | background: var(--tags-list-sphere-3-bg);
157 | box-shadow: var(--tags-list-sphere-3-glow);
158 | animation: bounce var(--animation-duration) -.1s var(--bounce-easing) forwards;
159 | }
160 |
161 | .tags-list-toggle-label::before {
162 | right: 3.3em;
163 | bottom: 2.75em;
164 | width: 0.73em;
165 | background: var(--tags-list-sphere-2-bg);
166 | box-shadow: var(--tags-list-sphere-2-glow);
167 | animation: bounce var(--animation-duration) -.2s var(--bounce-easing) forwards;
168 | }
169 |
170 | .tags-list-toggle[open] .tags-list-toggle-label {}
171 | .tags-list-toggle[open] .tags-list-toggle-label::after {}
172 | .tags-list-toggle[open] .tags-list-toggle-label::before {}
173 |
174 | /*
175 | * For some reason, mobile safari eats up a chunk from top padding.
176 | * Increase padding-top, and move top fader-gradient lower
177 | * It looks slightly agressive on responsive emulator, but fine on phone.
178 | * */
179 | .tags-list {
180 | background: var(--content-bg-color);
181 | justify-content: flex-start;
182 | align-items: center;
183 | gap: 2em;
184 | height: 100dvh;
185 | padding: 10em 0 8em;
186 | overflow: auto;
187 | flex-wrap: nowrap;
188 | }
189 |
190 | .tags-list::after,
191 | .tags-list::before {
192 | content: "";
193 | position: absolute;
194 | width: 100%;
195 | height: 30%;
196 | pointer-events: none;
197 | }
198 |
199 | .tags-list::before {
200 | --scroll: var(--tags-list-scroll-1, 0);
201 | content: "Topics";
202 | top: 0;
203 | z-index: 1;
204 | display: block;
205 | padding-top: 5dvh;
206 | font-size: calc(2em - var(--scroll) * 3em);
207 | font-weight: bold;
208 | letter-spacing: -.03em;
209 | text-align: center;
210 | text-shadow:
211 | 0 calc(1em + var(--scroll) * -20em)
212 | calc(var(--scroll) * 3em)
213 | rgba(
214 | var(--primary-color-r),
215 | var(--primary-color-g),
216 | var(--primary-color-b),
217 | calc(1 - var(--scroll) * 10)
218 | );
219 | color: transparent;
220 | background: radial-gradient(
221 | ellipse 100% 100% at center -20%,
222 | var(--content-bg-color) 20%,
223 | transparent
224 | );
225 | transform: translateZ(0);
226 | }
227 |
228 | .tags-list::after {
229 | bottom: 0;
230 | background: radial-gradient(
231 | ellipse 100% 100% at center 120%,
232 | var(--content-bg-color) 20%,
233 | transparent
234 | );
235 | }
236 |
237 | .tags-list-item {
238 | font-size: 1.6em;
239 | }
240 | }
241 |
242 | @media (prefers-color-scheme: dark) {
243 | .tags-list-item,
244 | .tag-count-badge {
245 | transition: all .4s;
246 | }
247 |
248 | .header:has(.logo-container:hover, .logo-container:focus-within) ~ .tags .tags-list-item {
249 | --proximity: calc(1 - var(--i) / var(--number-of-items));
250 | text-shadow:
251 | .25dvmin calc(2.5dvmin - var(--proximity) * 1.2dvmin)
252 | calc(1.75dvmin - var(--proximity) * 1dvmin)
253 | rgba(0, 0, 0, var(--proximity)),
254 | -.1dvmin -.1dvmin .2dvmin
255 | hsla(30, 100%, 60%,
256 | calc(var(--proximity) * var(--proximity) - .25)
257 | );
258 | }
259 |
260 | .header:has(.logo-container:hover, .logo-container:focus-within) ~ .tags .tag-count-badge {
261 | box-shadow:
262 | inset .1dvmin .3dvmin .3dvmin #fed3,
263 | inset -.1dvmin -.3dvmin .3dvmin #3213;
264 | }
265 | }
266 |
--------------------------------------------------------------------------------
/theme/assets/custom/script/components/chart-pagination.js:
--------------------------------------------------------------------------------
1 | import Modes from '../modes.js'
2 | import { last, query, queryAll, fixLeadingSlashes } from '../helpers.js'
3 |
4 | const findScrollContainer = () => {
5 | return query('.graph-container')
6 | }
7 |
8 | const findContentContainer = () => {
9 | return query('.links')
10 | }
11 |
12 | const findPaginationContainer = (scrollContainer = findScrollContainer()) => {
13 | return query('#overlay-pagination-container', scrollContainer)
14 | }
15 |
16 | const findToggle = (scrollContainer = findScrollContainer()) => {
17 | return query('#overlay-pagination-toggle', scrollContainer)
18 | }
19 |
20 | const findList = (scrollContainer = findScrollContainer()) => {
21 | return query('#overlay-pagination', scrollContainer)
22 | }
23 |
24 | const findListItems = (list = findList()) => {
25 | return queryAll('.overlay-pagination-item', list)
26 | }
27 |
28 | const findLinks = () => {
29 | return queryAll('.overlay-pagination-link')
30 | }
31 |
32 | const findPageLink = (pageNumber, list = findList()) => {
33 | return query(`[data-page-number="${pageNumber}"]`, list)
34 | }
35 |
36 | const findCurrentLink = (list = findList()) => {
37 | return query('.overlay-pagination-link.is-current', list)
38 | }
39 |
40 | const LINK_CLASS = 'overlay-pagination-link'
41 | const CURRENT_LINK_CLASS = 'is-current'
42 | const FAKE_ACTIVE_STATE_CLASS = 'faked-active-state'
43 |
44 | const DAY = 1000 * 60 * 60 * 24
45 |
46 | const eventListeners = {
47 | global: null
48 | }
49 |
50 | const setContainerProperties = () => {
51 | const container = findScrollContainer()
52 | container.classList.toggle('has-scroll', container.scrollWidth > container.clientWidth)
53 | }
54 |
55 | const setPageLinkProperties = (dayScale) => (page, i) => {
56 | const pageStartDate = i === 0 ? Date.now() : page[0]
57 | const pageEndDate = last(page)
58 | const daysElapsedInPage = (pageStartDate - pageEndDate) / DAY
59 | const width = daysElapsedInPage * dayScale
60 | const list = findList()
61 | const link = findListItems(list)[i]
62 | if (link) {
63 | link.style.setProperty('--i', i)
64 | link.style.setProperty('--width', width + 'px')
65 | }
66 | }
67 |
68 | const setListProperties = () => {
69 | const container = findPaginationContainer()
70 | const toggle = findToggle(container)
71 | const toggleLabels = {
72 | clickToOpen: 'Show graph pagination links',
73 | clickToClose: 'Hide graph pagination links'
74 | }
75 | toggle.addEventListener('click', () => {
76 | if (toggle.textContent === toggleLabels.clickToClose) {
77 | toggle.textContent = toggleLabels.clickToOpen
78 | } else {
79 | toggle.textContent = toggleLabels.clickToClose
80 | }
81 | })
82 | container.setAttribute('open', true)
83 | toggle.textContent = toggleLabels.clickToClose
84 | }
85 |
86 | const setCurrentPageLink = (pageNumber) => {
87 | const list = findList()
88 | const currentPageLink = findCurrentLink(list) || findLinks(list)[0]
89 | currentPageLink.classList.remove(CURRENT_LINK_CLASS)
90 |
91 | if (pageNumber) {
92 | const pageLink = findPageLink(pageNumber, list)
93 | pageLink.classList.add(CURRENT_LINK_CLASS)
94 | } else {
95 | const pageLink = findLinks(list)[0]
96 | pageLink.classList.add(CURRENT_LINK_CLASS)
97 | }
98 | }
99 |
100 | const scrollToCurrentPageLink = ({ behavior = 'auto' }) => {
101 | const link = findCurrentLink()
102 | if (!link) {
103 | return
104 | }
105 | const container = findScrollContainer()
106 | const { clientWidth: containerWidth } = container
107 | const { offsetLeft: linkLeft, clientWidth: linkWidth } = link
108 | requestAnimationFrame(() => {
109 | container.scrollTo({
110 | top: 0,
111 | left: linkLeft - (containerWidth / 2) + linkWidth / 2,
112 | behavior
113 | })
114 | })
115 | }
116 |
117 | const scrollBoost = ({ readonly }) => {
118 | return new window.ScrollBooster({
119 | viewport: findScrollContainer(),
120 | scrollMode: 'native',
121 |
122 | onPointerDown(state, event) {
123 | let element = event.target
124 | if (readonly) {
125 | element = findScrollContainer()
126 | }
127 | element.classList.add(FAKE_ACTIVE_STATE_CLASS)
128 | },
129 |
130 | onPointerUp(state, event) {
131 | if (readonly) {
132 | findScrollContainer().classList.remove(FAKE_ACTIVE_STATE_CLASS)
133 | } else {
134 | findLinks().forEach(link => {
135 | link.classList.remove(FAKE_ACTIVE_STATE_CLASS)
136 | })
137 | }
138 | },
139 |
140 | shouldScroll(state, event) {
141 | return readonly || event.target.classList.contains(LINK_CLASS)
142 | },
143 |
144 | // Later event handlers will know that drag is in progress
145 | onClick(state, event) {
146 | if (state.isMoving) {
147 | event.isDragScrolling = true
148 | }
149 | }
150 | })
151 | }
152 |
153 | const attachClickLinkListener = ({ pages, onClickLink }) => {
154 | const clickLinkListener = (event) => {
155 | if (!event.target.classList.contains(LINK_CLASS)) {
156 | return
157 | }
158 | if (event.isDragScrolling) {
159 | return
160 | }
161 | event.preventDefault()
162 |
163 | const pageNumberMatch = event.target.href.match(/\/page\/(\d+)/)
164 | const pageNumber = pageNumberMatch && Number(pageNumberMatch[1])
165 |
166 | onClickLink({
167 | pageNumber
168 | })
169 | }
170 |
171 | findScrollContainer().addEventListener('click', clickLinkListener)
172 | return clickLinkListener
173 | }
174 |
175 | const attachListenersToAdjustTogglePosition = () => {
176 | const scrollContainer = findScrollContainer()
177 | window.addEventListener('resize', setTogglePosition)
178 | window.addEventListener('orientationchange', setTogglePosition)
179 | scrollContainer.addEventListener('scroll', setTogglePosition)
180 | return setTogglePosition
181 | }
182 |
183 | const setTogglePosition = () => {
184 | const scrollContainer = findScrollContainer()
185 | scrollContainer.style.setProperty(
186 | '--graph-left', scrollContainer.scrollLeft + 'px'
187 | )
188 | }
189 |
190 | const getBaseUrl = (tag) => {
191 | const urlParts = [window.permalinkPrefix]
192 | if (tag) {
193 | urlParts.push('tags', tag)
194 | }
195 | return fixLeadingSlashes(urlParts.filter(Boolean).join('/'))
196 | }
197 |
198 | const getPageUrl = (baseUrl, pageNumber) => {
199 | if (baseUrl === '/') {
200 | return baseUrl + ['page', String(pageNumber)].join('/')
201 | }
202 | return [baseUrl, 'page', String(pageNumber)].join('/')
203 | }
204 |
205 | const template = ({ tag, pages, pageNumber }) => {
206 | const baseUrl = getBaseUrl(tag)
207 | return `
208 |
224 | `
225 | }
226 |
227 | const rerenderList = ({ tag, pages, pageNumber }) => {
228 | const list = findList()
229 | list.outerHTML = template({
230 | tag,
231 | pages,
232 | pageNumber
233 | })
234 | return list
235 | }
236 |
237 | const render = ({ readonly, mode, tag, pages, pageNumber = 0, dayScale, onPaginate }) => {
238 | if (!readonly && mode === Modes.full) {
239 | rerenderList({
240 | tag,
241 | pages,
242 | pageNumber
243 | })
244 | }
245 |
246 | setContainerProperties()
247 | setListProperties()
248 |
249 | pages
250 | .map(p => p.map(l => l.datePublished))
251 | .forEach(setPageLinkProperties(dayScale))
252 |
253 | scrollBoost({ readonly })
254 |
255 | if (!readonly) {
256 | attachClickLinkListener({
257 | pages,
258 | onClickLink: onPaginate
259 | })
260 |
261 | updateCurrentPage({
262 | pageNumber,
263 | scrollBehavior: 'auto'
264 | })
265 | }
266 |
267 | if (!eventListeners.global) {
268 | eventListeners.global = attachListenersToAdjustTogglePosition()
269 | }
270 | setTogglePosition()
271 | }
272 |
273 | const updateCurrentPage = ({ pageNumber, scrollBehavior = 'smooth' }) => {
274 | setCurrentPageLink(pageNumber)
275 | scrollToCurrentPageLink({
276 | behavior: scrollBehavior
277 | })
278 | }
279 |
280 | export default {
281 | render,
282 | updateCurrentPage
283 | }
284 |
--------------------------------------------------------------------------------
/theme/templates/components/logo.hbs:
--------------------------------------------------------------------------------
1 |
2 |
21 |
40 |
41 |
42 |
--------------------------------------------------------------------------------
/theme/assets/custom/script/vendor/scrollbooster.min.js:
--------------------------------------------------------------------------------
1 | !function(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e():"function"==typeof define&&define.amd?define("ScrollBooster",[],e):"object"==typeof exports?exports.ScrollBooster=e():t.ScrollBooster=e()}(this,(function(){return function(t){var e={};function i(o){if(e[o])return e[o].exports;var n=e[o]={i:o,l:!1,exports:{}};return t[o].call(n.exports,n,n.exports,i),n.l=!0,n.exports}return i.m=t,i.c=e,i.d=function(t,e,o){i.o(t,e)||Object.defineProperty(t,e,{enumerable:!0,get:o})},i.r=function(t){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(t,"__esModule",{value:!0})},i.t=function(t,e){if(1&e&&(t=i(t)),8&e)return t;if(4&e&&"object"==typeof t&&t&&t.__esModule)return t;var o=Object.create(null);if(i.r(o),Object.defineProperty(o,"default",{enumerable:!0,value:t}),2&e&&"string"!=typeof t)for(var n in t)i.d(o,n,function(e){return t[e]}.bind(null,n));return o},i.n=function(t){var e=t&&t.__esModule?function(){return t.default}:function(){return t};return i.d(e,"a",e),e},i.o=function(t,e){return Object.prototype.hasOwnProperty.call(t,e)},i.p="",i(i.s=0)}([function(t,e,i){"use strict";function o(t,e){var i=Object.keys(t);if(Object.getOwnPropertySymbols){var o=Object.getOwnPropertySymbols(t);e&&(o=o.filter((function(e){return Object.getOwnPropertyDescriptor(t,e).enumerable}))),i.push.apply(i,o)}return i}function n(t){for(var e=1;e0&&void 0!==arguments[0]?arguments[0]:{};s(this,t);var i={content:e.viewport.children[0],direction:"all",pointerMode:"all",scrollMode:void 0,bounce:!0,bounceForce:.1,friction:.05,textSelection:!1,inputsFocus:!0,emulateScroll:!1,preventDefaultOnEmulateScroll:!1,preventPointerMoveDefault:!0,lockScrollOnDragDirection:!1,pointerDownPreventDefault:!0,dragDirectionTolerance:40,onPointerDown:function(){},onPointerUp:function(){},onPointerMove:function(){},onClick:function(){},onUpdate:function(){},onWheel:function(){},shouldScroll:function(){return!0}};if(this.props=n(n({},i),e),this.props.viewport&&this.props.viewport instanceof Element)if(this.props.content){this.isDragging=!1,this.isTargetScroll=!1,this.isScrolling=!1,this.isRunning=!1;var o={x:0,y:0};this.position=n({},o),this.velocity=n({},o),this.dragStartPosition=n({},o),this.dragOffset=n({},o),this.clientOffset=n({},o),this.dragPosition=n({},o),this.targetPosition=n({},o),this.scrollOffset=n({},o),this.rafID=null,this.events={},this.updateMetrics(),this.handleEvents()}else console.error("ScrollBooster init error: Viewport does not have any content");else console.error('ScrollBooster init error: "viewport" config property must be present and must be Element')}var e,i,o;return e=t,(i=[{key:"updateOptions",value:function(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};this.props=n(n({},this.props),t),this.props.onUpdate(this.getState()),this.startAnimationLoop()}},{key:"updateMetrics",value:function(){var t;this.viewport={width:this.props.viewport.clientWidth,height:this.props.viewport.clientHeight},this.content={width:(t=this.props.content,Math.max(t.offsetWidth,t.scrollWidth)),height:l(this.props.content)},this.edgeX={from:Math.min(-this.content.width+this.viewport.width,0),to:0},this.edgeY={from:Math.min(-this.content.height+this.viewport.height,0),to:0},this.props.onUpdate(this.getState()),this.startAnimationLoop()}},{key:"startAnimationLoop",value:function(){var t=this;this.isRunning=!0,cancelAnimationFrame(this.rafID),this.rafID=requestAnimationFrame((function(){return t.animate()}))}},{key:"animate",value:function(){var t=this;if(this.isRunning){this.updateScrollPosition(),this.isMoving()||(this.isRunning=!1,this.isTargetScroll=!1);var e=this.getState();this.setContentPosition(e),this.props.onUpdate(e),this.rafID=requestAnimationFrame((function(){return t.animate()}))}}},{key:"updateScrollPosition",value:function(){this.applyEdgeForce(),this.applyDragForce(),this.applyScrollForce(),this.applyTargetForce();var t=1-this.props.friction;this.velocity.x*=t,this.velocity.y*=t,"vertical"!==this.props.direction&&(this.position.x+=this.velocity.x),"horizontal"!==this.props.direction&&(this.position.y+=this.velocity.y),this.props.bounce&&!this.isScrolling||this.isTargetScroll||(this.position.x=Math.max(Math.min(this.position.x,this.edgeX.to),this.edgeX.from),this.position.y=Math.max(Math.min(this.position.y,this.edgeY.to),this.edgeY.from))}},{key:"applyForce",value:function(t){this.velocity.x+=t.x,this.velocity.y+=t.y}},{key:"applyEdgeForce",value:function(){if(this.props.bounce&&!this.isDragging){var t=this.position.xthis.edgeX.to,i=this.position.ythis.edgeY.to,n=t||e,r=i||o;if(n||r){var s=t?this.edgeX.from:this.edgeX.to,a=i?this.edgeY.from:this.edgeY.to,l=s-this.position.x,p=a-this.position.y,c={x:l*this.props.bounceForce,y:p*this.props.bounceForce},h=this.position.x+(this.velocity.x+c.x)/this.props.friction,u=this.position.y+(this.velocity.y+c.y)/this.props.friction;(t&&h>=this.edgeX.from||e&&h<=this.edgeX.to)&&(c.x=l*this.props.bounceForce-this.velocity.x),(i&&u>=this.edgeY.from||o&&u<=this.edgeY.to)&&(c.y=p*this.props.bounceForce-this.velocity.y),this.applyForce({x:n?c.x:0,y:r?c.y:0})}}}},{key:"applyDragForce",value:function(){if(this.isDragging){var t=this.dragPosition.x-this.position.x,e=this.dragPosition.y-this.position.y;this.applyForce({x:t-this.velocity.x,y:e-this.velocity.y})}}},{key:"applyScrollForce",value:function(){this.isScrolling&&(this.applyForce({x:this.scrollOffset.x-this.velocity.x,y:this.scrollOffset.y-this.velocity.y}),this.scrollOffset.x=0,this.scrollOffset.y=0)}},{key:"applyTargetForce",value:function(){this.isTargetScroll&&this.applyForce({x:.08*(this.targetPosition.x-this.position.x)-this.velocity.x,y:.08*(this.targetPosition.y-this.position.y)-this.velocity.y})}},{key:"isMoving",value:function(){return this.isDragging||this.isScrolling||Math.abs(this.velocity.x)>=.01||Math.abs(this.velocity.y)>=.01}},{key:"scrollTo",value:function(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};this.isTargetScroll=!0,this.targetPosition.x=-t.x||0,this.targetPosition.y=-t.y||0,this.startAnimationLoop()}},{key:"setPosition",value:function(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};this.velocity.x=0,this.velocity.y=0,this.position.x=-t.x||0,this.position.y=-t.y||0,this.startAnimationLoop()}},{key:"getState",value:function(){return{isMoving:this.isMoving(),isDragging:!(!this.dragOffset.x&&!this.dragOffset.y),position:{x:-this.position.x,y:-this.position.y},dragOffset:this.dragOffset,dragAngle:this.getDragAngle(this.clientOffset.x,this.clientOffset.y),borderCollision:{left:this.position.x>=this.edgeX.to,right:this.position.x<=this.edgeX.from,top:this.position.y>=this.edgeY.to,bottom:this.position.y<=this.edgeY.from}}}},{key:"getDragAngle",value:function(t,e){return Math.round(Math.atan2(t,e)*(180/Math.PI))}},{key:"getDragDirection",value:function(t,e){return Math.abs(90-Math.abs(t))<=90-e?"horizontal":"vertical"}},{key:"setContentPosition",value:function(t){"transform"===this.props.scrollMode&&(this.props.content.style.transform="translate(".concat(-t.position.x,"px, ").concat(-t.position.y,"px)")),"native"===this.props.scrollMode&&(this.props.viewport.scrollTop=t.position.y,this.props.viewport.scrollLeft=t.position.x)}},{key:"handleEvents",value:function(){var t=this,e={x:0,y:0},i={x:0,y:0},o=null,n=null,r=!1,s=function(n){if(t.isDragging){var s=r?n.touches[0]:n,a=s.pageX,l=s.pageY,p=s.clientX,c=s.clientY;t.dragOffset.x=a-e.x,t.dragOffset.y=l-e.y,t.clientOffset.x=p-i.x,t.clientOffset.y=c-i.y,(Math.abs(t.clientOffset.x)>5&&!o||Math.abs(t.clientOffset.y)>5&&!o)&&(o=t.getDragDirection(t.getDragAngle(t.clientOffset.x,t.clientOffset.y),t.props.dragDirectionTolerance)),t.props.lockScrollOnDragDirection&&"all"!==t.props.lockScrollOnDragDirection?o===t.props.lockScrollOnDragDirection&&r?(t.dragPosition.x=t.dragStartPosition.x+t.dragOffset.x,t.dragPosition.y=t.dragStartPosition.y+t.dragOffset.y):r?(t.dragPosition.x=t.dragStartPosition.x,t.dragPosition.y=t.dragStartPosition.y):(t.dragPosition.x=t.dragStartPosition.x+t.dragOffset.x,t.dragPosition.y=t.dragStartPosition.y+t.dragOffset.y):(t.dragPosition.x=t.dragStartPosition.x+t.dragOffset.x,t.dragPosition.y=t.dragStartPosition.y+t.dragOffset.y)}};this.events.pointerdown=function(o){r=!(!o.touches||!o.touches[0]),t.props.onPointerDown(t.getState(),o,r);var n=r?o.touches[0]:o,a=n.pageX,l=n.pageY,p=n.clientX,c=n.clientY,h=t.props.viewport,u=h.getBoundingClientRect();if(!(p-u.left>=h.clientLeft+h.clientWidth)&&!(c-u.top>=h.clientTop+h.clientHeight)&&t.props.shouldScroll(t.getState(),o)&&2!==o.button&&("mouse"!==t.props.pointerMode||!r)&&("touch"!==t.props.pointerMode||r)&&!(t.props.inputsFocus&&["input","textarea","button","select","label"].indexOf(o.target.nodeName.toLowerCase())>-1)){if(t.props.textSelection){if(function(t,e,i){for(var o=t.childNodes,n=document.createRange(),r=0;r=a.left&&i>=a.top&&e<=a.right&&i<=a.bottom)return s}}return!1}(o.target,p,c))return;(f=window.getSelection?window.getSelection():document.selection)&&(f.removeAllRanges?f.removeAllRanges():f.empty&&f.empty())}var f;t.isDragging=!0,e.x=a,e.y=l,i.x=p,i.y=c,t.dragStartPosition.x=t.position.x,t.dragStartPosition.y=t.position.y,s(o),t.startAnimationLoop(),!r&&t.props.pointerDownPreventDefault&&o.preventDefault()}},this.events.pointermove=function(e){!e.cancelable||"all"!==t.props.lockScrollOnDragDirection&&t.props.lockScrollOnDragDirection!==o||e.preventDefault(),s(e),t.props.onPointerMove(t.getState(),e,r)},this.events.pointerup=function(e){t.isDragging=!1,o=null,t.props.onPointerUp(t.getState(),e,r)},this.events.wheel=function(e){var i=t.getState();t.props.emulateScroll&&(t.velocity.x=0,t.velocity.y=0,t.isScrolling=!0,t.scrollOffset.x=-e.deltaX,t.scrollOffset.y=-e.deltaY,t.props.onWheel(i,e),t.startAnimationLoop(),clearTimeout(n),n=setTimeout((function(){return t.isScrolling=!1}),80),t.props.preventDefaultOnEmulateScroll&&t.getDragDirection(t.getDragAngle(-e.deltaX,-e.deltaY),t.props.dragDirectionTolerance)===t.props.preventDefaultOnEmulateScroll&&e.preventDefault())},this.events.scroll=function(){var e=t.props.viewport,i=e.scrollLeft,o=e.scrollTop;Math.abs(t.position.x+i)>3&&(t.position.x=-i,t.velocity.x=0),Math.abs(t.position.y+o)>3&&(t.position.y=-o,t.velocity.y=0)},this.events.click=function(e){var i=t.getState(),o="vertical"!==t.props.direction?i.dragOffset.x:0,n="horizontal"!==t.props.direction?i.dragOffset.y:0;Math.max(Math.abs(o),Math.abs(n))>5&&(e.preventDefault(),e.stopPropagation()),t.props.onClick(i,e,r)},this.events.contentLoad=function(){return t.updateMetrics()},this.events.resize=function(){return t.updateMetrics()},this.props.viewport.addEventListener("mousedown",this.events.pointerdown),this.props.viewport.addEventListener("touchstart",this.events.pointerdown,{passive:!1}),this.props.viewport.addEventListener("click",this.events.click),this.props.viewport.addEventListener("wheel",this.events.wheel,{passive:!1}),this.props.viewport.addEventListener("scroll",this.events.scroll),this.props.content.addEventListener("load",this.events.contentLoad,!0),window.addEventListener("mousemove",this.events.pointermove),window.addEventListener("touchmove",this.events.pointermove,{passive:!1}),window.addEventListener("mouseup",this.events.pointerup),window.addEventListener("touchend",this.events.pointerup),window.addEventListener("resize",this.events.resize)}},{key:"destroy",value:function(){this.props.viewport.removeEventListener("mousedown",this.events.pointerdown),this.props.viewport.removeEventListener("touchstart",this.events.pointerdown),this.props.viewport.removeEventListener("click",this.events.click),this.props.viewport.removeEventListener("wheel",this.events.wheel),this.props.viewport.removeEventListener("scroll",this.events.scroll),this.props.content.removeEventListener("load",this.events.contentLoad),window.removeEventListener("mousemove",this.events.pointermove),window.removeEventListener("touchmove",this.events.pointermove),window.removeEventListener("mouseup",this.events.pointerup),window.removeEventListener("touchend",this.events.pointerup),window.removeEventListener("resize",this.events.resize)}}])&&a(e.prototype,i),o&&a(e,o),t}()}]).default}));
2 |
--------------------------------------------------------------------------------
/theme/assets/custom/script/vendor/kicss.js:
--------------------------------------------------------------------------------
1 | const { performInterpolation, purgeRangeCache } = (() => {
2 | function findRange(input, inputRange) {
3 | let i
4 | for (i = 1; i < inputRange.length - 1; ++i) {
5 | if (inputRange[i] >= input) {
6 | break
7 | }
8 | }
9 | return i - 1
10 | }
11 |
12 | const interpolate = ({ value, inputRange, outputRange}) => {
13 | const range = findRange(value, inputRange)
14 | const inputMin = inputRange[range]
15 | const inputMax = inputRange[range + 1]
16 | const outputMin = outputRange[range]
17 | const outputMax = outputRange[range + 1]
18 | let interpolated = value
19 | interpolated = (interpolated - inputMin) / (inputMax - inputMin)
20 | interpolated = interpolated * (outputMax - outputMin) + outputMin
21 | return interpolated
22 | }
23 |
24 | const performInterpolation = ({ interpolation, id, value }) => {
25 | const {
26 | name: interpolationName,
27 | scope,
28 | inputRange,
29 | outputRange,
30 | cache = true,
31 | cacheDuration = 300
32 | } = interpolation
33 | const [cachedInputRange, cachedOutputRane] = cache ?
34 | cacheRanges(`${interpolationName}-${id}`, [inputRange, outputRange], cacheDuration) :
35 | [
36 | typeof inputRange === 'function' ? inputRange() : inputRange,
37 | typeof outputRange === 'function' ? outputRange() : outputRange,
38 | ]
39 | const interpolated = interpolate({
40 | value,
41 | inputRange: cachedInputRange,
42 | outputRange: cachedOutputRane
43 | })
44 | return {
45 | interpolationName,
46 | interpolated,
47 | scope
48 | }
49 | }
50 |
51 | let rangeCache = {}
52 | const cacheRanges = (interpolationName, ranges, cacheDuration) => {
53 | const cachedRanges = rangeCache[interpolationName]
54 | if (cachedRanges && (Date.now() - cachedRanges.timestamp < cacheDuration)) {
55 | return cachedRanges.ranges
56 | }
57 | rangeCache[interpolationName] = {
58 | ranges: ranges.map(r => typeof r === 'function' ? r() : r),
59 | timestamp: Date.now()
60 | }
61 | return rangeCache[interpolationName].ranges
62 | }
63 |
64 | const purgeRangeCache = () => {
65 | rangeCache = {}
66 | }
67 |
68 | return {
69 | performInterpolation,
70 | purgeRangeCache
71 | }
72 | })()
73 |
74 | const { getCurrentScript, getScriptParameters } = (() => {
75 | const toFlagMap = (string) => {
76 | return string
77 | .split(',')
78 | .filter(Boolean)
79 | .reduce((flags, key) => ({
80 | ...flags,
81 | [key]: true
82 | }), {})
83 | }
84 |
85 | const getCurrentScript = (scriptName) => {
86 | const scripts = Array.from(document.getElementsByTagName('script'))
87 | const currentScript = scripts.find(
88 | script => script.src.includes(`/${scriptName}`)
89 | )
90 | return currentScript
91 | }
92 |
93 | const getScriptParameters = (currentScript) => {
94 | const query = currentScript.src.split('?').pop().split('&')
95 | const parameters = query.reduce((params, part) => {
96 | const [key, value] = part.split('=')
97 | return {
98 | ...params,
99 | [key]: value
100 | }
101 | }, {})
102 |
103 | if (!parameters) {
104 | return undefined
105 | }
106 |
107 | if (parameters.report) {
108 | return {
109 | ...parameters,
110 | report: toFlagMap(parameters.report)
111 | }
112 | }
113 |
114 | return parameters
115 | }
116 |
117 | return {
118 | getCurrentScript,
119 | getScriptParameters
120 | }
121 | })()
122 |
123 | const validations = (() => {
124 | const reportVariable = (...args) => {
125 | if (!args) {
126 | throw new Error('options are mandatory.')
127 | }
128 | if (typeof args[0] === 'string') {
129 | if (typeof args[1] === 'undefined') {
130 | throw new Error('2nd argument is necessary when only the variable name is present')
131 | }
132 | if (typeof args[1] === 'object' && typeof args[1].value === 'undefined') {
133 | throw new Error('`value` is mandatory')
134 | }
135 | } else if (typeof args[0] === 'object') {
136 | if (!args[0].name || !args[0].name.trim()) {
137 | throw new Error('`name` is mandatory')
138 | }
139 | if (typeof args[0].value === 'undefined') {
140 | throw new Error('`value` is mandatory')
141 | }
142 | } else {
143 | throw new Error('First argument must be a string or an object.')
144 | }
145 | }
146 |
147 | const reportPageScroll = (...args) => {
148 | const { direction, interpolations } = args[0]
149 | if (interpolations) {
150 | if (!direction) {
151 | throw new Error('"direction" must be provided for interpolations')
152 | }
153 | if (direction !== 'horizontal' && direction !== 'vertical') {
154 | throw new Error('"direction" can be only "horizontal" or "vertical".')
155 | }
156 | }
157 | }
158 |
159 | const reportScroll = (...args) => {
160 | if (!args[0]) {
161 | throw new Error('First argument must be name or options')
162 | }
163 | let direction
164 | if (typeof args[1] === 'string') {
165 | direction = args[1]
166 | } else if (typeof args[1] === 'object' && args[1].direction) {
167 | direction = args[1].direction
168 | }
169 | if (direction && direction !== 'horizontal' && direction !== 'vertical') {
170 | throw new Error('"direction" can be only "horizontal" or "vertical".')
171 | }
172 | }
173 |
174 | return {
175 | reportVariable,
176 | reportPageScroll,
177 | reportScroll
178 | }
179 | })()
180 |
181 | const setCSSProperty = (key, value, element = window.document.documentElement) => {
182 | element.style.setProperty(key, value)
183 | }
184 |
185 | const reportResponsiveVariable = (name, valueFn, scope) => {
186 | window.addEventListener('resize', () => setCSSProperty(name, valueFn(), scope))
187 | window.addEventListener('orientationchange', () => setCSSProperty(name, valueFn(), scope))
188 | setCSSProperty(name, valueFn(), scope)
189 | }
190 |
191 | const reportPageCursor = (event) => {
192 | let x, y
193 | if (event.touches) {
194 | x = event.touches.item(0).clientX
195 | y = event.touches.item(0).clientY
196 | } else {
197 | x = event.x
198 | y = event.y
199 | }
200 | setCSSProperty('--cursor-x', `${x}px`)
201 | setCSSProperty('--cursor-y',`${y}px`)
202 |
203 | const { innerWidth, innerHeight } = window
204 |
205 | setCSSProperty('--cursor-x-1', x / innerWidth)
206 | setCSSProperty('--cursor-y-1', y / innerHeight)
207 | }
208 |
209 | const reportPageScroll = ({ direction, interpolations }) => () => {
210 | validations.reportPageScroll({ direction, interpolations })
211 |
212 | const { scrollTop, scrollLeft } = document.documentElement
213 | setCSSProperty('--scroll-x', scrollLeft)
214 | setCSSProperty('--scroll-y', scrollTop)
215 |
216 | const { scrollWidth, scrollHeight } = document.documentElement
217 | const { innerWidth, innerHeight } = window
218 | setCSSProperty('--scroll-x-1', scrollLeft / (scrollWidth - innerWidth))
219 | setCSSProperty('--scroll-y-1', scrollTop / (scrollHeight - innerHeight))
220 |
221 | if (interpolations) {
222 | interpolations.forEach((interpolation, index) => {
223 | const { interpolationName, interpolated, scope } = performInterpolation({
224 | interpolation,
225 | id: index,
226 | value: direction === 'horizontal' ? scrollLeft : scrollTop
227 | })
228 | setCSSProperty(interpolationName, interpolated, scope)
229 | })
230 | }
231 | }
232 |
233 | const reportScroll = (...args) => (event) => {
234 | validations.reportScroll(...args)
235 |
236 | let name
237 | let direction = 'vertical'
238 | let interpolations
239 | if (typeof args[0] === 'string') {
240 | name = args[0]
241 |
242 | if (typeof args[1] === 'string') {
243 | direction = args[1]
244 | } else if (typeof args[1] === 'object') {
245 | direction = args[1].direction || direction
246 | interpolations = args[1].interpolations
247 | }
248 |
249 | } else if (typeof args[0] === 'object') {
250 | name = args[0].name
251 | direction = args[0].direction
252 | interpolations = args[0].interpolations
253 | }
254 |
255 | const { target } = event
256 | let absoluteScroll
257 | let targetScrollSize
258 | let targetSize
259 | if (direction === 'horizontal') {
260 | absoluteScroll = target.scrollLeft
261 | targetScrollSize = target.scrollWidth
262 | targetSize = target.clientWidth
263 | } else if (direction === 'vertical') {
264 | absoluteScroll = target.scrollTop
265 | targetScrollSize = target.scrollHeight
266 | targetSize = target.clientHeight
267 | }
268 | setCSSProperty(name, absoluteScroll)
269 | setCSSProperty(`${name}-1`, absoluteScroll / (targetScrollSize - targetSize))
270 |
271 | if (interpolations) {
272 | interpolations.forEach((interpolation, index) => {
273 | const { interpolationName, interpolated, scope } = performInterpolation({
274 | interpolation,
275 | id: index,
276 | value: absoluteScroll
277 | })
278 | setCSSProperty(interpolationName, interpolated, scope)
279 | })
280 | }
281 | }
282 |
283 | const reportVariable = (...args) => {
284 | validations.reportVariable(...args)
285 | let name
286 | let value
287 | let scope
288 | if (typeof args[0] === 'string') {
289 | name = args[0]
290 | if (typeof args[1] === 'function') {
291 | return reportResponsiveVariable(name, args[1])
292 | }
293 | if (typeof args[1] === 'object') {
294 | scope = args[1].scope
295 | value = args[1].value
296 | if (typeof args[1].value === 'function') {
297 | return reportResponsiveVariable(name, args[1].value, scope)
298 | }
299 | } else {
300 | value = args[1]
301 | }
302 | setCSSProperty(name, value, scope)
303 |
304 | } else if (typeof args[0] === 'object') {
305 | name = args[0].name
306 | value = args[0].value
307 | scope = args[0].scope
308 | if (typeof value === 'function') {
309 | return reportResponsiveVariable(name, value, scope)
310 | }
311 | setCSSProperty(name, value, scope)
312 | }
313 | }
314 |
315 | const reportIndex = (selector, {
316 | indexVariableName = '--index',
317 | rowIndexVariableName = '--row-index',
318 | rowIndexBy
319 | } = {
320 | indexVariableName: '--index',
321 | rowIndexVariableName: '--row-index'
322 | }) => {
323 | const elements = Array.from(document.querySelectorAll(selector))
324 | elements.forEach((element, index) => {
325 | setCSSProperty(indexVariableName, index, element)
326 | if (typeof rowIndexBy === 'number') {
327 | const rowIndex = Math.floor(index / rowIndexBy)
328 | setCSSProperty(rowIndexVariableName, rowIndex, element)
329 | }
330 | })
331 | }
332 |
333 | const cursor = () => {
334 | window.addEventListener('mousemove', reportPageCursor)
335 | window.addEventListener('touchmove', reportPageCursor)
336 | reportPageCursor({ x: 0, y: 0 })
337 | }
338 |
339 | const time = () => {
340 | const reportSeconds = () => {
341 | const seconds = (Date.now() - start) / 1000
342 | reportVariable('--seconds', seconds)
343 | }
344 |
345 | const reportMilliseconds = () => {
346 | const milliseconds = (Date.now() - start)
347 | reportVariable('--milliseconds', milliseconds)
348 | millisecondsLoop = requestAnimationFrame(reportMilliseconds)
349 | }
350 |
351 | let start = Date.now()
352 | let secondsLoop = window.setInterval(reportSeconds, 1000)
353 | let millisecondsLoop = requestAnimationFrame(reportMilliseconds)
354 |
355 | return {
356 | clear() {
357 | window.clearInterval(secondsLoop)
358 | window.cancelAnimationFrame(millisecondsLoop)
359 | }
360 | }
361 | }
362 |
363 | const reportGlobals = ({ scroll, cursor } = { scroll: true, cursor: true }) => {
364 | if (cursor) {
365 | window.addEventListener('mousemove', reportPageCursor)
366 | window.addEventListener('touchmove', reportPageCursor)
367 | reportPageCursor({ x: 0, y: 0 })
368 | }
369 | if (scroll) {
370 | let interpolations = scroll.interpolations
371 | let direction = scroll.direction
372 | window.addEventListener('scroll', reportPageScroll({
373 | direction,
374 | interpolations
375 | }))
376 | window.addEventListener('resize', (e) => {
377 | purgeRangeCache()
378 | reportPageScroll({
379 | direction,
380 | interpolations
381 | })(e)
382 | })
383 | reportPageScroll({
384 | direction,
385 | interpolations
386 | })()
387 | }
388 | }
389 |
390 | const currentScript = getCurrentScript('kicss.js')
391 | if (currentScript) {
392 | const queryParameters = getScriptParameters(currentScript)
393 | if (queryParameters && queryParameters.report) {
394 | const globalsToReport = queryParameters.report
395 | reportGlobals(globalsToReport)
396 | }
397 | window.kicss = {
398 | reportScroll,
399 | reportVariable,
400 | reportIndex,
401 | reportGlobals,
402 | cursor,
403 | time
404 | }
405 | }
406 |
407 | export {
408 | reportScroll,
409 | reportVariable,
410 | reportIndex,
411 | reportGlobals,
412 | cursor,
413 | time
414 | }
415 |
416 | export default {
417 | reportScroll,
418 | reportVariable,
419 | reportIndex,
420 | reportGlobals,
421 | cursor,
422 | time
423 | }
424 |
--------------------------------------------------------------------------------