mirror of
https://git.sr.ht/~cadence/bibliogram
synced 2024-11-22 16:17:29 +00:00
Add SPA post overlay
This commit is contained in:
parent
13e40259b7
commit
aac358cd65
@ -114,6 +114,34 @@ module.exports = [
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
route: `/fragment/post/(${constants.external.shortcode_regex})`, methods: ["GET"], code: ({fill}) => {
|
||||||
|
return getOrFetchShortcode(fill[0]).then(async post => {
|
||||||
|
await post.fetchChildren()
|
||||||
|
await post.fetchExtendedOwnerP() // serial await is okay since intermediate fetch result is cached
|
||||||
|
if (post.isVideo()) await post.fetchVideoURL()
|
||||||
|
return {
|
||||||
|
statusCode: 200,
|
||||||
|
contentType: "application/json",
|
||||||
|
content: {
|
||||||
|
title: post.getCaptionIntroduction(),
|
||||||
|
html: pugCache.get("pug/fragments/post.pug").web({post})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}).catch(error => {
|
||||||
|
if (error === constants.symbols.NOT_FOUND) {
|
||||||
|
return render(404, "pug/friendlyerror.pug", {
|
||||||
|
statusCode: 404,
|
||||||
|
title: "Not found",
|
||||||
|
message: "Somehow, you reached a post that doesn't exist.",
|
||||||
|
withInstancesLink: false
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
route: "/p", methods: ["GET"], code: async ({url}) => {
|
route: "/p", methods: ["GET"], code: async ({url}) => {
|
||||||
if (url.searchParams.has("p")) {
|
if (url.searchParams.has("p")) {
|
||||||
@ -135,7 +163,7 @@ module.exports = [
|
|||||||
route: `/p/(${constants.external.shortcode_regex})`, methods: ["GET"], code: ({fill}) => {
|
route: `/p/(${constants.external.shortcode_regex})`, methods: ["GET"], code: ({fill}) => {
|
||||||
return getOrFetchShortcode(fill[0]).then(async post => {
|
return getOrFetchShortcode(fill[0]).then(async post => {
|
||||||
await post.fetchChildren()
|
await post.fetchChildren()
|
||||||
await post.fetchExtendedOwnerP() // parallel await is okay since intermediate fetch result is cached
|
await post.fetchExtendedOwnerP() // serial await is okay since intermediate fetch result is cached
|
||||||
if (post.isVideo()) await post.fetchVideoURL()
|
if (post.isVideo()) await post.fetchVideoURL()
|
||||||
return render(200, "pug/post.pug", {post})
|
return render(200, "pug/post.pug", {post})
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
|
@ -15,9 +15,29 @@ class FreezeWidth extends ElemJS {
|
|||||||
|
|
||||||
const intersectionThreshold = 0
|
const intersectionThreshold = 0
|
||||||
|
|
||||||
|
class NextPageController {
|
||||||
|
constructor() {
|
||||||
|
this.instance = null
|
||||||
|
}
|
||||||
|
|
||||||
|
add() {
|
||||||
|
const nextPage = q("#next-page")
|
||||||
|
if (nextPage) {
|
||||||
|
this.instance = new NextPage(nextPage, this)
|
||||||
|
} else {
|
||||||
|
this.instance = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
activate() {
|
||||||
|
if (this.instance) this.instance.activate()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class NextPage extends FreezeWidth {
|
class NextPage extends FreezeWidth {
|
||||||
constructor(container) {
|
constructor(container, controller) {
|
||||||
super(container)
|
super(container)
|
||||||
|
this.controller = controller
|
||||||
this.clicked = false
|
this.clicked = false
|
||||||
this.nextPageNumber = +this.element.getAttribute("data-page")
|
this.nextPageNumber = +this.element.getAttribute("data-page")
|
||||||
this.attribute("href", "javascript:void(0)")
|
this.attribute("href", "javascript:void(0)")
|
||||||
@ -54,14 +74,17 @@ class NextPage extends FreezeWidth {
|
|||||||
q("#next-page-container").remove()
|
q("#next-page-container").remove()
|
||||||
this.observer.disconnect()
|
this.observer.disconnect()
|
||||||
q("#timeline").insertAdjacentHTML("beforeend", text)
|
q("#timeline").insertAdjacentHTML("beforeend", text)
|
||||||
addNextPageControl()
|
this.controller.add()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
activate() {
|
||||||
|
if (this.fetching) return
|
||||||
|
this.class("disabled")
|
||||||
|
this.fetch()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function addNextPageControl() {
|
const controller = new NextPageController()
|
||||||
const nextPage = q("#next-page")
|
controller.add()
|
||||||
if (nextPage) new NextPage(nextPage)
|
export {controller}
|
||||||
}
|
|
||||||
|
|
||||||
addNextPageControl()
|
|
||||||
|
125
src/site/html/static/js/post_overlay.js
Normal file
125
src/site/html/static/js/post_overlay.js
Normal file
@ -0,0 +1,125 @@
|
|||||||
|
import {q, ElemJS} from "./elemjs/elemjs.js"
|
||||||
|
|
||||||
|
/** @type {PostOverlay[]} */
|
||||||
|
const postOverlays = []
|
||||||
|
const titleHistory = []
|
||||||
|
titleHistory.push(document.title)
|
||||||
|
const shortcodeDataMap = new Map()
|
||||||
|
|
||||||
|
window.addEventListener("popstate", event => {
|
||||||
|
// console.log(event.state, postOverlays.length)
|
||||||
|
if (event.state) {
|
||||||
|
if (event.state.view === "post_overlay") {
|
||||||
|
loadPostOverlay(event.state.shortcode, false)
|
||||||
|
}
|
||||||
|
} else { // event.state === null which means back to originally loaded page, so pop overlay
|
||||||
|
setTimeout(() => { // make sure document is entirely loaded
|
||||||
|
if (titleHistory.length === 1) {
|
||||||
|
document.title = titleHistory[0]
|
||||||
|
} else if (titleHistory.length >= 2) {
|
||||||
|
titleHistory.pop()
|
||||||
|
document.title = titleHistory.slice(-1)[0]
|
||||||
|
}
|
||||||
|
if (postOverlays.length) {
|
||||||
|
popOverlay()
|
||||||
|
} else {
|
||||||
|
window.location.reload()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
function pushOverlay(overlay) {
|
||||||
|
postOverlays.push(overlay)
|
||||||
|
document.body.style.overflowY = "hidden"
|
||||||
|
}
|
||||||
|
|
||||||
|
function popOverlay() {
|
||||||
|
const top = postOverlays.pop()
|
||||||
|
if (top) {
|
||||||
|
top.pop()
|
||||||
|
}
|
||||||
|
if (postOverlays.length === 0) document.body.style.overflowY = "auto"
|
||||||
|
}
|
||||||
|
|
||||||
|
class PostOverlay extends ElemJS {
|
||||||
|
constructor() {
|
||||||
|
super("div")
|
||||||
|
this.class("post-overlay")
|
||||||
|
this.event("click", event => {
|
||||||
|
if (event.target === event.currentTarget) history.back()
|
||||||
|
})
|
||||||
|
this.loaded = false
|
||||||
|
this.available = true
|
||||||
|
setTimeout(() => {
|
||||||
|
if (!this.loaded) {
|
||||||
|
this.class("loading")
|
||||||
|
this.child(
|
||||||
|
new ElemJS("div").class("loading-inner").text("Loading...")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
setContent(html) {
|
||||||
|
this.html(html)
|
||||||
|
this.loaded = true
|
||||||
|
this.removeClass("loading")
|
||||||
|
}
|
||||||
|
|
||||||
|
showError() {
|
||||||
|
this.loaded = true
|
||||||
|
this.class("loading")
|
||||||
|
this.clearChildren()
|
||||||
|
this.child(
|
||||||
|
new ElemJS("div").class("loading-inner").text("Request failed.")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pop() {
|
||||||
|
this.element.remove()
|
||||||
|
this.available = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const timeline = q("#timeline")
|
||||||
|
if (timeline) {
|
||||||
|
timeline.addEventListener("click", event => {
|
||||||
|
/** @type {HTMLElement[]} */
|
||||||
|
//@ts-ignore
|
||||||
|
const path = event.composedPath()
|
||||||
|
const postLink = path.find(element => element.classList && element.classList.contains("sized-link") && element.hasAttribute("data-shortcode"))
|
||||||
|
if (postLink) {
|
||||||
|
event.preventDefault()
|
||||||
|
const shortcode = postLink.getAttribute("data-shortcode")
|
||||||
|
loadPostOverlay(shortcode, true)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function fetchShortcodeFragment(shortcode) {
|
||||||
|
if (shortcodeDataMap.has(shortcode)) return Promise.resolve(shortcodeDataMap.get(shortcode))
|
||||||
|
else return fetch(`/fragment/post/${shortcode}`).then(res => res.json())
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadPostOverlay(shortcode, shouldPushState) {
|
||||||
|
const overlay = new PostOverlay()
|
||||||
|
document.body.appendChild(overlay.element)
|
||||||
|
pushOverlay(overlay)
|
||||||
|
if (shouldPushState) history.pushState({view: "post_overlay", shortcode: shortcode}, "", `/p/${shortcode}`)
|
||||||
|
const fetcher = fetchShortcodeFragment(shortcode)
|
||||||
|
fetcher.then(root => {
|
||||||
|
shortcodeDataMap.set(shortcode, root)
|
||||||
|
if (overlay.available) {
|
||||||
|
const {title, html} = root
|
||||||
|
overlay.setContent(html)
|
||||||
|
if (overlay.available) {
|
||||||
|
document.title = title
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
fetcher.catch(error => {
|
||||||
|
console.error(error)
|
||||||
|
overlay.showError()
|
||||||
|
})
|
||||||
|
}
|
5
src/site/pug/fragments/post.pug
Normal file
5
src/site/pug/fragments/post.pug
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
//- Needs post
|
||||||
|
|
||||||
|
include ../includes/post.pug
|
||||||
|
|
||||||
|
+post(post)
|
17
src/site/pug/includes/post.pug
Normal file
17
src/site/pug/includes/post.pug
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
include ./display_structured
|
||||||
|
|
||||||
|
mixin post(post)
|
||||||
|
.post-page-divider
|
||||||
|
section.description-section
|
||||||
|
header.user-header
|
||||||
|
img(src=post.ownerPfpCacheP width=150 height=150 alt="").pfp
|
||||||
|
a.name(href=`/u/${post.getBasicOwner().username}`)= `${post.data.owner.full_name} (@${post.getBasicOwner().username})`
|
||||||
|
if post.getCaption()
|
||||||
|
p.structured-text.description
|
||||||
|
+display_structured(post.getStructuredCaption())
|
||||||
|
section.images-gallery
|
||||||
|
for entry in post.children
|
||||||
|
if entry.isVideo()
|
||||||
|
video(src=entry.getVideoUrlP() controls preload="auto" width=entry.data.dimensions.width height=entry.data.dimensions.height).sized-video
|
||||||
|
else
|
||||||
|
img(src=entry.getDisplayUrlP() alt=entry.getAlt() width=entry.data.dimensions.width height=entry.data.dimensions.height).sized-image
|
@ -11,5 +11,5 @@ mixin timeline_page(page, pageIndex)
|
|||||||
- const suggestedSize = 260 //- from css :(
|
- const suggestedSize = 260 //- from css :(
|
||||||
each image in page
|
each image in page
|
||||||
- const thumbnail = image.getSuggestedThumbnailP(suggestedSize) //- use this as the src in case there are problems with srcset
|
- const thumbnail = image.getSuggestedThumbnailP(suggestedSize) //- use this as the src in case there are problems with srcset
|
||||||
a(href=`/p/${image.data.shortcode}`).sized-link
|
a(href=`/p/${image.data.shortcode}` data-shortcode=image.data.shortcode).sized-link
|
||||||
img(src=thumbnail.src alt=image.getAlt() width=thumbnail.config_width height=thumbnail.config_height srcset=image.getThumbnailSrcsetP() sizes=image.getThumbnailSizes()).sized-image
|
img(src=thumbnail.src alt=image.getAlt() width=thumbnail.config_width height=thumbnail.config_height srcset=image.getThumbnailSrcsetP() sizes=image.getThumbnailSizes()).sized-image
|
||||||
|
@ -1,6 +1,4 @@
|
|||||||
include includes/display_structured
|
include includes/post
|
||||||
|
|
||||||
- const numberFormat = new Intl.NumberFormat().format
|
|
||||||
|
|
||||||
doctype html
|
doctype html
|
||||||
html
|
html
|
||||||
@ -12,18 +10,7 @@ html
|
|||||||
=`Post from @${post.getBasicOwner().username}`
|
=`Post from @${post.getBasicOwner().username}`
|
||||||
=` | Bibliogram`
|
=` | Bibliogram`
|
||||||
include includes/head
|
include includes/head
|
||||||
|
script(type="module" src="/static/js/post_overlay.js")
|
||||||
body.post-page
|
body.post-page
|
||||||
main.post-page-divider
|
main
|
||||||
section.description-section
|
+post(post)
|
||||||
header.user-header
|
|
||||||
img(src=post.ownerPfpCacheP width=150 height=150 alt="").pfp
|
|
||||||
a.name(href=`/u/${post.getBasicOwner().username}`)= `${post.data.owner.full_name} (@${post.getBasicOwner().username})`
|
|
||||||
if post.getCaption()
|
|
||||||
p.structured-text.description
|
|
||||||
+display_structured(post.getStructuredCaption())
|
|
||||||
section.images-gallery
|
|
||||||
for entry in post.children
|
|
||||||
if entry.isVideo()
|
|
||||||
video(src=entry.getVideoUrlP() controls preload="auto" width=entry.data.dimensions.width height=entry.data.dimensions.height).sized-video
|
|
||||||
else
|
|
||||||
img(src=entry.getDisplayUrlP() alt=entry.getAlt() width=entry.data.dimensions.width height=entry.data.dimensions.height).sized-image
|
|
||||||
|
@ -16,6 +16,7 @@ html
|
|||||||
title= `@${user.data.username} | Bibliogram`
|
title= `@${user.data.username} | Bibliogram`
|
||||||
include includes/head
|
include includes/head
|
||||||
script(src="/static/js/pagination.js" type="module")
|
script(src="/static/js/pagination.js" type="module")
|
||||||
|
script(src="/static/js/post_overlay.js" type="module")
|
||||||
body
|
body
|
||||||
.main-divider
|
.main-divider
|
||||||
header.profile-overview
|
header.profile-overview
|
||||||
@ -46,7 +47,7 @@ html
|
|||||||
if constants.settings.rss_enabled
|
if constants.settings.rss_enabled
|
||||||
+feed_link("RSS", "rss", user.data.username, "application/rss+xml", display_feed_validation_buttons)
|
+feed_link("RSS", "rss", user.data.username, "application/rss+xml", display_feed_validation_buttons)
|
||||||
+feed_link("Atom", "atom", user.data.username, "application/atom+xml", display_feed_validation_buttons)
|
+feed_link("Atom", "atom", user.data.username, "application/atom+xml", display_feed_validation_buttons)
|
||||||
a(rel="noreferrer noopener" href=`https://www.instagram.com/${user.data.username}`) instagram.com
|
a(rel="noreferrer noopener" href=`https://www.instagram.com/${user.data.username}` target="_blank") instagram.com
|
||||||
|
|
||||||
- const hasPosts = !user.data.is_private && user.timeline.pages.length && user.timeline.pages[0].length
|
- const hasPosts = !user.data.is_private && user.timeline.pages.length && user.timeline.pages[0].length
|
||||||
main(class=hasPosts ? "" : "no-posts")#timeline.timeline
|
main(class=hasPosts ? "" : "no-posts")#timeline.timeline
|
||||||
|
@ -215,95 +215,95 @@ body
|
|||||||
@include sized
|
@include sized
|
||||||
|
|
||||||
.post-page
|
.post-page
|
||||||
background-color: #6a6b71
|
background-color: #505156
|
||||||
|
|
||||||
.post-page-divider
|
.post-page-divider
|
||||||
|
display: grid
|
||||||
|
grid-template-columns: 360px auto
|
||||||
|
max-width: 1200px
|
||||||
|
margin: 0 auto
|
||||||
|
min-height: 100vh
|
||||||
|
|
||||||
|
@media screen and (max-width: $layout-a-max)
|
||||||
|
display: flex
|
||||||
|
flex-direction: column
|
||||||
|
|
||||||
|
.description-section
|
||||||
display: grid
|
display: grid
|
||||||
grid-template-columns: 360px auto
|
align-items: start
|
||||||
max-width: 1200px
|
align-content: normal
|
||||||
margin: 0 auto
|
background-color: #eee
|
||||||
min-height: 100vh
|
position: sticky
|
||||||
|
top: 0
|
||||||
|
height: 100vh
|
||||||
|
overflow-y: auto
|
||||||
|
box-sizing: border-box
|
||||||
|
|
||||||
@media screen and (max-width: $layout-a-max)
|
@media screen and (max-width: $layout-a-max)
|
||||||
|
position: inherit
|
||||||
|
top: inherit
|
||||||
|
height: inherit
|
||||||
|
|
||||||
|
.user-header
|
||||||
display: flex
|
display: flex
|
||||||
flex-direction: column
|
|
||||||
|
|
||||||
.description-section
|
|
||||||
display: grid
|
|
||||||
align-items: start
|
|
||||||
align-content: normal
|
|
||||||
background-color: #eee
|
|
||||||
position: sticky
|
|
||||||
top: 0
|
|
||||||
height: 100vh
|
|
||||||
overflow-y: auto
|
|
||||||
box-sizing: border-box
|
|
||||||
|
|
||||||
@media screen and (max-width: $layout-a-max)
|
|
||||||
position: inherit
|
|
||||||
top: inherit
|
|
||||||
height: inherit
|
|
||||||
|
|
||||||
.user-header
|
|
||||||
display: flex
|
|
||||||
align-items: center
|
|
||||||
justify-content: center
|
|
||||||
background-color: #b3b3b3
|
|
||||||
padding: 10px
|
|
||||||
|
|
||||||
.pfp
|
|
||||||
$size: 40px
|
|
||||||
|
|
||||||
width: $size
|
|
||||||
height: $size
|
|
||||||
margin-right: 10px
|
|
||||||
background-color: rgba(40, 40, 40, 0.25)
|
|
||||||
|
|
||||||
.name
|
|
||||||
font-size: 20px
|
|
||||||
color: black
|
|
||||||
text-decoration: none
|
|
||||||
|
|
||||||
&:hover
|
|
||||||
text-decoration: underline
|
|
||||||
|
|
||||||
.description
|
|
||||||
margin: 12px
|
|
||||||
white-space: pre-line
|
|
||||||
overflow-wrap: anywhere
|
|
||||||
font-size: 20px
|
|
||||||
line-height: 1.4
|
|
||||||
|
|
||||||
@media screen and (max-width: $layout-a-max)
|
|
||||||
font-size: 18px
|
|
||||||
|
|
||||||
.images-gallery
|
|
||||||
display: flex
|
|
||||||
flex-direction: column
|
|
||||||
align-items: center
|
align-items: center
|
||||||
justify-content: center
|
justify-content: center
|
||||||
background-color: #262728
|
background-color: #b3b3b3
|
||||||
padding: 10px
|
padding: 10px
|
||||||
|
|
||||||
|
.pfp
|
||||||
|
$size: 40px
|
||||||
|
|
||||||
|
width: $size
|
||||||
|
height: $size
|
||||||
|
margin-right: 10px
|
||||||
|
background-color: rgba(40, 40, 40, 0.25)
|
||||||
|
|
||||||
|
.name
|
||||||
|
font-size: 20px
|
||||||
|
color: black
|
||||||
|
text-decoration: none
|
||||||
|
|
||||||
|
&:hover
|
||||||
|
text-decoration: underline
|
||||||
|
|
||||||
|
.description
|
||||||
|
margin: 12px
|
||||||
|
white-space: pre-line
|
||||||
|
overflow-wrap: anywhere
|
||||||
|
font-size: 20px
|
||||||
|
line-height: 1.4
|
||||||
|
|
||||||
@media screen and (max-width: $layout-a-max)
|
@media screen and (max-width: $layout-a-max)
|
||||||
flex: 1
|
font-size: 18px
|
||||||
|
|
||||||
.sized-image, .sized-video
|
.images-gallery
|
||||||
color: #eee
|
display: flex
|
||||||
background-color: #3b3c3d
|
flex-direction: column
|
||||||
max-height: 94vh
|
align-items: center
|
||||||
max-width: 100%
|
justify-content: center
|
||||||
|
background-color: #262728
|
||||||
|
padding: 10px
|
||||||
|
|
||||||
&:not(:last-child)
|
@media screen and (max-width: $layout-a-max)
|
||||||
margin-bottom: 10px
|
flex: 1
|
||||||
|
|
||||||
.sized-image
|
.sized-image, .sized-video
|
||||||
width: auto
|
color: #eee
|
||||||
height: auto
|
background-color: #3b3c3d
|
||||||
|
max-height: 94vh
|
||||||
|
max-width: 100%
|
||||||
|
|
||||||
.sized-video
|
&:not(:last-child)
|
||||||
width: 100%
|
margin-bottom: 10px
|
||||||
height: 100%
|
|
||||||
|
.sized-image
|
||||||
|
width: auto
|
||||||
|
height: auto
|
||||||
|
|
||||||
|
.sized-video
|
||||||
|
width: 100%
|
||||||
|
height: 100%
|
||||||
|
|
||||||
.error-page
|
.error-page
|
||||||
box-sizing: border-box
|
box-sizing: border-box
|
||||||
@ -490,3 +490,31 @@ body
|
|||||||
margin-top: 45px
|
margin-top: 45px
|
||||||
padding-top: 15px
|
padding-top: 15px
|
||||||
border-top: 1px solid #714141
|
border-top: 1px solid #714141
|
||||||
|
|
||||||
|
.post-overlay
|
||||||
|
position: fixed
|
||||||
|
top: 0
|
||||||
|
left: 0
|
||||||
|
right: 0
|
||||||
|
bottom: 0
|
||||||
|
background: rgba(0, 0, 0, 0.7)
|
||||||
|
z-index: 10
|
||||||
|
overflow-y: auto
|
||||||
|
|
||||||
|
&:not(.loading) > *
|
||||||
|
min-height: 100vh
|
||||||
|
|
||||||
|
&.loading
|
||||||
|
display: flex
|
||||||
|
justify-content: center
|
||||||
|
align-items: center
|
||||||
|
|
||||||
|
.loading-inner
|
||||||
|
color: white
|
||||||
|
font-size: 30px
|
||||||
|
line-height: 1
|
||||||
|
padding: 26px
|
||||||
|
border-radius: 20px
|
||||||
|
border: 2px solid #aaa
|
||||||
|
font-weight: bold
|
||||||
|
background-color: #282828
|
||||||
|
Loading…
Reference in New Issue
Block a user