diff --git a/unlock-app/src/components/interface/LatestBlogs.tsx b/unlock-app/src/components/interface/LatestBlogs.tsx new file mode 100644 index 00000000000..ce6134f76ed --- /dev/null +++ b/unlock-app/src/components/interface/LatestBlogs.tsx @@ -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 ( +
+
Show Latest Blogs
+
+ Check out the latest updates from our blog. +
+
+ ) +} + +export const LatestBlogs = () => { + const blogs: Blog[] = getLatestBlogs() + + return ( +
+

Latest Blogs

+
+ {!blogs || blogs.length === 0 ? ( +
No blogs available.
+ ) : ( + blogs.map( + (blog, i) => + !blog.viewed && ( + updateBlog(blog.id)} + rel="noopener noreferrer" + className="block p-4 border border-gray-200 rounded-lg hover:shadow-md transition-shadow bg-white" + > +
+
+ {blog.title} +
+
+ Updated: {dayjs(blog.updated).format('MMM DD, YYYY')} +
+
+ Read more → +
+
+
+ ) + ) + )} +
+
+ ) +} + +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() + 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, + }) + ) +} diff --git a/unlock-app/src/components/interface/layouts/index/NotificationsMenu.tsx b/unlock-app/src/components/interface/layouts/index/NotificationsMenu.tsx index 761016b3fc7..f302f195f34 100644 --- a/unlock-app/src/components/interface/layouts/index/NotificationsMenu.tsx +++ b/unlock-app/src/components/interface/layouts/index/NotificationsMenu.tsx @@ -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' @@ -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() @@ -46,6 +48,12 @@ export function NotificationsMenu() { }) } + notifications.push({ + id: '2', + content: , + timestamp: new Date(), + }) + return ( <> @@ -80,7 +88,7 @@ export function NotificationsMenu() {
@@ -111,6 +119,18 @@ export function NotificationsMenu() {
)} + + {showBlogsModal && ( + +
e.stopPropagation()}> + +
+
+ )} ) }