Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(unlock-app): Import latest blog posts in notification menu #15258

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
175 changes: 175 additions & 0 deletions unlock-app/src/components/interface/LatestBlogs.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
import axios from 'axios'
import dayjs from 'dayjs'
import isToday from 'dayjs/plugin/isToday'
import { useEffect } from 'react'
dayjs.extend(isToday)

interface Blog {
title: string
link: string
id: string
updated: string
content: string
viewed: boolean
}

export function LatestBlogsLink({
setModalOpen,
}: {
setModalOpen: (modalOpen: boolean) => void
}) {
useEffect(() => {
saveLatestBlogs('https://unlock-protocol.com/blog.rss')
}, [])

const handleClick = async (e: React.MouseEvent) => {
e.preventDefault()
e.stopPropagation()
setModalOpen(true)
}

return (
<div className="flex flex-col gap-1 cursor-pointer" onClick={handleClick}>
<div className="font-medium">Show Latest Blogs</div>
<div className="text-sm text-gray-500">
Check out the latest updates from our blog.
</div>
</div>
)
}

export const LatestBlogs = () => {
const blogs: Blog[] = getLatestBlogs()

return (
<div className="p-4">
<h2 className="text-lg font-medium mb-4">Latest Blogs</h2>
<div className="space-y-4 max-h-[80vh] overflow-y-scroll">
{!blogs || blogs.length === 0 ? (
<div className="text-sm text-gray-600">No blogs available.</div>
) : (
blogs.map(
(blog, i) =>
!blog.viewed && (
<a
key={i}
href={blog.link}
target="_blank"
onClick={() => updateBlog(blog.id)}
rel="noopener noreferrer"
className="block p-4 border border-gray-200 rounded-lg hover:shadow-md transition-shadow bg-white"
>
<div className="flex flex-col space-y-4">
<div className="text-base font-semibold text-gray-800">
{blog.title}
</div>
<div className="text-xs text-gray-500">
Updated: {dayjs(blog.updated).format('MMM DD, YYYY')}
</div>
<div className="text-sm text-blue-600 hover:underline mt-2">
Read more →
</div>
</div>
</a>
)
)
)}
</div>
</div>
)
}

async function parseAtomFeed(url: string) {
const response = await axios.get(url)
const text = response?.data

const parser = new DOMParser()
const doc = parser.parseFromString(text, 'application/xml')

if (doc.querySelector('parsererror')) {
throw new Error('Error parsing Atom feed.')
}

const atomNS = 'http://www.w3.org/2005/Atom'
const entries = doc.getElementsByTagNameNS(atomNS, 'entry')

const storedData = localStorage.getItem('latest_blogs')
let viewedMap = new Map<string, boolean>()
if (storedData) {
const parsed = JSON.parse(storedData)
viewedMap = new Map(parsed.blogs.map((b: any) => [b.id, b.viewed]))
}

const unreadItems: Blog[] = []
for (const entry of Array.from(entries)) {
const id =
entry.getElementsByTagNameNS(atomNS, 'id')[0]?.textContent?.trim() || ''
const title =
entry.getElementsByTagNameNS(atomNS, 'title')[0]?.textContent?.trim() ||
''
const linkElement = entry.getElementsByTagNameNS(atomNS, 'link')[0]
const link = linkElement?.getAttribute('href')?.trim() || ''
const updated =
entry.getElementsByTagNameNS(atomNS, 'updated')[0]?.textContent?.trim() ||
''
const content =
entry.getElementsByTagNameNS(atomNS, 'content')[0]?.textContent?.trim() ||
''

const viewed = viewedMap.has(id) ? viewedMap.get(id)! : false
if (!viewed) {
unreadItems.push({ title, link, id, updated, content, viewed: false })
if (unreadItems.length === 10) break
}
}

return {
entries: unreadItems,
}
}

export async function saveLatestBlogs(url: string) {
const storedDate =
localStorage.getItem('latest_blogs') &&
JSON.parse(localStorage.getItem('latest_blogs')!).fetched_on
if (storedDate && dayjs(storedDate).isToday()) {
return
}

const { entries } = await parseAtomFeed(url)
localStorage.setItem(
'latest_blogs',
JSON.stringify({ blogs: entries, fetched_on: new Date().toISOString() })
)
}

function getLatestBlogs() {
const storedData = localStorage.getItem('latest_blogs')
if (!storedData) {
return null
}

const parsed = JSON.parse(storedData)
return parsed.blogs
}

function updateBlog(blogId: string) {
const storedData = localStorage.getItem('latest_blogs')
if (!storedData) return

const parsed = JSON.parse(storedData)
const updatedBlogs = parsed.blogs.map((b: Blog) => {
if (b.id === blogId) {
return { ...b, viewed: true }
}
return b
})

localStorage.setItem(
'latest_blogs',
JSON.stringify({
blogs: updatedBlogs,
fetched_on: parsed.fetched_on,
})
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Fragment, ReactNode, useState } from 'react'
import { BiBell as BellIcon } from 'react-icons/bi'
import { Button } from '@unlock-protocol/ui'
import { PromptEmailLink } from '../../PromptEmailLink'
import { LatestBlogs, LatestBlogsLink } from '../../LatestBlogs'
import { useAuthenticate } from '~/hooks/useAuthenticate'
import { usePathname } from 'next/navigation'
import { Modal } from '@unlock-protocol/ui'
Expand All @@ -23,6 +24,7 @@ interface NotificationProps {
export function NotificationsMenu() {
const [isOpen, setIsOpen] = useState(false)
const [showModal, setShowModal] = useState(false)
const [showBlogsModal, setShowBlogsModal] = useState(false)
const { account, email } = useAuthenticate()
const pathname = usePathname()

Expand All @@ -46,6 +48,12 @@ export function NotificationsMenu() {
})
}

notifications.push({
id: '2',
content: <LatestBlogsLink setModalOpen={setShowBlogsModal} />,
timestamp: new Date(),
})

return (
<>
<Menu as="div" className="relative mr-4 z-10">
Expand Down Expand Up @@ -80,7 +88,7 @@ export function NotificationsMenu() {
<MenuItems
static
className={`absolute right-1/2 translate-x-1/2 mt-2 w-80 ${
notifications.length < 3 ? 'h-auto max-h-48' : 'h-96'
notifications.length < 3 ? 'h-auto max-h-50' : 'h-96'
} origin-top rounded-md divide-y divide-gray-300 bg-white shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none overflow-y-auto`}
>
<div className="p-2">
Expand Down Expand Up @@ -111,6 +119,18 @@ export function NotificationsMenu() {
</div>
</Modal>
)}

{showBlogsModal && (
<Modal
isOpen={showBlogsModal}
setIsOpen={setShowBlogsModal}
size="small"
>
<div onClick={(e) => e.stopPropagation()}>
<LatestBlogs />
</div>
</Modal>
)}
</>
)
}
Loading