feat: blogs #118

Merged
enes merged 20 commits from feature/blogs into staging 2024-11-11 12:00:59 +00:00
3 changed files with 132 additions and 61 deletions
Showing only changes of commit f30ac01ea6 - Show all commits

View File

@ -9,17 +9,19 @@ import { marked } from 'marked'
import { LoadingSpinner } from 'components/LoadingSpinner' import { LoadingSpinner } from 'components/LoadingSpinner'
import { ProfileSection } from 'components/ProfileSection' import { ProfileSection } from 'components/ProfileSection'
import { Comments } from 'components/comment' import { Comments } from 'components/comment'
import { Addressable, BlogDetails } from 'types' import { Addressable, BlogPageLoaderResult } from 'types'
import placeholder from '../../assets/img/DEGMods Placeholder Img.png' import placeholder from '../../assets/img/DEGMods Placeholder Img.png'
import { PublishDetails } from 'components/Internal/PublishDetails' import { PublishDetails } from 'components/Internal/PublishDetails'
import { Interactions } from 'components/Internal/Interactions' import { Interactions } from 'components/Internal/Interactions'
import { BlogCard } from 'components/BlogCard'
export const BlogPage = () => { export const BlogPage = () => {
const data = useLoaderData() as Partial<BlogDetails> const { blog, latest } = useLoaderData() as BlogPageLoaderResult
const [commentCount, setCommentCount] = useState(0) const [commentCount, setCommentCount] = useState(0)
const html = marked.parse(data?.content || '', { async: false }) const html = marked.parse(blog?.content || '', { async: false })
const sanitized = DOMPurify.sanitize(html) const sanitized = DOMPurify.sanitize(html)
const editor = useEditor({ const editor = useEditor(
{
content: sanitized, content: sanitized,
extensions: [ extensions: [
StarterKit, StarterKit,
@ -32,14 +34,16 @@ export const BlogPage = () => {
}) })
], ],
editable: false editable: false
}) },
[sanitized]
)
return ( return (
<div className='InnerBodyMain'> <div className='InnerBodyMain'>
<div className='ContainerMain'> <div className='ContainerMain'>
<div className='IBMSecMainGroup IBMSecMainGroupAlt'> <div className='IBMSecMainGroup IBMSecMainGroupAlt'>
<div className='IBMSMSplitMain'> <div className='IBMSMSplitMain'>
{!data ? ( {!blog ? (
<LoadingSpinner desc={'Loading...'} /> <LoadingSpinner desc={'Loading...'} />
) : ( ) : (
<div className='IBMSMSplitMainBigSide'> <div className='IBMSMSplitMainBigSide'>
@ -67,27 +71,27 @@ export const BlogPage = () => {
className='IBMSMSMBSSPostPicture' className='IBMSMSMBSSPostPicture'
style={{ style={{
background: `url("${ background: `url("${
data.image !== '' ? data.image : placeholder blog.image !== '' ? blog.image : placeholder
}") center / cover no-repeat` }") center / cover no-repeat`
}} }}
></div> ></div>
<div className='IBMSMSMBSSPostInside'> <div className='IBMSMSMBSSPostInside'>
<div className='IBMSMSMBSSPostTitle'> <div className='IBMSMSMBSSPostTitle'>
<h1 className='IBMSMSMBSSPostTitleHeading'> <h1 className='IBMSMSMBSSPostTitleHeading'>
{data.title} {blog.title}
</h1> </h1>
</div> </div>
<div className='IBMSMSMBSSPostBody'> <div className='IBMSMSMBSSPostBody'>
<EditorContent editor={editor} /> <EditorContent editor={editor} />
</div> </div>
<div className='IBMSMSMBSSTags'> <div className='IBMSMSMBSSTags'>
{data.nsfw && ( {blog.nsfw && (
<div className='IBMSMSMBSSTagsTag IBMSMSMBSSTagsTagNSFW'> <div className='IBMSMSMBSSTagsTag IBMSMSMBSSTagsTagNSFW'>
<p>NSFW</p> <p>NSFW</p>
</div> </div>
)} )}
{data.tTags && {blog.tTags &&
data.tTags.map((t) => ( blog.tTags.map((t) => (
<a key={t} className='IBMSMSMBSSTagsTag'> <a key={t} className='IBMSMSMBSSTagsTag'>
{t} {t}
</a> </a>
@ -96,48 +100,38 @@ export const BlogPage = () => {
</div> </div>
</div> </div>
<Interactions <Interactions
addressable={data as Addressable} addressable={blog as Addressable}
commentCount={commentCount} commentCount={commentCount}
/> />
<PublishDetails <PublishDetails
published_at={data.published_at || 0} published_at={blog.published_at || 0}
edited_at={data.edited_at || 0} edited_at={blog.edited_at || 0}
site={data.rTag || 'N/A'} site={blog.rTag || 'N/A'}
/> />
{/* <div className="IBMSMSplitMainBigSideSec"> {!!latest.length && (
<div className="IBMSMSMBSSPostsWrapper"> <div className='IBMSMSplitMainBigSideSec'>
<h4 className="IBMSMSMBSSPostsTitle">Latest POSTER-NAME Posts</h4> <div className='IBMSMSMBSSPostsWrapper'>
<div className="IBMSMList IBMSMListAlt"><a className="cardBlogMainWrapperLink" href="blog-inner.html"> <h4 className='IBMSMSMBSSPostsTitle'>
<div className="cardBlogMain" style="background: url(&quot;https://nichegamer.com/wp-content/uploads/2023/01/onimai-01-07-2023.jpg&quot;) center / cover no-repeat;"> Latest blog posts
<div className="cardBlogMainInside" style="background: linear-gradient(rgba(255,255,255,0) 0%, #232323 100%);"> </h4>
<h3 style="display: -webkit-box;-webkit-box-orient: vertical;overflow: hidden;-webkit-line-clamp: 2;font-size: 20px;line-height: 1.5;color: rgba(255,255,255,0.75);text-shadow: 0 0 8px rgba(0,0,0,0.25);">This is a blog title, the best blog title in the world!</h3> <div className='IBMSMList IBMSMListAlt'>
{latest.map((b) => (
<BlogCard key={b.id} {...b} />
))}
</div> </div>
</div> </div>
</a><a className="cardBlogMainWrapperLink" href="blog-inner.html">
<div className="cardBlogMain" style="background: url(&quot;https://pbs.twimg.com/media/GDrRJOOXYAAeysT.jpg:large&quot;) center / cover no-repeat;">
<div className="cardBlogMainInside" style="background: linear-gradient(rgba(255,255,255,0) 0%, #232323 100%);">
<h3 style="display: -webkit-box;-webkit-box-orient: vertical;overflow: hidden;-webkit-line-clamp: 2;font-size: 20px;line-height: 1.5;color: rgba(255,255,255,0.75);text-shadow: 0 0 8px rgba(0,0,0,0.25);">This is a blog title, the best blog title in the world!</h3>
</div> </div>
</div> )}
</a><a className="cardBlogMainWrapperLink" href="blog-inner.html">
<div className="cardBlogMain" style="background: url(&quot;assets/img/DEGMods%20Placeholder%20Img.png&quot;) center / cover no-repeat;">
<div className="cardBlogMainInside" style="background: linear-gradient(rgba(255,255,255,0) 0%, #232323 100%);">
<h3 style="display: -webkit-box;-webkit-box-orient: vertical;overflow: hidden;-webkit-line-clamp: 2;font-size: 20px;line-height: 1.5;color: rgba(255,255,255,0.75);text-shadow: 0 0 8px rgba(0,0,0,0.25);">This is a blog title, the best blog title in the world!</h3>
</div>
</div>
</a></div>
</div>
</div> */}
<div className='IBMSMSplitMainBigSideSec'> <div className='IBMSMSplitMainBigSideSec'>
<Comments <Comments
addressable={data as Addressable} addressable={blog as Addressable}
setCommentCount={setCommentCount} setCommentCount={setCommentCount}
/> />
</div> </div>
</div> </div>
</div> </div>
)} )}
{!!data?.author && <ProfileSection pubkey={data.author} />} {!!blog?.author && <ProfileSection pubkey={blog.author} />}
</div> </div>
</div> </div>
</div> </div>

View File

@ -1,10 +1,17 @@
import { filterForEventsTaggingId } from '@nostr-dev-kit/ndk' import { filterForEventsTaggingId, NDKFilter } from '@nostr-dev-kit/ndk'
import { NDKContextType } from 'contexts/NDKContext' import { NDKContextType } from 'contexts/NDKContext'
import { kinds, nip19 } from 'nostr-tools'
import { LoaderFunctionArgs, redirect } from 'react-router-dom' import { LoaderFunctionArgs, redirect } from 'react-router-dom'
import { toast } from 'react-toastify' import { toast } from 'react-toastify'
import { appRoutes } from 'routes' import { appRoutes } from 'routes'
import { log, LogType } from 'utils' import { BlogPageLoaderResult, FilterOptions, NSFWFilter } from 'types'
import { extractBlogDetails } from 'utils/blog' import {
DEFAULT_FILTER_OPTIONS,
getLocalStorageItem,
log,
LogType
} from 'utils'
import { extractBlogCardDetails, extractBlogDetails } from 'utils/blog'
export const blogRouteLoader = export const blogRouteLoader =
(ndkContext: NDKContextType) => (ndkContext: NDKContextType) =>
@ -15,21 +22,86 @@ export const blogRouteLoader =
return redirect(appRoutes.blogs) return redirect(appRoutes.blogs)
} }
// Decode author from naddr
const decoded = nip19.decode<'naddr'>(naddr as `naddr1${string}`)
const { pubkey } = decoded.data
try { try {
// Get the filter with #a from naddr for the main blog content
const filter = filterForEventsTaggingId(naddr) const filter = filterForEventsTaggingId(naddr)
if (!filter) { if (!filter) {
log(true, LogType.Error, 'Unable to create filter from blog naddr.') log(true, LogType.Error, 'Unable to create filter from blog naddr.')
return redirect(appRoutes.blogs) return redirect(appRoutes.blogs)
} }
const event = await ndkContext.fetchEvent(filter) // Get the blog filter options for latest blogs
if (!event) { const filterOptions = JSON.parse(
log(true, LogType.Error, 'Unable to fetch the blog event.') getLocalStorageItem('filter-blog', DEFAULT_FILTER_OPTIONS)
return null ) as FilterOptions
// Fetch 4 in case the current blog is included in the latest
const latestModsFilter: NDKFilter = {
authors: [pubkey],
kinds: [kinds.LongFormArticle],
limit: 4
}
// Add source filter
if (filterOptions.source === window.location.host) {
latestModsFilter['#r'] = [filterOptions.source]
}
// Filter by NSFW tag
// NSFWFilter.Show_NSFW -> filter not needed
// NSFWFilter.Only_NSFW -> true
// NSFWFilter.Hide_NSFW -> false
if (filterOptions.nsfw !== NSFWFilter.Show_NSFW) {
latestModsFilter['#nsfw'] = [
(filterOptions.nsfw === NSFWFilter.Only_NSFW).toString()
]
} }
const blogDetails = extractBlogDetails(event) // Parallel fetch blog event and latest events
return blogDetails const settled = await Promise.allSettled([
ndkContext.fetchEvent(filter),
ndkContext.fetchEvents(latestModsFilter)
])
const result: BlogPageLoaderResult = {
blog: undefined,
latest: []
}
// Check the blog event result
const fetchEventResult = settled[0]
if (fetchEventResult.status === 'fulfilled' && fetchEventResult.value) {
// Extract the blog details from the event
result.blog = extractBlogDetails(fetchEventResult.value)
} else if (fetchEventResult.status === 'rejected') {
log(
true,
LogType.Error,
'Unable to fetch the blog event.',
fetchEventResult.reason
)
}
// Check the lateast blog events
const fetchEventsResult = settled[1]
if (fetchEventsResult.status === 'fulfilled' && fetchEventsResult.value) {
// Extract the blog card details from the events
result.latest = fetchEventsResult.value
.map(extractBlogCardDetails)
.filter((b) => b.id !== result.blog?.id) // Filter out current blog if present
.slice(0, 3) // Take only three
} else if (fetchEventsResult.status === 'rejected') {
log(
true,
LogType.Error,
'Unable to fetch the latest blog events.',
fetchEventsResult.reason
)
}
return result
} catch (error) { } catch (error) {
log( log(
true, true,

View File

@ -27,3 +27,8 @@ export interface BlogFormErrors extends Partial<BlogEventSubmitForm> {}
export interface BlogCardDetails extends BlogDetails { export interface BlogCardDetails extends BlogDetails {
naddr: string naddr: string
} }
export interface BlogPageLoaderResult {
blog: Partial<BlogDetails> | undefined
latest: Partial<BlogDetails>[]
}