chore: additional formatting w/prettier
This commit is contained in:
parent
ea75e585e1
commit
ee77555c32
39 changed files with 1544 additions and 1584 deletions
|
@ -15,16 +15,16 @@ My next.js api looks like this:
|
|||
|
||||
```typescript
|
||||
export default async function handler(req: any, res: any) {
|
||||
const KEY_CORYD = process.env.API_KEY_WEBMENTIONS_CORYD_DEV
|
||||
const KEY_BLOG = process.env.API_KEY_WEBMENTIONS_BLOG_CORYD_DEV
|
||||
const DOMAIN = req.query.domain
|
||||
const TARGET = req.query.target
|
||||
const data = await fetch(
|
||||
`https://webmention.io/api/mentions.jf2?token=${
|
||||
DOMAIN === 'coryd.dev' ? KEY_CORYD : KEY_BLOG
|
||||
}${TARGET ? `&target=${TARGET}` : ''}&per-page=1000`
|
||||
).then((response) => response.json())
|
||||
res.json(data)
|
||||
const KEY_CORYD = process.env.API_KEY_WEBMENTIONS_CORYD_DEV
|
||||
const KEY_BLOG = process.env.API_KEY_WEBMENTIONS_BLOG_CORYD_DEV
|
||||
const DOMAIN = req.query.domain
|
||||
const TARGET = req.query.target
|
||||
const data = await fetch(
|
||||
`https://webmention.io/api/mentions.jf2?token=${DOMAIN === 'coryd.dev' ? KEY_CORYD : KEY_BLOG}${
|
||||
TARGET ? `&target=${TARGET}` : ''
|
||||
}&per-page=1000`
|
||||
).then((response) => response.json())
|
||||
res.json(data)
|
||||
}
|
||||
```
|
||||
|
||||
|
@ -34,91 +34,85 @@ This is called on the client side as follows:
|
|||
|
||||
```javascript
|
||||
document.addEventListener('DOMContentLoaded', (event) => {
|
||||
;(function () {
|
||||
const formatDate = (date) => {
|
||||
var d = new Date(date),
|
||||
month = '' + (d.getMonth() + 1),
|
||||
day = '' + d.getDate(),
|
||||
year = d.getFullYear()
|
||||
;(function () {
|
||||
const formatDate = (date) => {
|
||||
var d = new Date(date),
|
||||
month = '' + (d.getMonth() + 1),
|
||||
day = '' + d.getDate(),
|
||||
year = d.getFullYear()
|
||||
|
||||
if (month.length < 2) month = '0' + month
|
||||
if (day.length < 2) day = '0' + day
|
||||
if (month.length < 2) month = '0' + month
|
||||
if (day.length < 2) day = '0' + day
|
||||
|
||||
return [month, day, year].join('-')
|
||||
}
|
||||
const webmentionsWrapper = document.getElementById('webmentions')
|
||||
const webmentionsLikesWrapper = document.getElementById('webmentions-likes-wrapper')
|
||||
const webmentionsBoostsWrapper = document.getElementById('webmentions-boosts-wrapper')
|
||||
const webmentionsCommentsWrapper = document.getElementById('webmentions-comments-wrapper')
|
||||
if (webmentionsWrapper && window) {
|
||||
try {
|
||||
fetch('https://utils.coryd.dev/api/webmentions?domain=blog.coryd.dev')
|
||||
.then((response) => response.json())
|
||||
.then((data) => {
|
||||
const mentions = data.children
|
||||
if (mentions.length === 0 || window.location.pathname === '/') {
|
||||
webmentionsWrapper.remove()
|
||||
return
|
||||
}
|
||||
|
||||
let likes = ''
|
||||
let boosts = ''
|
||||
let comments = ''
|
||||
|
||||
mentions.map((mention) => {
|
||||
if (
|
||||
mention['wm-property'] === 'like-of' &&
|
||||
mention['wm-target'].includes(window.location.href)
|
||||
) {
|
||||
likes += `<a href="${mention.url}" rel="noopener noreferrer"><img class="avatar" src="${mention.author.photo}" alt="${mention.author.name}" /></a>`
|
||||
}
|
||||
|
||||
if (
|
||||
mention['wm-property'] === 'repost-of' &&
|
||||
mention['wm-target'].includes(window.location.href)
|
||||
) {
|
||||
boosts += `<a href="${mention.url}" rel="noopener noreferrer"><img class="avatar" src="${mention.author.photo}" alt="${mention.author.name}" /></a>`
|
||||
}
|
||||
|
||||
if (
|
||||
mention['wm-property'] === 'in-reply-to' &&
|
||||
mention['wm-target'].includes(window.location.href)
|
||||
) {
|
||||
comments += `<div class="webmention-comment"><a href="${
|
||||
mention.url
|
||||
}" rel="noopener noreferrer"><div class="webmention-comment-top"><img class="avatar" src="${
|
||||
mention.author.photo
|
||||
}" alt="${mention.author.name}" /><div class="time">${formatDate(
|
||||
mention.published
|
||||
)}</div></div><div class="comment-body">${
|
||||
mention.content.text
|
||||
}</div></a></div>`
|
||||
}
|
||||
})
|
||||
|
||||
webmentionsLikesWrapper.innerHTML = ''
|
||||
webmentionsLikesWrapper.insertAdjacentHTML('beforeEnd', likes)
|
||||
webmentionsBoostsWrapper.innerHTML = ''
|
||||
webmentionsBoostsWrapper.insertAdjacentHTML('beforeEnd', boosts)
|
||||
webmentionsCommentsWrapper.innerHTML = ''
|
||||
webmentionsCommentsWrapper.insertAdjacentHTML('beforeEnd', comments)
|
||||
webmentionsWrapper.style.opacity = 1
|
||||
|
||||
if (likes === '')
|
||||
document.getElementById('webmentions-likes').innerHTML === ''
|
||||
if (boosts === '')
|
||||
document.getElementById('webmentions-boosts').innerHTML === ''
|
||||
if (comments === '')
|
||||
document.getElementById('webmentions-comments').innerHTML === ''
|
||||
|
||||
if (likes === '' && boosts === '' && comments === '')
|
||||
webmentionsWrapper.remove()
|
||||
})
|
||||
} catch (e) {
|
||||
webmentionsWrapper.remove()
|
||||
return [month, day, year].join('-')
|
||||
}
|
||||
const webmentionsWrapper = document.getElementById('webmentions')
|
||||
const webmentionsLikesWrapper = document.getElementById('webmentions-likes-wrapper')
|
||||
const webmentionsBoostsWrapper = document.getElementById('webmentions-boosts-wrapper')
|
||||
const webmentionsCommentsWrapper = document.getElementById('webmentions-comments-wrapper')
|
||||
if (webmentionsWrapper && window) {
|
||||
try {
|
||||
fetch('https://utils.coryd.dev/api/webmentions?domain=blog.coryd.dev')
|
||||
.then((response) => response.json())
|
||||
.then((data) => {
|
||||
const mentions = data.children
|
||||
if (mentions.length === 0 || window.location.pathname === '/') {
|
||||
webmentionsWrapper.remove()
|
||||
return
|
||||
}
|
||||
}
|
||||
})()
|
||||
|
||||
let likes = ''
|
||||
let boosts = ''
|
||||
let comments = ''
|
||||
|
||||
mentions.map((mention) => {
|
||||
if (
|
||||
mention['wm-property'] === 'like-of' &&
|
||||
mention['wm-target'].includes(window.location.href)
|
||||
) {
|
||||
likes += `<a href="${mention.url}" rel="noopener noreferrer"><img class="avatar" src="${mention.author.photo}" alt="${mention.author.name}" /></a>`
|
||||
}
|
||||
|
||||
if (
|
||||
mention['wm-property'] === 'repost-of' &&
|
||||
mention['wm-target'].includes(window.location.href)
|
||||
) {
|
||||
boosts += `<a href="${mention.url}" rel="noopener noreferrer"><img class="avatar" src="${mention.author.photo}" alt="${mention.author.name}" /></a>`
|
||||
}
|
||||
|
||||
if (
|
||||
mention['wm-property'] === 'in-reply-to' &&
|
||||
mention['wm-target'].includes(window.location.href)
|
||||
) {
|
||||
comments += `<div class="webmention-comment"><a href="${
|
||||
mention.url
|
||||
}" rel="noopener noreferrer"><div class="webmention-comment-top"><img class="avatar" src="${
|
||||
mention.author.photo
|
||||
}" alt="${mention.author.name}" /><div class="time">${formatDate(
|
||||
mention.published
|
||||
)}</div></div><div class="comment-body">${mention.content.text}</div></a></div>`
|
||||
}
|
||||
})
|
||||
|
||||
webmentionsLikesWrapper.innerHTML = ''
|
||||
webmentionsLikesWrapper.insertAdjacentHTML('beforeEnd', likes)
|
||||
webmentionsBoostsWrapper.innerHTML = ''
|
||||
webmentionsBoostsWrapper.insertAdjacentHTML('beforeEnd', boosts)
|
||||
webmentionsCommentsWrapper.innerHTML = ''
|
||||
webmentionsCommentsWrapper.insertAdjacentHTML('beforeEnd', comments)
|
||||
webmentionsWrapper.style.opacity = 1
|
||||
|
||||
if (likes === '') document.getElementById('webmentions-likes').innerHTML === ''
|
||||
if (boosts === '') document.getElementById('webmentions-boosts').innerHTML === ''
|
||||
if (comments === '') document.getElementById('webmentions-comments').innerHTML === ''
|
||||
|
||||
if (likes === '' && boosts === '' && comments === '') webmentionsWrapper.remove()
|
||||
})
|
||||
} catch (e) {
|
||||
webmentionsWrapper.remove()
|
||||
}
|
||||
}
|
||||
})()
|
||||
})
|
||||
```
|
||||
|
||||
|
@ -128,18 +122,18 @@ The webmentions HTML shell is as follows:
|
|||
|
||||
```html
|
||||
<div id="webmentions" class="background-purple container">
|
||||
<div id="webmentions-likes">
|
||||
<h2><i class="fa-solid fa-fw fa-star"></i> Likes</h2>
|
||||
<div id="webmentions-likes-wrapper"></div>
|
||||
</div>
|
||||
<div id="webmentions-boosts">
|
||||
<h2><i class="fa-solid fa-fw fa-rocket"></i> Boosts</h2>
|
||||
<div id="webmentions-boosts-wrapper"></div>
|
||||
</div>
|
||||
<div id="webmentions-comments">
|
||||
<h2><i class="fa-solid fa-fw fa-comment"></i> Comments</h2>
|
||||
<div id="webmentions-comments-wrapper"></div>
|
||||
</div>
|
||||
<div id="webmentions-likes">
|
||||
<h2><i class="fa-solid fa-fw fa-star"></i> Likes</h2>
|
||||
<div id="webmentions-likes-wrapper"></div>
|
||||
</div>
|
||||
<div id="webmentions-boosts">
|
||||
<h2><i class="fa-solid fa-fw fa-rocket"></i> Boosts</h2>
|
||||
<div id="webmentions-boosts-wrapper"></div>
|
||||
</div>
|
||||
<div id="webmentions-comments">
|
||||
<h2><i class="fa-solid fa-fw fa-comment"></i> Comments</h2>
|
||||
<div id="webmentions-comments-wrapper"></div>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
|
|
|
@ -21,19 +21,19 @@ I'm already exposing my most recently listened tracks and actively read books on
|
|||
|
||||
```typescript
|
||||
export default async function handler(req: any, res: any) {
|
||||
const KEY = process.env.API_KEY_LASTFM
|
||||
const METHODS: { [key: string]: string } = {
|
||||
default: 'user.getrecenttracks',
|
||||
albums: 'user.gettopalbums',
|
||||
artists: 'user.gettopartists',
|
||||
}
|
||||
const METHOD = METHODS[req.query.type] || METHODS['default']
|
||||
const data = await fetch(
|
||||
`http://ws.audioscrobbler.com/2.0/?method=${METHOD}&user=cdme_&api_key=${KEY}&limit=${
|
||||
req.query.limit || 20
|
||||
}&format=${req.query.format || 'json'}&period=${req.query.period || 'overall'}`
|
||||
).then((response) => response.json())
|
||||
res.json(data)
|
||||
const KEY = process.env.API_KEY_LASTFM
|
||||
const METHODS: { [key: string]: string } = {
|
||||
default: 'user.getrecenttracks',
|
||||
albums: 'user.gettopalbums',
|
||||
artists: 'user.gettopartists',
|
||||
}
|
||||
const METHOD = METHODS[req.query.type] || METHODS['default']
|
||||
const data = await fetch(
|
||||
`http://ws.audioscrobbler.com/2.0/?method=${METHOD}&user=cdme_&api_key=${KEY}&limit=${
|
||||
req.query.limit || 20
|
||||
}&format=${req.query.format || 'json'}&period=${req.query.period || 'overall'}`
|
||||
).then((response) => response.json())
|
||||
res.json(data)
|
||||
}
|
||||
```
|
||||
|
||||
|
@ -45,24 +45,24 @@ Last.fm's API returns album images, but no longer returns artist images. To solv
|
|||
import siteMetadata from '@/data/siteMetadata'
|
||||
|
||||
export default async function handler(req: any, res: any) {
|
||||
const env = process.env.NODE_ENV
|
||||
let host = siteMetadata.siteUrl
|
||||
if (env === 'development') host = 'http://localhost:3000'
|
||||
const ARTIST = req.query.artist
|
||||
const ALBUM = req.query.album
|
||||
const MEDIA = ARTIST ? 'artists' : 'albums'
|
||||
const MEDIA_VAL = ARTIST ? ARTIST : ALBUM
|
||||
const env = process.env.NODE_ENV
|
||||
let host = siteMetadata.siteUrl
|
||||
if (env === 'development') host = 'http://localhost:3000'
|
||||
const ARTIST = req.query.artist
|
||||
const ALBUM = req.query.album
|
||||
const MEDIA = ARTIST ? 'artists' : 'albums'
|
||||
const MEDIA_VAL = ARTIST ? ARTIST : ALBUM
|
||||
|
||||
const data = await fetch(`${host}/media/${MEDIA}/${MEDIA_VAL}.jpg`)
|
||||
.then((response) => {
|
||||
if (response.status === 200) return `${host}/media/${MEDIA}/${MEDIA_VAL}.jpg`
|
||||
fetch(
|
||||
`${host}/api/omg/paste-edit?paste=404-images&editType=append&content=${MEDIA_VAL}`
|
||||
).then((response) => response.json())
|
||||
return `${host}/media/404.jpg`
|
||||
})
|
||||
.then((image) => image)
|
||||
res.redirect(data)
|
||||
const data = await fetch(`${host}/media/${MEDIA}/${MEDIA_VAL}.jpg`)
|
||||
.then((response) => {
|
||||
if (response.status === 200) return `${host}/media/${MEDIA}/${MEDIA_VAL}.jpg`
|
||||
fetch(
|
||||
`${host}/api/omg/paste-edit?paste=404-images&editType=append&content=${MEDIA_VAL}`
|
||||
).then((response) => response.json())
|
||||
return `${host}/media/404.jpg`
|
||||
})
|
||||
.then((image) => image)
|
||||
res.redirect(data)
|
||||
}
|
||||
```
|
||||
|
||||
|
@ -73,12 +73,12 @@ import { extract } from '@extractus/feed-extractor'
|
|||
import siteMetadata from '@/data/siteMetadata'
|
||||
|
||||
export default async function handler(req: any, res: any) {
|
||||
const env = process.env.NODE_ENV
|
||||
let host = siteMetadata.siteUrl
|
||||
if (env === 'development') host = 'http://localhost:3000'
|
||||
const url = `${host}/feeds/books`
|
||||
const result = await extract(url)
|
||||
res.json(result)
|
||||
const env = process.env.NODE_ENV
|
||||
let host = siteMetadata.siteUrl
|
||||
if (env === 'development') host = 'http://localhost:3000'
|
||||
const url = `${host}/feeds/books`
|
||||
const result = await extract(url)
|
||||
res.json(result)
|
||||
}
|
||||
```
|
||||
|
||||
|
@ -89,20 +89,20 @@ import { extract } from '@extractus/feed-extractor'
|
|||
import siteMetadata from '@/data/siteMetadata'
|
||||
|
||||
export default async function handler(req: any, res: any) {
|
||||
const KEY = process.env.API_KEY_TRAKT
|
||||
const env = process.env.NODE_ENV
|
||||
let host = siteMetadata.siteUrl
|
||||
if (env === 'development') host = 'http://localhost:3000'
|
||||
const url = `${host}/feeds/tv?slurm=${KEY}`
|
||||
const result = await extract(url, {
|
||||
getExtraEntryFields: (feedEntry) => {
|
||||
return {
|
||||
image: feedEntry['media:content']['@_url'],
|
||||
thumbnail: feedEntry['media:thumbnail']['@_url'],
|
||||
}
|
||||
},
|
||||
})
|
||||
res.json(result)
|
||||
const KEY = process.env.API_KEY_TRAKT
|
||||
const env = process.env.NODE_ENV
|
||||
let host = siteMetadata.siteUrl
|
||||
if (env === 'development') host = 'http://localhost:3000'
|
||||
const url = `${host}/feeds/tv?slurm=${KEY}`
|
||||
const result = await extract(url, {
|
||||
getExtraEntryFields: (feedEntry) => {
|
||||
return {
|
||||
image: feedEntry['media:content']['@_url'],
|
||||
thumbnail: feedEntry['media:thumbnail']['@_url'],
|
||||
}
|
||||
},
|
||||
})
|
||||
res.json(result)
|
||||
}
|
||||
```
|
||||
|
||||
|
@ -113,12 +113,12 @@ import { extract } from '@extractus/feed-extractor'
|
|||
import siteMetadata from '@/data/siteMetadata'
|
||||
|
||||
export default async function handler(req: any, res: any) {
|
||||
const env = process.env.NODE_ENV
|
||||
let host = siteMetadata.siteUrl
|
||||
if (env === 'development') host = 'http://localhost:3000'
|
||||
const url = `${host}/feeds/movies`
|
||||
const result = await extract(url)
|
||||
res.json(result)
|
||||
const env = process.env.NODE_ENV
|
||||
let host = siteMetadata.siteUrl
|
||||
if (env === 'development') host = 'http://localhost:3000'
|
||||
const url = `${host}/feeds/movies`
|
||||
const result = await extract(url)
|
||||
res.json(result)
|
||||
}
|
||||
```
|
||||
|
||||
|
@ -133,199 +133,178 @@ import { nowResponseToMarkdown } from '@/utils/transforms'
|
|||
import { ALBUM_DENYLIST } from '@/utils/constants'
|
||||
|
||||
export default async function handler(req: any, res: any) {
|
||||
const env = process.env.NODE_ENV
|
||||
const { APP_KEY_OMG, API_KEY_OMG } = process.env
|
||||
const ACTION_KEY = req.headers.authorization?.split(' ')[1]
|
||||
const env = process.env.NODE_ENV
|
||||
const { APP_KEY_OMG, API_KEY_OMG } = process.env
|
||||
const ACTION_KEY = req.headers.authorization?.split(' ')[1]
|
||||
|
||||
let host = siteMetadata.siteUrl
|
||||
if (env === 'development') host = 'http://localhost:3000'
|
||||
let host = siteMetadata.siteUrl
|
||||
if (env === 'development') host = 'http://localhost:3000'
|
||||
|
||||
try {
|
||||
if (ACTION_KEY === APP_KEY_OMG) {
|
||||
const now = await fetch('https://api.omg.lol/address/cory/pastebin/now.yaml')
|
||||
.then((res) => res.json())
|
||||
.then((json) => {
|
||||
const now = jsYaml.load(json.response.paste.content)
|
||||
Object.keys(jsYaml.load(json.response.paste.content)).forEach((key) => {
|
||||
now[key] = listsToMarkdown(now[key])
|
||||
})
|
||||
try {
|
||||
if (ACTION_KEY === APP_KEY_OMG) {
|
||||
const now = await fetch('https://api.omg.lol/address/cory/pastebin/now.yaml')
|
||||
.then((res) => res.json())
|
||||
.then((json) => {
|
||||
const now = jsYaml.load(json.response.paste.content)
|
||||
Object.keys(jsYaml.load(json.response.paste.content)).forEach((key) => {
|
||||
now[key] = listsToMarkdown(now[key])
|
||||
})
|
||||
|
||||
return { now }
|
||||
})
|
||||
return { now }
|
||||
})
|
||||
|
||||
const books = await fetch(`${host}/api/books`)
|
||||
.then((res) => res.json())
|
||||
.then((json) => {
|
||||
const data = json.entries
|
||||
.slice(0, 5)
|
||||
.map((book: { title: string; link: string }) => {
|
||||
return {
|
||||
title: book.title,
|
||||
link: book.link,
|
||||
}
|
||||
})
|
||||
return {
|
||||
json: data,
|
||||
md: data
|
||||
.map((d: any) => {
|
||||
return `- [${d.title}](${d.link}) {${getRandomIcon('books')}}`
|
||||
})
|
||||
.join('\n'),
|
||||
}
|
||||
})
|
||||
const books = await fetch(`${host}/api/books`)
|
||||
.then((res) => res.json())
|
||||
.then((json) => {
|
||||
const data = json.entries.slice(0, 5).map((book: { title: string; link: string }) => {
|
||||
return {
|
||||
title: book.title,
|
||||
link: book.link,
|
||||
}
|
||||
})
|
||||
return {
|
||||
json: data,
|
||||
md: data
|
||||
.map((d: any) => {
|
||||
return `- [${d.title}](${d.link}) {${getRandomIcon('books')}}`
|
||||
})
|
||||
.join('\n'),
|
||||
}
|
||||
})
|
||||
|
||||
const movies = await fetch(`${host}/api/movies`)
|
||||
.then((res) => res.json())
|
||||
.then((json) => {
|
||||
const data = json.entries
|
||||
.slice(0, 5)
|
||||
.map((movie: { title: string; link: string; description: string }) => {
|
||||
return {
|
||||
title: movie.title,
|
||||
link: movie.link,
|
||||
desc: movie.description,
|
||||
}
|
||||
})
|
||||
return {
|
||||
json: data,
|
||||
md: data
|
||||
.map((d: any) => {
|
||||
return `- [${d.title}](${d.link}): ${d.desc} {${getRandomIcon(
|
||||
'movies'
|
||||
)}}`
|
||||
})
|
||||
.join('\n'),
|
||||
}
|
||||
})
|
||||
|
||||
const tv = await fetch(`${host}/api/tv`)
|
||||
.then((res) => res.json())
|
||||
.then((json) => {
|
||||
const data = json.entries
|
||||
.splice(0, 5)
|
||||
.map(
|
||||
(episode: {
|
||||
title: string
|
||||
link: string
|
||||
image: string
|
||||
thumbnail: string
|
||||
}) => {
|
||||
return {
|
||||
title: episode.title,
|
||||
link: episode.link,
|
||||
image: episode.image,
|
||||
thumbnail: episode.thumbnail,
|
||||
}
|
||||
}
|
||||
)
|
||||
return {
|
||||
json: data,
|
||||
html: data
|
||||
.map((d: any) => {
|
||||
return `<div class="container"><a href=${d.link} title='${d.title} by ${d.artist}'><div class='cover'></div><div class='details'><div class='text-main'>${d.title}</div></div><img src='${d.thumbnail}' alt='${d.title}' /></div></a>`
|
||||
})
|
||||
.join('\n'),
|
||||
md: data
|
||||
.map((d: any) => {
|
||||
return `- [${d.title}](${d.link}) {${getRandomIcon('tv')}}`
|
||||
})
|
||||
.join('\n'),
|
||||
}
|
||||
})
|
||||
|
||||
const musicArtists = await fetch(
|
||||
`https://utils.coryd.dev/api/music?type=artists&period=7day&limit=8`
|
||||
)
|
||||
.then((res) => res.json())
|
||||
.then((json) => {
|
||||
const data = json.topartists.artist.map((a: any) => {
|
||||
return {
|
||||
artist: a.name,
|
||||
link: `https://rateyourmusic.com/search?searchterm=${encodeURIComponent(
|
||||
a.name
|
||||
)}`,
|
||||
image: `${host}/api/media?artist=${a.name
|
||||
.replace(/\s+/g, '-')
|
||||
.toLowerCase()}`,
|
||||
}
|
||||
})
|
||||
return {
|
||||
json: data,
|
||||
html: data
|
||||
.map((d: any) => {
|
||||
return `<div class="container"><a href=${d.link} title='${d.title} by ${d.artist}'><div class='cover'></div><div class='details'><div class='text-main'>${d.artist}</div></div><img src='${d.image}' alt='${d.artist}' /></div></a>`
|
||||
})
|
||||
.join('\n'),
|
||||
md: data
|
||||
.map((d: any) => {
|
||||
return `- [${d.artist}](${d.link}) {${getRandomIcon('music')}}`
|
||||
})
|
||||
.join('\n'),
|
||||
}
|
||||
})
|
||||
|
||||
const musicAlbums = await fetch(
|
||||
`https://utils.coryd.dev/api/music?type=albums&period=7day&limit=8`
|
||||
)
|
||||
.then((res) => res.json())
|
||||
.then((json) => {
|
||||
const data = json.topalbums.album.map((a: any) => ({
|
||||
title: a.name,
|
||||
artist: a.artist.name,
|
||||
link: `https://rateyourmusic.com/search?searchterm=${encodeURIComponent(
|
||||
a.name
|
||||
)}`,
|
||||
image: !ALBUM_DENYLIST.includes(a.name.replace(/\s+/g, '-').toLowerCase())
|
||||
? a.image[a.image.length - 1]['#text']
|
||||
: `${host}/api/media?album=${a.name
|
||||
.replace(/\s+/g, '-')
|
||||
.toLowerCase()}`,
|
||||
}))
|
||||
return {
|
||||
json: data,
|
||||
html: data
|
||||
.map((d: any) => {
|
||||
return `<div class="container"><a href=${d.link} title='${d.title} by ${d.artist}'><div class='cover'></div><div class='details'><div class='text-main'>${d.title}</div><div class='text-secondary'>${d.artist}</div></div><img src='${d.image}' alt='${d.title} by ${d.artist}' /></div></a>`
|
||||
})
|
||||
.join('\n'),
|
||||
md: data
|
||||
.map((d: any) => {
|
||||
return `- [${d.title}](${d.link}) by ${d.artist} {${getRandomIcon(
|
||||
'music'
|
||||
)}}`
|
||||
})
|
||||
.join('\n'),
|
||||
}
|
||||
})
|
||||
|
||||
fetch('https://api.omg.lol/address/cory/now', {
|
||||
method: 'post',
|
||||
headers: {
|
||||
Authorization: `Bearer ${API_KEY_OMG}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
content: nowResponseToMarkdown({
|
||||
now,
|
||||
books,
|
||||
movies,
|
||||
tv,
|
||||
music: {
|
||||
artists: musicArtists,
|
||||
albums: musicAlbums,
|
||||
},
|
||||
}),
|
||||
listed: 1,
|
||||
}),
|
||||
const movies = await fetch(`${host}/api/movies`)
|
||||
.then((res) => res.json())
|
||||
.then((json) => {
|
||||
const data = json.entries
|
||||
.slice(0, 5)
|
||||
.map((movie: { title: string; link: string; description: string }) => {
|
||||
return {
|
||||
title: movie.title,
|
||||
link: movie.link,
|
||||
desc: movie.description,
|
||||
}
|
||||
})
|
||||
return {
|
||||
json: data,
|
||||
md: data
|
||||
.map((d: any) => {
|
||||
return `- [${d.title}](${d.link}): ${d.desc} {${getRandomIcon('movies')}}`
|
||||
})
|
||||
.join('\n'),
|
||||
}
|
||||
})
|
||||
|
||||
res.status(200).json({ success: true })
|
||||
} else {
|
||||
res.status(401).json({ success: false })
|
||||
}
|
||||
} catch (err) {
|
||||
res.status(500).json({ success: false })
|
||||
const tv = await fetch(`${host}/api/tv`)
|
||||
.then((res) => res.json())
|
||||
.then((json) => {
|
||||
const data = json.entries
|
||||
.splice(0, 5)
|
||||
.map((episode: { title: string; link: string; image: string; thumbnail: string }) => {
|
||||
return {
|
||||
title: episode.title,
|
||||
link: episode.link,
|
||||
image: episode.image,
|
||||
thumbnail: episode.thumbnail,
|
||||
}
|
||||
})
|
||||
return {
|
||||
json: data,
|
||||
html: data
|
||||
.map((d: any) => {
|
||||
return `<div class="container"><a href=${d.link} title='${d.title} by ${d.artist}'><div class='cover'></div><div class='details'><div class='text-main'>${d.title}</div></div><img src='${d.thumbnail}' alt='${d.title}' /></div></a>`
|
||||
})
|
||||
.join('\n'),
|
||||
md: data
|
||||
.map((d: any) => {
|
||||
return `- [${d.title}](${d.link}) {${getRandomIcon('tv')}}`
|
||||
})
|
||||
.join('\n'),
|
||||
}
|
||||
})
|
||||
|
||||
const musicArtists = await fetch(
|
||||
`https://utils.coryd.dev/api/music?type=artists&period=7day&limit=8`
|
||||
)
|
||||
.then((res) => res.json())
|
||||
.then((json) => {
|
||||
const data = json.topartists.artist.map((a: any) => {
|
||||
return {
|
||||
artist: a.name,
|
||||
link: `https://rateyourmusic.com/search?searchterm=${encodeURIComponent(a.name)}`,
|
||||
image: `${host}/api/media?artist=${a.name.replace(/\s+/g, '-').toLowerCase()}`,
|
||||
}
|
||||
})
|
||||
return {
|
||||
json: data,
|
||||
html: data
|
||||
.map((d: any) => {
|
||||
return `<div class="container"><a href=${d.link} title='${d.title} by ${d.artist}'><div class='cover'></div><div class='details'><div class='text-main'>${d.artist}</div></div><img src='${d.image}' alt='${d.artist}' /></div></a>`
|
||||
})
|
||||
.join('\n'),
|
||||
md: data
|
||||
.map((d: any) => {
|
||||
return `- [${d.artist}](${d.link}) {${getRandomIcon('music')}}`
|
||||
})
|
||||
.join('\n'),
|
||||
}
|
||||
})
|
||||
|
||||
const musicAlbums = await fetch(
|
||||
`https://utils.coryd.dev/api/music?type=albums&period=7day&limit=8`
|
||||
)
|
||||
.then((res) => res.json())
|
||||
.then((json) => {
|
||||
const data = json.topalbums.album.map((a: any) => ({
|
||||
title: a.name,
|
||||
artist: a.artist.name,
|
||||
link: `https://rateyourmusic.com/search?searchterm=${encodeURIComponent(a.name)}`,
|
||||
image: !ALBUM_DENYLIST.includes(a.name.replace(/\s+/g, '-').toLowerCase())
|
||||
? a.image[a.image.length - 1]['#text']
|
||||
: `${host}/api/media?album=${a.name.replace(/\s+/g, '-').toLowerCase()}`,
|
||||
}))
|
||||
return {
|
||||
json: data,
|
||||
html: data
|
||||
.map((d: any) => {
|
||||
return `<div class="container"><a href=${d.link} title='${d.title} by ${d.artist}'><div class='cover'></div><div class='details'><div class='text-main'>${d.title}</div><div class='text-secondary'>${d.artist}</div></div><img src='${d.image}' alt='${d.title} by ${d.artist}' /></div></a>`
|
||||
})
|
||||
.join('\n'),
|
||||
md: data
|
||||
.map((d: any) => {
|
||||
return `- [${d.title}](${d.link}) by ${d.artist} {${getRandomIcon('music')}}`
|
||||
})
|
||||
.join('\n'),
|
||||
}
|
||||
})
|
||||
|
||||
fetch('https://api.omg.lol/address/cory/now', {
|
||||
method: 'post',
|
||||
headers: {
|
||||
Authorization: `Bearer ${API_KEY_OMG}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
content: nowResponseToMarkdown({
|
||||
now,
|
||||
books,
|
||||
movies,
|
||||
tv,
|
||||
music: {
|
||||
artists: musicArtists,
|
||||
albums: musicAlbums,
|
||||
},
|
||||
}),
|
||||
listed: 1,
|
||||
}),
|
||||
})
|
||||
|
||||
res.status(200).json({ success: true })
|
||||
} else {
|
||||
res.status(401).json({ success: false })
|
||||
}
|
||||
} catch (err) {
|
||||
res.status(500).json({ success: false })
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
@ -335,14 +314,14 @@ For items displayed from Markdown I'm attaching a random FontAwesome icon (e.g.
|
|||
|
||||
```typescript
|
||||
export const getRandomIcon = (type: string) => {
|
||||
const icons = {
|
||||
books: ['book', 'book-bookmark', 'book-open', 'book-open-reader', 'bookmark'],
|
||||
music: ['music', 'headphones', 'record-vinyl', 'radio', 'guitar', 'compact-disc'],
|
||||
movies: ['film', 'display', 'video', 'ticket'],
|
||||
tv: ['tv', 'display', 'video'],
|
||||
}
|
||||
const icons = {
|
||||
books: ['book', 'book-bookmark', 'book-open', 'book-open-reader', 'bookmark'],
|
||||
music: ['music', 'headphones', 'record-vinyl', 'radio', 'guitar', 'compact-disc'],
|
||||
movies: ['film', 'display', 'video', 'ticket'],
|
||||
tv: ['tv', 'display', 'video'],
|
||||
}
|
||||
|
||||
return icons[type][Math.floor(Math.random() * (icons[type].length - 1 - 0))]
|
||||
return icons[type][Math.floor(Math.random() * (icons[type].length - 1 - 0))]
|
||||
}
|
||||
```
|
||||
|
||||
|
@ -351,16 +330,16 @@ As the final step to wrap this up, calls to `/api/now` are made every 8 hours us
|
|||
```yaml
|
||||
name: scheduled-cron-job
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 */8 * * *'
|
||||
schedule:
|
||||
- cron: '0 */8 * * *'
|
||||
jobs:
|
||||
cron:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: scheduled-cron-job
|
||||
run: |
|
||||
curl -X POST 'https://utils.coryd.dev/api/now' \
|
||||
-H 'Authorization: Bearer ${{ secrets.ACTION_KEY }}'
|
||||
cron:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: scheduled-cron-job
|
||||
run: |
|
||||
curl -X POST 'https://utils.coryd.dev/api/now' \
|
||||
-H 'Authorization: Bearer ${{ secrets.ACTION_KEY }}'
|
||||
```
|
||||
|
||||
This endpoint can also be manually called using another workflow:
|
||||
|
@ -369,21 +348,21 @@ This endpoint can also be manually called using another workflow:
|
|||
name: manual-job
|
||||
on: [workflow_dispatch]
|
||||
jobs:
|
||||
cron:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: manual-job
|
||||
run: |
|
||||
curl -X POST 'https://utils.coryd.dev/api/now' \
|
||||
-H 'Authorization: Bearer ${{ secrets.ACTION_KEY }}'
|
||||
cron:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: manual-job
|
||||
run: |
|
||||
curl -X POST 'https://utils.coryd.dev/api/now' \
|
||||
-H 'Authorization: Bearer ${{ secrets.ACTION_KEY }}'
|
||||
```
|
||||
|
||||
So far this works seamlessly — if I want to update or add static content I can do so via my yaml paste at paste.lol and the change will roll out in due time.
|
||||
|
||||
Questions? Comments? Feel free to get in touch:
|
||||
|
||||
- [Email](mailto:hi@coryd.dev)
|
||||
- [Mastodon](https://social.lol/@cory)
|
||||
- [Email](mailto:hi@coryd.dev)
|
||||
- [Mastodon](https://social.lol/@cory)
|
||||
|
||||
---
|
||||
|
||||
|
|
|
@ -16,100 +16,98 @@ import { SERVICES, TAGS } from './config'
|
|||
import createMastoPost from './createMastoPost'
|
||||
|
||||
export default async function syndicate(init?: string) {
|
||||
const TOKEN_CORYDDEV_GISTS = process.env.TOKEN_CORYDDEV_GISTS
|
||||
const GIST_ID_SYNDICATION_CACHE = '406166f337b9ed2d494951757a70b9d1'
|
||||
const GIST_NAME_SYNDICATION_CACHE = 'syndication-cache.json'
|
||||
const CLEAN_OBJECT = () => {
|
||||
const INIT_OBJECT = {}
|
||||
Object.keys(SERVICES).map((service) => (INIT_OBJECT[service] = []))
|
||||
return INIT_OBJECT
|
||||
}
|
||||
const TOKEN_CORYDDEV_GISTS = process.env.TOKEN_CORYDDEV_GISTS
|
||||
const GIST_ID_SYNDICATION_CACHE = '406166f337b9ed2d494951757a70b9d1'
|
||||
const GIST_NAME_SYNDICATION_CACHE = 'syndication-cache.json'
|
||||
const CLEAN_OBJECT = () => {
|
||||
const INIT_OBJECT = {}
|
||||
Object.keys(SERVICES).map((service) => (INIT_OBJECT[service] = []))
|
||||
return INIT_OBJECT
|
||||
}
|
||||
|
||||
async function hydrateCache() {
|
||||
const CACHE_DATA = CLEAN_OBJECT()
|
||||
for (const service in SERVICES) {
|
||||
const data = await extract(SERVICES[service])
|
||||
const entries = data?.entries
|
||||
entries.map((entry: FeedEntry) => CACHE_DATA[service].push(entry.id))
|
||||
async function hydrateCache() {
|
||||
const CACHE_DATA = CLEAN_OBJECT()
|
||||
for (const service in SERVICES) {
|
||||
const data = await extract(SERVICES[service])
|
||||
const entries = data?.entries
|
||||
entries.map((entry: FeedEntry) => CACHE_DATA[service].push(entry.id))
|
||||
}
|
||||
await fetch(`https://api.github.com/gists/${GIST_ID_SYNDICATION_CACHE}`, {
|
||||
method: 'PATCH',
|
||||
headers: {
|
||||
Authorization: `Bearer ${TOKEN_CORYDDEV_GISTS}`,
|
||||
'Content-Type': 'application/vnd.github+json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
gist_id: GIST_ID_SYNDICATION_CACHE,
|
||||
files: {
|
||||
'syndication-cache.json': {
|
||||
content: JSON.stringify(CACHE_DATA),
|
||||
},
|
||||
},
|
||||
}),
|
||||
})
|
||||
.then((response) => response.json())
|
||||
.catch((err) => console.log(err))
|
||||
}
|
||||
|
||||
const DATA = await fetch(`https://api.github.com/gists/${GIST_ID_SYNDICATION_CACHE}`).then(
|
||||
(response) => response.json()
|
||||
)
|
||||
const CONTENT = DATA?.files[GIST_NAME_SYNDICATION_CACHE].content
|
||||
|
||||
// rewrite the sync data if init is reset
|
||||
if (CONTENT === '' || init === 'true') hydrateCache()
|
||||
|
||||
if (CONTENT && CONTENT !== '' && !init) {
|
||||
const existingData = await fetch(
|
||||
`https://api.github.com/gists/${GIST_ID_SYNDICATION_CACHE}`
|
||||
).then((response) => response.json())
|
||||
const existingContent = JSON.parse(existingData?.files[GIST_NAME_SYNDICATION_CACHE].content)
|
||||
|
||||
for (const service in SERVICES) {
|
||||
const data = await extract(SERVICES[service], {
|
||||
getExtraEntryFields: (feedEntry) => {
|
||||
return {
|
||||
tags: feedEntry['cd:tags'],
|
||||
}
|
||||
},
|
||||
})
|
||||
const entries: (FeedEntry & { tags?: string })[] = data?.entries
|
||||
if (!existingContent[service].includes(entries[0].id)) {
|
||||
let tags = ''
|
||||
if (entries[0].tags) {
|
||||
entries[0].tags
|
||||
.split(',')
|
||||
.forEach((a, index) =>
|
||||
index === 0 ? (tags += `#${toPascalCase(a)}`) : (tags += ` #${toPascalCase(a)}`)
|
||||
)
|
||||
tags += ` ${TAGS[service]}`
|
||||
} else {
|
||||
tags = TAGS[service]
|
||||
}
|
||||
existingContent[service].push(entries[0].id)
|
||||
createMastoPost(`${entries[0].title} ${entries[0].link} ${tags}`)
|
||||
await fetch(`https://api.github.com/gists/${GIST_ID_SYNDICATION_CACHE}`, {
|
||||
method: 'PATCH',
|
||||
headers: {
|
||||
Authorization: `Bearer ${TOKEN_CORYDDEV_GISTS}`,
|
||||
'Content-Type': 'application/vnd.github+json',
|
||||
method: 'PATCH',
|
||||
headers: {
|
||||
Authorization: `Bearer ${TOKEN_CORYDDEV_GISTS}`,
|
||||
'Content-Type': 'application/vnd.github+json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
gist_id: GIST_ID_SYNDICATION_CACHE,
|
||||
files: {
|
||||
'syndication-cache.json': {
|
||||
content: JSON.stringify(existingContent),
|
||||
},
|
||||
},
|
||||
body: JSON.stringify({
|
||||
gist_id: GIST_ID_SYNDICATION_CACHE,
|
||||
files: {
|
||||
'syndication-cache.json': {
|
||||
content: JSON.stringify(CACHE_DATA),
|
||||
},
|
||||
},
|
||||
}),
|
||||
}),
|
||||
})
|
||||
.then((response) => response.json())
|
||||
.catch((err) => console.log(err))
|
||||
}
|
||||
|
||||
const DATA = await fetch(`https://api.github.com/gists/${GIST_ID_SYNDICATION_CACHE}`).then(
|
||||
(response) => response.json()
|
||||
)
|
||||
const CONTENT = DATA?.files[GIST_NAME_SYNDICATION_CACHE].content
|
||||
|
||||
// rewrite the sync data if init is reset
|
||||
if (CONTENT === '' || init === 'true') hydrateCache()
|
||||
|
||||
if (CONTENT && CONTENT !== '' && !init) {
|
||||
const existingData = await fetch(
|
||||
`https://api.github.com/gists/${GIST_ID_SYNDICATION_CACHE}`
|
||||
).then((response) => response.json())
|
||||
const existingContent = JSON.parse(existingData?.files[GIST_NAME_SYNDICATION_CACHE].content)
|
||||
|
||||
for (const service in SERVICES) {
|
||||
const data = await extract(SERVICES[service], {
|
||||
getExtraEntryFields: (feedEntry) => {
|
||||
return {
|
||||
tags: feedEntry['cd:tags'],
|
||||
}
|
||||
},
|
||||
})
|
||||
const entries: (FeedEntry & { tags?: string })[] = data?.entries
|
||||
if (!existingContent[service].includes(entries[0].id)) {
|
||||
let tags = ''
|
||||
if (entries[0].tags) {
|
||||
entries[0].tags
|
||||
.split(',')
|
||||
.forEach((a, index) =>
|
||||
index === 0
|
||||
? (tags += `#${toPascalCase(a)}`)
|
||||
: (tags += ` #${toPascalCase(a)}`)
|
||||
)
|
||||
tags += ` ${TAGS[service]}`
|
||||
} else {
|
||||
tags = TAGS[service]
|
||||
}
|
||||
existingContent[service].push(entries[0].id)
|
||||
createMastoPost(`${entries[0].title} ${entries[0].link} ${tags}`)
|
||||
await fetch(`https://api.github.com/gists/${GIST_ID_SYNDICATION_CACHE}`, {
|
||||
method: 'PATCH',
|
||||
headers: {
|
||||
Authorization: `Bearer ${TOKEN_CORYDDEV_GISTS}`,
|
||||
'Content-Type': 'application/vnd.github+json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
gist_id: GIST_ID_SYNDICATION_CACHE,
|
||||
files: {
|
||||
'syndication-cache.json': {
|
||||
content: JSON.stringify(existingContent),
|
||||
},
|
||||
},
|
||||
}),
|
||||
})
|
||||
.then((response) => response.json())
|
||||
.catch((err) => console.log(err))
|
||||
}
|
||||
}
|
||||
.then((response) => response.json())
|
||||
.catch((err) => console.log(err))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
@ -119,9 +117,9 @@ Once the cache is hydrated the script will check the feeds available in `lib/syn
|
|||
|
||||
```typescript
|
||||
export const SERVICES = {
|
||||
'coryd.dev': 'https://coryd.dev/feed.xml',
|
||||
glass: 'https://glass.photo/coryd/rss',
|
||||
letterboxd: 'https://letterboxd.com/cdme/rss/',
|
||||
'coryd.dev': 'https://coryd.dev/feed.xml',
|
||||
glass: 'https://glass.photo/coryd/rss',
|
||||
letterboxd: 'https://letterboxd.com/cdme/rss/',
|
||||
}
|
||||
```
|
||||
|
||||
|
@ -129,9 +127,9 @@ As we iterate through this object we also attach tags specific to each service u
|
|||
|
||||
```typescript
|
||||
export const TAGS = {
|
||||
'coryd.dev': '#Blog',
|
||||
glass: '#Photo #Glass',
|
||||
letterboxd: '#Movie #Letterboxd',
|
||||
'coryd.dev': '#Blog',
|
||||
glass: '#Photo #Glass',
|
||||
letterboxd: '#Movie #Letterboxd',
|
||||
}
|
||||
```
|
||||
|
||||
|
@ -168,7 +166,7 @@ const generateRss = (posts: PostFrontMatter[], page = 'feed.xml') => `
|
|||
<webMaster>${siteMetadata.email} (${siteMetadata.author})</webMaster>
|
||||
<lastBuildDate>${new Date(posts[0].date).toUTCString()}</lastBuildDate>
|
||||
<atom:link href="${
|
||||
siteMetadata.siteUrl
|
||||
siteMetadata.siteUrl
|
||||
}/${page}" rel="self" type="application/rss+xml"/>
|
||||
${posts.map(generateRssItem).join('')}
|
||||
</channel>
|
||||
|
@ -214,18 +212,18 @@ import { MASTODON_INSTANCE } from './config'
|
|||
const KEY = process.env.API_KEY_MASTODON
|
||||
|
||||
const createMastoPost = async (content: string) => {
|
||||
const formData = new FormData()
|
||||
formData.append('status', content)
|
||||
const formData = new FormData()
|
||||
formData.append('status', content)
|
||||
|
||||
const res = await fetch(`${MASTODON_INSTANCE}/api/v1/statuses`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
Authorization: `Bearer ${KEY}`,
|
||||
},
|
||||
body: formData,
|
||||
})
|
||||
return res.json()
|
||||
const res = await fetch(`${MASTODON_INSTANCE}/api/v1/statuses`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
Authorization: `Bearer ${KEY}`,
|
||||
},
|
||||
body: formData,
|
||||
})
|
||||
return res.json()
|
||||
}
|
||||
|
||||
export default createMastoPost
|
||||
|
@ -236,16 +234,16 @@ Back at GitHub, this is all kicked off every hour on the hour using the followin
|
|||
```yaml
|
||||
name: scheduled-cron-job
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 * * * *'
|
||||
schedule:
|
||||
- cron: '0 * * * *'
|
||||
jobs:
|
||||
cron:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: scheduled-cron-job
|
||||
run: |
|
||||
curl -X POST 'https://coryd.dev/api/syndicate' \
|
||||
-H 'Authorization: Bearer ${{ secrets.VERCEL_SYNDICATE_KEY }}'
|
||||
cron:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: scheduled-cron-job
|
||||
run: |
|
||||
curl -X POST 'https://coryd.dev/api/syndicate' \
|
||||
-H 'Authorization: Bearer ${{ secrets.VERCEL_SYNDICATE_KEY }}'
|
||||
```
|
||||
|
||||
Now, as I post things elsewhere, they'll make their way back to Mastodon with a simple title, link and tag set. Read them if you'd like, or filter them out altogether.
|
||||
|
|
|
@ -17,10 +17,10 @@ import Link from 'next/link'
|
|||
import { PageSEO } from '@/components/SEO'
|
||||
import { Spin } from '@/components/Loading'
|
||||
import {
|
||||
MapPinIcon,
|
||||
CodeBracketIcon,
|
||||
MegaphoneIcon,
|
||||
CommandLineIcon,
|
||||
MapPinIcon,
|
||||
CodeBracketIcon,
|
||||
MegaphoneIcon,
|
||||
CommandLineIcon,
|
||||
} from '@heroicons/react/24/solid'
|
||||
import Status from '@/components/Status'
|
||||
import Albums from '@/components/media/Albums'
|
||||
|
@ -34,111 +34,107 @@ let host = siteMetadata.siteUrl
|
|||
if (env === 'development') host = 'http://localhost:3000'
|
||||
|
||||
export async function getStaticProps() {
|
||||
return {
|
||||
props: await loadNowData('status,artists,albums,books,movies,tv'),
|
||||
revalidate: 3600,
|
||||
}
|
||||
return {
|
||||
props: await loadNowData('status,artists,albums,books,movies,tv'),
|
||||
revalidate: 3600,
|
||||
}
|
||||
}
|
||||
|
||||
export default function Now(props) {
|
||||
const { response, error } = useJson(`${host}/api/now`, props)
|
||||
const { status, artists, albums, books, movies, tv } = response
|
||||
const { response, error } = useJson(`${host}/api/now`, props)
|
||||
const { status, artists, albums, books, movies, tv } = response
|
||||
|
||||
if (error) return null
|
||||
if (!response) return <Spin className="my-2 flex justify-center" />
|
||||
if (error) return null
|
||||
if (!response) return <Spin className="my-2 flex justify-center" />
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageSEO
|
||||
title={`Now - ${siteMetadata.author}`}
|
||||
description={siteMetadata.description.now}
|
||||
/>
|
||||
<div className="divide-y divide-gray-200 dark:divide-gray-700">
|
||||
<div className="space-y-2 pt-6 pb-8 md:space-y-5">
|
||||
<h1 className="text-3xl font-extrabold leading-9 tracking-tight text-gray-900 dark:text-gray-100 sm:text-4xl sm:leading-10 md:text-6xl md:leading-14">
|
||||
Now
|
||||
</h1>
|
||||
</div>
|
||||
<div className="pt-12">
|
||||
<h3 className="text-xl font-extrabold leading-9 tracking-tight text-gray-900 dark:text-gray-100 sm:text-2xl sm:leading-10 md:text-4xl md:leading-14">
|
||||
Currently
|
||||
</h3>
|
||||
<div className="pl-5 md:pl-10">
|
||||
<Status status={status} />
|
||||
<p className="mt-2 text-lg leading-7 text-gray-500 dark:text-gray-100">
|
||||
<MapPinIcon className="mr-1 inline h-6 w-6" />
|
||||
Living in Camarillo, California with my beautiful family, 4 rescue dogs and
|
||||
a guinea pig.
|
||||
</p>
|
||||
<p className="mt-2 text-lg leading-7 text-gray-500 dark:text-gray-100">
|
||||
<CodeBracketIcon className="mr-1 inline h-6 w-6" />
|
||||
Working at <Link
|
||||
className="text-primary-500 hover:text-primary-600 dark:hover:text-primary-400"
|
||||
href="https://hashicorp.com"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
HashiCorp
|
||||
</Link>
|
||||
</p>
|
||||
<p className="mt-2 text-lg leading-7 text-gray-500 dark:text-gray-100">
|
||||
<MegaphoneIcon className="mr-1 inline h-6 w-6" />
|
||||
Rooting for the{` `}
|
||||
<Link
|
||||
className="text-primary-500 hover:text-primary-600 dark:hover:text-primary-400"
|
||||
href="https://lakers.com"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Lakers
|
||||
</Link>
|
||||
, for better or worse.
|
||||
</p>
|
||||
</div>
|
||||
<h3 className="pt-6 text-xl font-extrabold leading-9 tracking-tight text-gray-900 dark:text-gray-100 sm:text-2xl sm:leading-10 md:text-4xl md:leading-14">
|
||||
Making
|
||||
</h3>
|
||||
<div className="pl-5 md:pl-10">
|
||||
<p className="mt-2 text-lg leading-7 text-gray-500 dark:text-gray-100">
|
||||
<CommandLineIcon className="mr-1 inline h-6 w-6" />
|
||||
Hacking away on random projects like this page, my <Link
|
||||
className="text-primary-500 hover:text-primary-600 dark:hover:text-primary-400"
|
||||
href="/blog"
|
||||
passHref
|
||||
>
|
||||
blog
|
||||
</Link> and whatever else I can find time for.
|
||||
</p>
|
||||
</div>
|
||||
<Artists artists={artists} />
|
||||
<Albums albums={albums} />
|
||||
<Reading books={books} />
|
||||
<Movies movies={movies} />
|
||||
<TV tv={tv} />
|
||||
<p className="pt-8 text-center text-xs text-gray-900 dark:text-gray-100">
|
||||
(This is a{' '}
|
||||
<Link
|
||||
className="text-primary-500 hover:text-primary-600 dark:hover:text-primary-400"
|
||||
href="https://nownownow.com/about"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
now page
|
||||
</Link>
|
||||
, and if you have your own site, <Link
|
||||
className="text-primary-500 hover:text-primary-600 dark:hover:text-primary-400"
|
||||
href="https://nownownow.com/about"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
you should make one, too
|
||||
</Link>
|
||||
.)
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
return (
|
||||
<>
|
||||
<PageSEO title={`Now - ${siteMetadata.author}`} description={siteMetadata.description.now} />
|
||||
<div className="divide-y divide-gray-200 dark:divide-gray-700">
|
||||
<div className="space-y-2 pt-6 pb-8 md:space-y-5">
|
||||
<h1 className="text-3xl font-extrabold leading-9 tracking-tight text-gray-900 dark:text-gray-100 sm:text-4xl sm:leading-10 md:text-6xl md:leading-14">
|
||||
Now
|
||||
</h1>
|
||||
</div>
|
||||
<div className="pt-12">
|
||||
<h3 className="text-xl font-extrabold leading-9 tracking-tight text-gray-900 dark:text-gray-100 sm:text-2xl sm:leading-10 md:text-4xl md:leading-14">
|
||||
Currently
|
||||
</h3>
|
||||
<div className="pl-5 md:pl-10">
|
||||
<Status status={status} />
|
||||
<p className="mt-2 text-lg leading-7 text-gray-500 dark:text-gray-100">
|
||||
<MapPinIcon className="mr-1 inline h-6 w-6" />
|
||||
Living in Camarillo, California with my beautiful family, 4 rescue dogs and a guinea pig.
|
||||
</p>
|
||||
<p className="mt-2 text-lg leading-7 text-gray-500 dark:text-gray-100">
|
||||
<CodeBracketIcon className="mr-1 inline h-6 w-6" />
|
||||
Working at <Link
|
||||
className="text-primary-500 hover:text-primary-600 dark:hover:text-primary-400"
|
||||
href="https://hashicorp.com"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
HashiCorp
|
||||
</Link>
|
||||
</p>
|
||||
<p className="mt-2 text-lg leading-7 text-gray-500 dark:text-gray-100">
|
||||
<MegaphoneIcon className="mr-1 inline h-6 w-6" />
|
||||
Rooting for the{` `}
|
||||
<Link
|
||||
className="text-primary-500 hover:text-primary-600 dark:hover:text-primary-400"
|
||||
href="https://lakers.com"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Lakers
|
||||
</Link>
|
||||
, for better or worse.
|
||||
</p>
|
||||
</div>
|
||||
<h3 className="pt-6 text-xl font-extrabold leading-9 tracking-tight text-gray-900 dark:text-gray-100 sm:text-2xl sm:leading-10 md:text-4xl md:leading-14">
|
||||
Making
|
||||
</h3>
|
||||
<div className="pl-5 md:pl-10">
|
||||
<p className="mt-2 text-lg leading-7 text-gray-500 dark:text-gray-100">
|
||||
<CommandLineIcon className="mr-1 inline h-6 w-6" />
|
||||
Hacking away on random projects like this page, my <Link
|
||||
className="text-primary-500 hover:text-primary-600 dark:hover:text-primary-400"
|
||||
href="/blog"
|
||||
passHref
|
||||
>
|
||||
blog
|
||||
</Link> and whatever else I can find time for.
|
||||
</p>
|
||||
</div>
|
||||
<Artists artists={artists} />
|
||||
<Albums albums={albums} />
|
||||
<Reading books={books} />
|
||||
<Movies movies={movies} />
|
||||
<TV tv={tv} />
|
||||
<p className="pt-8 text-center text-xs text-gray-900 dark:text-gray-100">
|
||||
(This is a{' '}
|
||||
<Link
|
||||
className="text-primary-500 hover:text-primary-600 dark:hover:text-primary-400"
|
||||
href="https://nownownow.com/about"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
now page
|
||||
</Link>
|
||||
, and if you have your own site, <Link
|
||||
className="text-primary-500 hover:text-primary-600 dark:hover:text-primary-400"
|
||||
href="https://nownownow.com/about"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
you should make one, too
|
||||
</Link>
|
||||
.)
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
|
@ -151,113 +147,113 @@ import { Albums, Artists, Status, TransformedRss } from '@/types/api'
|
|||
import { Tracks } from '@/types/api/tracks'
|
||||
|
||||
export default async function loadNowData(endpoints?: string) {
|
||||
const selectedEndpoints = endpoints?.split(',') || null
|
||||
const TV_KEY = process.env.API_KEY_TRAKT
|
||||
const MUSIC_KEY = process.env.API_KEY_LASTFM
|
||||
const env = process.env.NODE_ENV
|
||||
let host = siteMetadata.siteUrl
|
||||
if (env === 'development') host = 'http://localhost:3000'
|
||||
const selectedEndpoints = endpoints?.split(',') || null
|
||||
const TV_KEY = process.env.API_KEY_TRAKT
|
||||
const MUSIC_KEY = process.env.API_KEY_LASTFM
|
||||
const env = process.env.NODE_ENV
|
||||
let host = siteMetadata.siteUrl
|
||||
if (env === 'development') host = 'http://localhost:3000'
|
||||
|
||||
let statusJson = null
|
||||
let artistsJson = null
|
||||
let albumsJson = null
|
||||
let booksJson = null
|
||||
let moviesJson = null
|
||||
let tvJson = null
|
||||
let currentTrackJson = null
|
||||
let statusJson = null
|
||||
let artistsJson = null
|
||||
let albumsJson = null
|
||||
let booksJson = null
|
||||
let moviesJson = null
|
||||
let tvJson = null
|
||||
let currentTrackJson = null
|
||||
|
||||
// status
|
||||
if ((endpoints && selectedEndpoints.includes('status')) || !endpoints) {
|
||||
const statusUrl = 'https://api.omg.lol/address/cory/statuses/'
|
||||
statusJson = await fetch(statusUrl)
|
||||
.then((response) => response.json())
|
||||
.catch((error) => {
|
||||
console.log(error)
|
||||
return {}
|
||||
})
|
||||
}
|
||||
// status
|
||||
if ((endpoints && selectedEndpoints.includes('status')) || !endpoints) {
|
||||
const statusUrl = 'https://api.omg.lol/address/cory/statuses/'
|
||||
statusJson = await fetch(statusUrl)
|
||||
.then((response) => response.json())
|
||||
.catch((error) => {
|
||||
console.log(error)
|
||||
return {}
|
||||
})
|
||||
}
|
||||
|
||||
// artists
|
||||
if ((endpoints && selectedEndpoints.includes('artists')) || !endpoints) {
|
||||
const artistsUrl = `http://ws.audioscrobbler.com/2.0/?method=user.gettopartists&user=cdme_&api_key=${MUSIC_KEY}&limit=8&format=json&period=7day`
|
||||
artistsJson = await fetch(artistsUrl)
|
||||
.then((response) => response.json())
|
||||
.catch((error) => {
|
||||
console.log(error)
|
||||
return {}
|
||||
})
|
||||
}
|
||||
// artists
|
||||
if ((endpoints && selectedEndpoints.includes('artists')) || !endpoints) {
|
||||
const artistsUrl = `http://ws.audioscrobbler.com/2.0/?method=user.gettopartists&user=cdme_&api_key=${MUSIC_KEY}&limit=8&format=json&period=7day`
|
||||
artistsJson = await fetch(artistsUrl)
|
||||
.then((response) => response.json())
|
||||
.catch((error) => {
|
||||
console.log(error)
|
||||
return {}
|
||||
})
|
||||
}
|
||||
|
||||
// albums
|
||||
if ((endpoints && selectedEndpoints.includes('albums')) || !endpoints) {
|
||||
const albumsUrl = `http://ws.audioscrobbler.com/2.0/?method=user.gettopalbums&user=cdme_&api_key=${MUSIC_KEY}&limit=8&format=json&period=7day`
|
||||
albumsJson = await fetch(albumsUrl)
|
||||
.then((response) => response.json())
|
||||
.catch((error) => {
|
||||
console.log(error)
|
||||
return {}
|
||||
})
|
||||
}
|
||||
// albums
|
||||
if ((endpoints && selectedEndpoints.includes('albums')) || !endpoints) {
|
||||
const albumsUrl = `http://ws.audioscrobbler.com/2.0/?method=user.gettopalbums&user=cdme_&api_key=${MUSIC_KEY}&limit=8&format=json&period=7day`
|
||||
albumsJson = await fetch(albumsUrl)
|
||||
.then((response) => response.json())
|
||||
.catch((error) => {
|
||||
console.log(error)
|
||||
return {}
|
||||
})
|
||||
}
|
||||
|
||||
// books
|
||||
if ((endpoints && selectedEndpoints.includes('books')) || !endpoints) {
|
||||
const booksUrl = `${host}/feeds/books`
|
||||
booksJson = await extract(booksUrl).catch((error) => {
|
||||
console.log(error)
|
||||
return {}
|
||||
})
|
||||
}
|
||||
// books
|
||||
if ((endpoints && selectedEndpoints.includes('books')) || !endpoints) {
|
||||
const booksUrl = `${host}/feeds/books`
|
||||
booksJson = await extract(booksUrl).catch((error) => {
|
||||
console.log(error)
|
||||
return {}
|
||||
})
|
||||
}
|
||||
|
||||
// movies
|
||||
if ((endpoints && selectedEndpoints.includes('movies')) || !endpoints) {
|
||||
const moviesUrl = `${host}/feeds/movies`
|
||||
moviesJson = await extract(moviesUrl).catch((error) => {
|
||||
console.log(error)
|
||||
return {}
|
||||
})
|
||||
moviesJson.entries = moviesJson.entries.splice(0, 5)
|
||||
}
|
||||
// movies
|
||||
if ((endpoints && selectedEndpoints.includes('movies')) || !endpoints) {
|
||||
const moviesUrl = `${host}/feeds/movies`
|
||||
moviesJson = await extract(moviesUrl).catch((error) => {
|
||||
console.log(error)
|
||||
return {}
|
||||
})
|
||||
moviesJson.entries = moviesJson.entries.splice(0, 5)
|
||||
}
|
||||
|
||||
// tv
|
||||
if ((endpoints && selectedEndpoints.includes('tv')) || !endpoints) {
|
||||
const tvUrl = `${host}/feeds/tv?slurm=${TV_KEY}`
|
||||
tvJson = await extract(tvUrl).catch((error) => {
|
||||
console.log(error)
|
||||
return {}
|
||||
})
|
||||
tvJson.entries = tvJson.entries.splice(0, 5)
|
||||
}
|
||||
// tv
|
||||
if ((endpoints && selectedEndpoints.includes('tv')) || !endpoints) {
|
||||
const tvUrl = `${host}/feeds/tv?slurm=${TV_KEY}`
|
||||
tvJson = await extract(tvUrl).catch((error) => {
|
||||
console.log(error)
|
||||
return {}
|
||||
})
|
||||
tvJson.entries = tvJson.entries.splice(0, 5)
|
||||
}
|
||||
|
||||
// current track
|
||||
if ((endpoints && selectedEndpoints.includes('currentTrack')) || !endpoints) {
|
||||
const currentTrackUrl = `http://ws.audioscrobbler.com/2.0/?method=user.getrecenttracks&user=cdme_&api_key=${MUSIC_KEY}&limit=1&format=json&period=7day`
|
||||
currentTrackJson = await fetch(currentTrackUrl)
|
||||
.then((response) => response.json())
|
||||
.catch((error) => {
|
||||
console.log(error)
|
||||
return {}
|
||||
})
|
||||
}
|
||||
// current track
|
||||
if ((endpoints && selectedEndpoints.includes('currentTrack')) || !endpoints) {
|
||||
const currentTrackUrl = `http://ws.audioscrobbler.com/2.0/?method=user.getrecenttracks&user=cdme_&api_key=${MUSIC_KEY}&limit=1&format=json&period=7day`
|
||||
currentTrackJson = await fetch(currentTrackUrl)
|
||||
.then((response) => response.json())
|
||||
.catch((error) => {
|
||||
console.log(error)
|
||||
return {}
|
||||
})
|
||||
}
|
||||
|
||||
const res: {
|
||||
status?: Status
|
||||
artists?: Artists
|
||||
albums?: Albums
|
||||
books?: TransformedRss
|
||||
movies?: TransformedRss
|
||||
tv?: TransformedRss
|
||||
currentTrack?: Tracks
|
||||
} = {}
|
||||
if (statusJson) res.status = statusJson.response.statuses.splice(0, 1)[0]
|
||||
if (artistsJson) res.artists = artistsJson?.topartists.artist
|
||||
if (albumsJson) res.albums = albumsJson?.topalbums.album
|
||||
if (booksJson) res.books = booksJson?.entries
|
||||
if (moviesJson) res.movies = moviesJson?.entries
|
||||
if (tvJson) res.tv = tvJson?.entries
|
||||
if (currentTrackJson) res.currentTrack = currentTrackJson?.recenttracks?.track?.[0]
|
||||
const res: {
|
||||
status?: Status
|
||||
artists?: Artists
|
||||
albums?: Albums
|
||||
books?: TransformedRss
|
||||
movies?: TransformedRss
|
||||
tv?: TransformedRss
|
||||
currentTrack?: Tracks
|
||||
} = {}
|
||||
if (statusJson) res.status = statusJson.response.statuses.splice(0, 1)[0]
|
||||
if (artistsJson) res.artists = artistsJson?.topartists.artist
|
||||
if (albumsJson) res.albums = albumsJson?.topalbums.album
|
||||
if (booksJson) res.books = booksJson?.entries
|
||||
if (moviesJson) res.movies = moviesJson?.entries
|
||||
if (tvJson) res.tv = tvJson?.entries
|
||||
if (currentTrackJson) res.currentTrack = currentTrackJson?.recenttracks?.track?.[0]
|
||||
|
||||
// unified response
|
||||
return res
|
||||
// unified response
|
||||
return res
|
||||
}
|
||||
```
|
||||
|
||||
|
@ -269,22 +265,22 @@ import { Spin } from '@/components/Loading'
|
|||
import { Album } from '@/types/api'
|
||||
|
||||
const Albums = (props: { albums: Album[] }) => {
|
||||
const { albums } = props
|
||||
const { albums } = props
|
||||
|
||||
if (!albums) return <Spin className="my-12 flex justify-center" />
|
||||
if (!albums) return <Spin className="my-12 flex justify-center" />
|
||||
|
||||
return (
|
||||
<>
|
||||
<h3 className="pt-4 pb-4 text-xl font-extrabold leading-9 tracking-tight text-gray-900 dark:text-gray-100 sm:text-2xl sm:leading-10 md:text-4xl md:leading-14">
|
||||
Listening: albums
|
||||
</h3>
|
||||
<div className="grid grid-cols-2 gap-2 md:grid-cols-4">
|
||||
{albums?.map((album) => (
|
||||
<Cover key={album.mbid} media={album} type="album" />
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
return (
|
||||
<>
|
||||
<h3 className="pt-4 pb-4 text-xl font-extrabold leading-9 tracking-tight text-gray-900 dark:text-gray-100 sm:text-2xl sm:leading-10 md:text-4xl md:leading-14">
|
||||
Listening: albums
|
||||
</h3>
|
||||
<div className="grid grid-cols-2 gap-2 md:grid-cols-4">
|
||||
{albums?.map((album) => (
|
||||
<Cover key={album.mbid} media={album} type="album" />
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default Albums
|
||||
|
@ -299,44 +295,44 @@ import Link from 'next/link'
|
|||
import { ALBUM_DENYLIST } from '@/utils/constants'
|
||||
|
||||
const Cover = (props: { media: Media; type: 'artist' | 'album' }) => {
|
||||
const { media, type } = props
|
||||
const image = (media: Media) => {
|
||||
let img = ''
|
||||
if (type === 'album')
|
||||
img = !ALBUM_DENYLIST.includes(media.name.replace(/\s+/g, '-').toLowerCase())
|
||||
? media.image[media.image.length - 1]['#text']
|
||||
: `/media/artists/${media.name.replace(/\s+/g, '-').toLowerCase()}.jpg`
|
||||
if (type === 'artist')
|
||||
img = `/media/artists/${media.name.replace(/\s+/g, '-').toLowerCase()}.jpg`
|
||||
return img
|
||||
}
|
||||
const { media, type } = props
|
||||
const image = (media: Media) => {
|
||||
let img = ''
|
||||
if (type === 'album')
|
||||
img = !ALBUM_DENYLIST.includes(media.name.replace(/\s+/g, '-').toLowerCase())
|
||||
? media.image[media.image.length - 1]['#text']
|
||||
: `/media/artists/${media.name.replace(/\s+/g, '-').toLowerCase()}.jpg`
|
||||
if (type === 'artist')
|
||||
img = `/media/artists/${media.name.replace(/\s+/g, '-').toLowerCase()}.jpg`
|
||||
return img
|
||||
}
|
||||
|
||||
return (
|
||||
<Link
|
||||
className="text-primary-500 hover:text-primary-600 dark:hover:text-primary-400"
|
||||
href={media.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
title={media.name}
|
||||
>
|
||||
<div className="relative">
|
||||
<div className="absolute left-0 top-0 h-full w-full rounded-lg border border-primary-500 bg-cover-gradient dark:border-gray-500"></div>
|
||||
<div className="absolute left-1 bottom-2 drop-shadow-md">
|
||||
<div className="px-1 text-xs font-bold text-white">{media.name}</div>
|
||||
<div className="px-1 text-xs text-white">
|
||||
{type === 'album' ? media.artist.name : `${media.playcount} plays`}
|
||||
</div>
|
||||
</div>
|
||||
<ImageWithFallback
|
||||
src={image(media)}
|
||||
alt={media.name}
|
||||
className="rounded-lg"
|
||||
width="350"
|
||||
height="350"
|
||||
/>
|
||||
</div>
|
||||
</Link>
|
||||
)
|
||||
return (
|
||||
<Link
|
||||
className="text-primary-500 hover:text-primary-600 dark:hover:text-primary-400"
|
||||
href={media.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
title={media.name}
|
||||
>
|
||||
<div className="relative">
|
||||
<div className="absolute left-0 top-0 h-full w-full rounded-lg border border-primary-500 bg-cover-gradient dark:border-gray-500"></div>
|
||||
<div className="absolute left-1 bottom-2 drop-shadow-md">
|
||||
<div className="px-1 text-xs font-bold text-white">{media.name}</div>
|
||||
<div className="px-1 text-xs text-white">
|
||||
{type === 'album' ? media.artist.name : `${media.playcount} plays`}
|
||||
</div>
|
||||
</div>
|
||||
<ImageWithFallback
|
||||
src={image(media)}
|
||||
alt={media.name}
|
||||
className="rounded-lg"
|
||||
width="350"
|
||||
height="350"
|
||||
/>
|
||||
</div>
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
export default Cover
|
||||
|
@ -348,11 +344,11 @@ All of the components for this page [can be viewed on GitHub](https://github.com
|
|||
import loadNowData from '@/lib/now'
|
||||
|
||||
export default async function handler(req, res) {
|
||||
res.setHeader('Cache-Control', 's-maxage=3600, stale-while-revalidate')
|
||||
res.setHeader('Cache-Control', 's-maxage=3600, stale-while-revalidate')
|
||||
|
||||
const endpoints = req.query.endpoints
|
||||
const response = await loadNowData(endpoints)
|
||||
res.json(response)
|
||||
const endpoints = req.query.endpoints
|
||||
const response = await loadNowData(endpoints)
|
||||
res.json(response)
|
||||
}
|
||||
```
|
||||
|
||||
|
|
|
@ -13,13 +13,13 @@ My /now page is a series of discreet sections — the **Currently** block is [po
|
|||
const EleventyFetch = require('@11ty/eleventy-fetch')
|
||||
|
||||
module.exports = async function () {
|
||||
const url = 'https://api.omg.lol/address/cory/statuses/'
|
||||
const res = EleventyFetch(url, {
|
||||
duration: '1h',
|
||||
type: 'json',
|
||||
})
|
||||
const status = await res
|
||||
return status.response.statuses[0]
|
||||
const url = 'https://api.omg.lol/address/cory/statuses/'
|
||||
const res = EleventyFetch(url, {
|
||||
duration: '1h',
|
||||
type: 'json',
|
||||
})
|
||||
const status = await res
|
||||
return status.response.statuses[0]
|
||||
}
|
||||
```
|
||||
|
||||
|
@ -29,14 +29,14 @@ The **Listening: artists** and **Listening: albums** sections draw on data from
|
|||
const EleventyFetch = require('@11ty/eleventy-fetch')
|
||||
|
||||
module.exports = async function () {
|
||||
const MUSIC_KEY = process.env.API_KEY_LASTFM
|
||||
const url = `http://ws.audioscrobbler.com/2.0/?method=user.gettopartists&user=cdme_&api_key=${MUSIC_KEY}&limit=8&format=json&period=7day`
|
||||
const res = EleventyFetch(url, {
|
||||
duration: '1h',
|
||||
type: 'json',
|
||||
})
|
||||
const artists = await res
|
||||
return artists.topartists.artist
|
||||
const MUSIC_KEY = process.env.API_KEY_LASTFM
|
||||
const url = `http://ws.audioscrobbler.com/2.0/?method=user.gettopartists&user=cdme_&api_key=${MUSIC_KEY}&limit=8&format=json&period=7day`
|
||||
const res = EleventyFetch(url, {
|
||||
duration: '1h',
|
||||
type: 'json',
|
||||
})
|
||||
const artists = await res
|
||||
return artists.topartists.artist
|
||||
}
|
||||
```
|
||||
|
||||
|
@ -149,13 +149,13 @@ const { extract } = require('@extractus/feed-extractor')
|
|||
const { AssetCache } = require('@11ty/eleventy-fetch')
|
||||
|
||||
module.exports = async function () {
|
||||
const url = 'https://oku.club/rss/collection/POaRa'
|
||||
const asset = new AssetCache('books_data')
|
||||
if (asset.isCacheValid('1h')) return await asset.getCachedValue()
|
||||
const res = await extract(url).catch((error) => {})
|
||||
const data = res.entries
|
||||
await asset.save(data, 'json')
|
||||
return data
|
||||
const url = 'https://oku.club/rss/collection/POaRa'
|
||||
const asset = new AssetCache('books_data')
|
||||
if (asset.isCacheValid('1h')) return await asset.getCachedValue()
|
||||
const res = await extract(url).catch((error) => {})
|
||||
const data = res.entries
|
||||
await asset.save(data, 'json')
|
||||
return data
|
||||
}
|
||||
```
|
||||
|
||||
|
|
|
@ -15,9 +15,9 @@ Once you've added the appropriate tags from webmention.io, connected your desire
|
|||
import loadWebmentions from '@/lib/webmentions'
|
||||
|
||||
export default async function handler(req, res) {
|
||||
const target = req.query.target
|
||||
const response = await loadWebmentions(target)
|
||||
res.json(response)
|
||||
const target = req.query.target
|
||||
const response = await loadWebmentions(target)
|
||||
res.json(response)
|
||||
}
|
||||
```
|
||||
|
||||
|
@ -36,125 +36,119 @@ import Image from 'next/image'
|
|||
import { formatDate } from '@/utils/formatters'
|
||||
|
||||
const WebmentionsCore = () => {
|
||||
const { asPath } = useRouter()
|
||||
const { response, error } = useJson(`/api/webmentions?target=${siteMetadata.siteUrl}${asPath}`)
|
||||
const webmentions = response?.children
|
||||
const hasLikes =
|
||||
webmentions?.filter((mention) => mention['wm-property'] === 'like-of').length > 0
|
||||
const hasComments =
|
||||
webmentions?.filter((mention) => mention['wm-property'] === 'in-reply-to').length > 0
|
||||
const boostsCount = webmentions?.filter(
|
||||
(mention) =>
|
||||
mention['wm-property'] === 'repost-of' || mention['wm-property'] === 'mention-of'
|
||||
).length
|
||||
const hasBoosts = boostsCount > 0
|
||||
const hasMention = hasLikes || hasComments || hasBoosts
|
||||
const { asPath } = useRouter()
|
||||
const { response, error } = useJson(`/api/webmentions?target=${siteMetadata.siteUrl}${asPath}`)
|
||||
const webmentions = response?.children
|
||||
const hasLikes = webmentions?.filter((mention) => mention['wm-property'] === 'like-of').length > 0
|
||||
const hasComments =
|
||||
webmentions?.filter((mention) => mention['wm-property'] === 'in-reply-to').length > 0
|
||||
const boostsCount = webmentions?.filter(
|
||||
(mention) => mention['wm-property'] === 'repost-of' || mention['wm-property'] === 'mention-of'
|
||||
).length
|
||||
const hasBoosts = boostsCount > 0
|
||||
const hasMention = hasLikes || hasComments || hasBoosts
|
||||
|
||||
if (error) return null
|
||||
if (!response) return <Spin className="my-2 flex justify-center" />
|
||||
|
||||
const Boosts = () => {
|
||||
return (
|
||||
<div className="flex flex-row items-center">
|
||||
<div className="mr-2 h-5 w-5">
|
||||
<Rocket />
|
||||
</div>
|
||||
{` `}
|
||||
<span className="text-sm">{boostsCount}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const Likes = () => (
|
||||
<>
|
||||
<div className="flex flex-row items-center">
|
||||
<div className="mr-2 h-5 w-5">
|
||||
<Heart />
|
||||
</div>
|
||||
<ul className="ml-2 flex flex-row">
|
||||
{webmentions?.map((mention) => {
|
||||
if (mention['wm-property'] === 'like-of')
|
||||
return (
|
||||
<li key={mention['wm-id']} className="-ml-2">
|
||||
<Link
|
||||
href={mention.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Image
|
||||
className="h-10 w-10 rounded-full border border-primary-500 dark:border-gray-500"
|
||||
src={mention.author.photo}
|
||||
alt={mention.author.name}
|
||||
width="40"
|
||||
height="40"
|
||||
/>
|
||||
</Link>
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
|
||||
const Comments = () => {
|
||||
return (
|
||||
<>
|
||||
{webmentions?.map((mention) => {
|
||||
if (mention['wm-property'] === 'in-reply-to') {
|
||||
return (
|
||||
<Link
|
||||
className="border-bottom flex flex-row items-center border-gray-100 pb-4"
|
||||
key={mention['wm-id']}
|
||||
href={mention.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Image
|
||||
className="h-12 w-12 rounded-full border border-primary-500 dark:border-gray-500"
|
||||
src={mention.author.photo}
|
||||
alt={mention.author.name}
|
||||
width="48"
|
||||
height="48"
|
||||
/>
|
||||
<div className="ml-3">
|
||||
<p className="text-sm">{mention.content?.text}</p>
|
||||
<p className="mt-1 text-xs">{formatDate(mention.published)}</p>
|
||||
</div>
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
})}
|
||||
</>
|
||||
)
|
||||
}
|
||||
if (error) return null
|
||||
if (!response) return <Spin className="my-2 flex justify-center" />
|
||||
|
||||
const Boosts = () => {
|
||||
return (
|
||||
<>
|
||||
{hasMention ? (
|
||||
<div className="text-gray-500 dark:text-gray-100">
|
||||
<h4 className="pt-3 text-xl font-extrabold leading-9 tracking-tight text-gray-900 dark:text-gray-100 md:text-2xl md:leading-10 ">
|
||||
Webmentions
|
||||
</h4>
|
||||
{hasBoosts ? (
|
||||
<div className="pt-2 pb-4">
|
||||
<Boosts />
|
||||
</div>
|
||||
) : null}
|
||||
{hasLikes ? (
|
||||
<div className="pt-2 pb-4">
|
||||
<Likes />
|
||||
</div>
|
||||
) : null}
|
||||
{hasComments ? (
|
||||
<div className="pt-2 pb-4">
|
||||
<Comments />
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
</>
|
||||
<div className="flex flex-row items-center">
|
||||
<div className="mr-2 h-5 w-5">
|
||||
<Rocket />
|
||||
</div>
|
||||
{` `}
|
||||
<span className="text-sm">{boostsCount}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const Likes = () => (
|
||||
<>
|
||||
<div className="flex flex-row items-center">
|
||||
<div className="mr-2 h-5 w-5">
|
||||
<Heart />
|
||||
</div>
|
||||
<ul className="ml-2 flex flex-row">
|
||||
{webmentions?.map((mention) => {
|
||||
if (mention['wm-property'] === 'like-of')
|
||||
return (
|
||||
<li key={mention['wm-id']} className="-ml-2">
|
||||
<Link href={mention.url} target="_blank" rel="noopener noreferrer">
|
||||
<Image
|
||||
className="h-10 w-10 rounded-full border border-primary-500 dark:border-gray-500"
|
||||
src={mention.author.photo}
|
||||
alt={mention.author.name}
|
||||
width="40"
|
||||
height="40"
|
||||
/>
|
||||
</Link>
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
|
||||
const Comments = () => {
|
||||
return (
|
||||
<>
|
||||
{webmentions?.map((mention) => {
|
||||
if (mention['wm-property'] === 'in-reply-to') {
|
||||
return (
|
||||
<Link
|
||||
className="border-bottom flex flex-row items-center border-gray-100 pb-4"
|
||||
key={mention['wm-id']}
|
||||
href={mention.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Image
|
||||
className="h-12 w-12 rounded-full border border-primary-500 dark:border-gray-500"
|
||||
src={mention.author.photo}
|
||||
alt={mention.author.name}
|
||||
width="48"
|
||||
height="48"
|
||||
/>
|
||||
<div className="ml-3">
|
||||
<p className="text-sm">{mention.content?.text}</p>
|
||||
<p className="mt-1 text-xs">{formatDate(mention.published)}</p>
|
||||
</div>
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
})}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{hasMention ? (
|
||||
<div className="text-gray-500 dark:text-gray-100">
|
||||
<h4 className="pt-3 text-xl font-extrabold leading-9 tracking-tight text-gray-900 dark:text-gray-100 md:text-2xl md:leading-10 ">
|
||||
Webmentions
|
||||
</h4>
|
||||
{hasBoosts ? (
|
||||
<div className="pt-2 pb-4">
|
||||
<Boosts />
|
||||
</div>
|
||||
) : null}
|
||||
{hasLikes ? (
|
||||
<div className="pt-2 pb-4">
|
||||
<Likes />
|
||||
</div>
|
||||
) : null}
|
||||
{hasComments ? (
|
||||
<div className="pt-2 pb-4">
|
||||
<Comments />
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default WebmentionsCore
|
||||
|
@ -167,22 +161,22 @@ import { useEffect, useState } from 'react'
|
|||
import useSWR from 'swr'
|
||||
|
||||
export const useJson = (url: string, props?: any) => {
|
||||
const [response, setResponse] = useState<any>({})
|
||||
const [response, setResponse] = useState<any>({})
|
||||
|
||||
const fetcher = (url: string) =>
|
||||
fetch(url)
|
||||
.then((res) => res.json())
|
||||
.catch()
|
||||
const { data, error } = useSWR(url, fetcher, { fallbackData: props, refreshInterval: 30000 })
|
||||
const fetcher = (url: string) =>
|
||||
fetch(url)
|
||||
.then((res) => res.json())
|
||||
.catch()
|
||||
const { data, error } = useSWR(url, fetcher, { fallbackData: props, refreshInterval: 30000 })
|
||||
|
||||
useEffect(() => {
|
||||
setResponse(data)
|
||||
}, [data, setResponse])
|
||||
useEffect(() => {
|
||||
setResponse(data)
|
||||
}, [data, setResponse])
|
||||
|
||||
return {
|
||||
response,
|
||||
error,
|
||||
}
|
||||
return {
|
||||
response,
|
||||
error,
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
@ -195,8 +189,8 @@ import dynamic from 'next/dynamic'
|
|||
import { Spin } from '@/components/Loading'
|
||||
|
||||
const Webmentions = dynamic(() => import('@/components/webmentions/WebmentionsCore'), {
|
||||
ssr: false,
|
||||
loading: () => <Spin className="my-2 flex justify-center" />,
|
||||
ssr: false,
|
||||
loading: () => <Spin className="my-2 flex justify-center" />,
|
||||
})
|
||||
|
||||
export default Webmentions
|
||||
|
|
|
@ -5,9 +5,10 @@ draft: false
|
|||
tags: ['.env', '11ty', 'eleventy']
|
||||
---
|
||||
|
||||
**dotenv-flow:**
|
||||
**dotenv-flow:**
|
||||
|
||||
> **dotenv-flow** extends **dotenv** adding the ability to have multiple `.env*` files like `.env.development`, `.env.test` and `.env.production`, also allowing defined variables to be overwritten individually in the appropriate `.env*.local` file.
|
||||
|
||||
The Eleventy docs recommend the `dotenv` package for working with `.env` files[^1], but I've found `dotenv-flow` to be a bit more useful inasmuch as support for `.env*` file patterns make development more convenient.<!-- excerpt -->
|
||||
|
||||
[^1]: Which is awesome — it works perfectly.
|
||||
[^1]: Which is awesome — it works perfectly.
|
||||
|
|
|
@ -14,24 +14,24 @@ To update my feeds ([feed.xml](https://coryd.dev/feed.xml) and [follow.xml](http
|
|||
```yaml
|
||||
name: Scheduled Vercel build
|
||||
env:
|
||||
VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
|
||||
VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}
|
||||
VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
|
||||
VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 * * * *'
|
||||
schedule:
|
||||
- cron: '0 * * * *'
|
||||
jobs:
|
||||
cron:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Install Vercel CLI
|
||||
run: npm install --global vercel@latest
|
||||
- name: Pull Vercel Environment Information
|
||||
run: vercel pull --yes --environment=production --token=${{ secrets.VERCEL_TOKEN }}
|
||||
- name: Build Project Artifacts
|
||||
run: vercel build --prod --token=${{ secrets.VERCEL_TOKEN }}
|
||||
- name: Deploy Project Artifacts to Vercel
|
||||
run: vercel deploy --prebuilt --prod --token=${{ secrets.VERCEL_TOKEN }}
|
||||
cron:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Install Vercel CLI
|
||||
run: npm install --global vercel@latest
|
||||
- name: Pull Vercel Environment Information
|
||||
run: vercel pull --yes --environment=production --token=${{ secrets.VERCEL_TOKEN }}
|
||||
- name: Build Project Artifacts
|
||||
run: vercel build --prod --token=${{ secrets.VERCEL_TOKEN }}
|
||||
- name: Deploy Project Artifacts to Vercel
|
||||
run: vercel deploy --prebuilt --prod --token=${{ secrets.VERCEL_TOKEN }}
|
||||
```
|
||||
|
||||
{% endraw %}
|
||||
|
@ -47,22 +47,22 @@ If you need to manually trigger a build, you can do so using a workflow with a {
|
|||
```yaml
|
||||
name: Manual Vercel build
|
||||
env:
|
||||
VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
|
||||
VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}
|
||||
VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
|
||||
VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}
|
||||
on: [workflow_dispatch]
|
||||
jobs:
|
||||
cron:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Install Vercel CLI
|
||||
run: npm install --global vercel@latest
|
||||
- name: Pull Vercel Environment Information
|
||||
run: vercel pull --yes --environment=production --token=${{ secrets.VERCEL_TOKEN }}
|
||||
- name: Build Project Artifacts
|
||||
run: vercel build --prod --token=${{ secrets.VERCEL_TOKEN }}
|
||||
- name: Deploy Project Artifacts to Vercel
|
||||
run: vercel deploy --prebuilt --prod --token=${{ secrets.VERCEL_TOKEN }}
|
||||
cron:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Install Vercel CLI
|
||||
run: npm install --global vercel@latest
|
||||
- name: Pull Vercel Environment Information
|
||||
run: vercel pull --yes --environment=production --token=${{ secrets.VERCEL_TOKEN }}
|
||||
- name: Build Project Artifacts
|
||||
run: vercel build --prod --token=${{ secrets.VERCEL_TOKEN }}
|
||||
- name: Deploy Project Artifacts to Vercel
|
||||
run: vercel deploy --prebuilt --prod --token=${{ secrets.VERCEL_TOKEN }}
|
||||
```
|
||||
|
||||
{% endraw %}
|
||||
|
|
|
@ -17,16 +17,16 @@ I'm fetching data from [webmention.io](https://webmention.io) at build time in `
|
|||
const EleventyFetch = require('@11ty/eleventy-fetch')
|
||||
|
||||
module.exports = async function () {
|
||||
const KEY_CORYD = process.env.API_KEY_WEBMENTIONS_CORYD_DEV
|
||||
const url = `https://webmention.io/api/mentions.jf2?token=${KEY_CORYD}&per-page=1000`
|
||||
const res = EleventyFetch(url, {
|
||||
duration: '1h',
|
||||
type: 'json',
|
||||
})
|
||||
const webmentions = await res
|
||||
return {
|
||||
mentions: webmentions.children,
|
||||
}
|
||||
const KEY_CORYD = process.env.API_KEY_WEBMENTIONS_CORYD_DEV
|
||||
const url = `https://webmention.io/api/mentions.jf2?token=${KEY_CORYD}&per-page=1000`
|
||||
const res = EleventyFetch(url, {
|
||||
duration: '1h',
|
||||
type: 'json',
|
||||
})
|
||||
const webmentions = await res
|
||||
return {
|
||||
mentions: webmentions.children,
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
@ -35,29 +35,30 @@ I have cache duration set to `1h` and a scheduled build operating on approximate
|
|||
```yaml
|
||||
name: Scheduled Vercel build
|
||||
env:
|
||||
VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
|
||||
VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}
|
||||
VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
|
||||
VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 * * * *'
|
||||
schedule:
|
||||
- cron: '0 * * * *'
|
||||
jobs:
|
||||
cron:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Install Vercel CLI
|
||||
run: npm install --global vercel@latest
|
||||
- name: Pull Vercel Environment Information
|
||||
run: vercel pull --yes --environment=production --token=${{ secrets.VERCEL_TOKEN }}
|
||||
- name: Build Project Artifacts
|
||||
run: vercel build --prod --token=${{ secrets.VERCEL_TOKEN }}
|
||||
- name: Deploy Project Artifacts to Vercel
|
||||
run: vercel deploy --prebuilt --prod --token=${{ secrets.VERCEL_TOKEN }}
|
||||
cron:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Install Vercel CLI
|
||||
run: npm install --global vercel@latest
|
||||
- name: Pull Vercel Environment Information
|
||||
run: vercel pull --yes --environment=production --token=${{ secrets.VERCEL_TOKEN }}
|
||||
- name: Build Project Artifacts
|
||||
run: vercel build --prod --token=${{ secrets.VERCEL_TOKEN }}
|
||||
- name: Deploy Project Artifacts to Vercel
|
||||
run: vercel deploy --prebuilt --prod --token=${{ secrets.VERCEL_TOKEN }}
|
||||
```
|
||||
|
||||
When the build runs, it renders any mentions of a given post via a [liquid.js](https://liquidjs.com/) template that looks like this:
|
||||
|
||||
{% raw %}
|
||||
|
||||
```liquid
|
||||
{% if webmentions %}
|
||||
<div class="border-t border-gray-200 mt-12 pt-14 dark:border-gray-700">
|
||||
|
@ -130,6 +131,7 @@ When the build runs, it renders any mentions of a given post via a [liquid.js](h
|
|||
</div>
|
||||
{% endif %}
|
||||
```
|
||||
|
||||
{% endraw %}
|
||||
|
||||
This conditionally displays different mention types based on the available data after being passed through the `webmentionsByUrl` filter which I shamelessly lifted from [Robb](https://github.com/rknightuk/rknight.me/blob/8e2a5c5f886cae6c04add7893b8bf8a2d6295ddf/config/filters.js#L48-L84).
|
||||
|
|
Reference in a new issue