From 4a7899cfde21a8189e08ce89f9dcf49f78b00b4a Mon Sep 17 00:00:00 2001 From: freakoverse Date: Mon, 2 Sep 2024 18:52:21 +0000 Subject: [PATCH 1/5] Update src/styles/cardGames.css --- src/styles/cardGames.css | 1 + 1 file changed, 1 insertion(+) diff --git a/src/styles/cardGames.css b/src/styles/cardGames.css index 1e6aadd..b440c9e 100644 --- a/src/styles/cardGames.css +++ b/src/styles/cardGames.css @@ -43,4 +43,5 @@ -webkit-line-clamp: 1; font-size: 18px; line-height: 1.5; + text-align: center; } -- 2.34.1 From fad1ff98b3266dcd99a12b9e2a8e3a36538cc919 Mon Sep 17 00:00:00 2001 From: daniyal Date: Tue, 3 Sep 2024 13:05:37 +0500 Subject: [PATCH 2/5] feat: implemented logic for profile box --- src/components/ProfileSection.tsx | 615 +++++++++++++++++++++++++++--- src/pages/innerMod.tsx | 2 +- src/pages/profile.tsx | 3 + src/routes/index.tsx | 11 +- 4 files changed, 579 insertions(+), 52 deletions(-) create mode 100644 src/pages/profile.tsx diff --git a/src/components/ProfileSection.tsx b/src/components/ProfileSection.tsx index 0b15bad..8277205 100644 --- a/src/components/ProfileSection.tsx +++ b/src/components/ProfileSection.tsx @@ -1,8 +1,66 @@ +import { Filter, kinds, nip19, UnsignedEvent, Event } from 'nostr-tools' +import { QRCodeSVG } from 'qrcode.react' +import { useCallback, useState } from 'react' +import { toast } from 'react-toastify' +import { + MetadataController, + RelayController, + UserRelaysType, + ZapController +} from '../controllers' +import { useAppSelector, useDidMount } from '../hooks' import '../styles/author.css' import '../styles/innerPage.css' import '../styles/socialPosts.css' +import { PaymentRequest, UserProfile } from '../types' +import { + copyTextToClipboard, + formatNumber, + log, + LogType, + unformatNumber, + now +} from '../utils' +import { LoadingSpinner } from './LoadingSpinner' +import { ZapButtons, ZapPresets, ZapQR } from './Zap' +import { getProfilePageRoute } from '../routes' +import { useNavigate } from 'react-router-dom' + +type Props = { + pubkey: string +} + +export const ProfileSection = ({ pubkey }: Props) => { + const navigate = useNavigate() + const [profile, setProfile] = useState() + + useDidMount(async () => { + const metadataController = await MetadataController.getInstance() + metadataController.findMetadata(pubkey).then((res) => { + setProfile(res) + }) + }) + + const handleCopy = async () => { + copyTextToClipboard(profile?.npub as string).then((isCopied) => { + if (isCopied) { + toast.success('Npub copied to clipboard!') + } else { + toast.error( + 'Failed to copy, look into console for more details on error!' + ) + } + }) + } + + if (!profile) return null + + const profileRoute = getProfilePageRoute( + nip19.nprofileEncode({ + pubkey + }) + ) -export const ProfileSection = () => { return (
@@ -12,7 +70,11 @@ export const ProfileSection = () => {
-

{author.bio}

+

{profile.bio}

{ >
- +
@@ -154,20 +191,6 @@ export const ProfileSection = () => { ) } -interface Author { - name: string - handle: string - address: string - bio: string -} - -const author: Author = { - name: 'User name', - handle: 'nip5handle@domain.com', - address: 'npub1address', - bio: `user bio, this is a long string of temporary text that would be replaced with the user bio from their metada address` -} - interface Post { name: string link: string @@ -193,3 +216,495 @@ const posts: Post[] = [ imageUrl: '/assets/img/DEGMods%20Placeholder%20Img.png' } ] + +type QRButtonWithPopUpProps = { + pubkey: string +} + +const QRButtonWithPopUp = ({ pubkey }: QRButtonWithPopUpProps) => { + const [isOpen, setIsOpen] = useState(false) + + const nprofile = nip19.nprofileEncode({ + pubkey + }) + + const onQrCodeClicked = async () => { + const href = `https://njump.me/${nprofile}` + const a = document.createElement('a') + a.href = href + a.target = '_blank' // Open in a new tab + a.rel = 'noopener noreferrer' // Recommended for security reasons + a.click() + } + + return ( + <> +
setIsOpen(true)} + > + + + +
+ + {isOpen && ( +
+
+
+
+
+
+

Nostr Address

+
+
setIsOpen(false)} + > + + + +
+
+
+ +
+
+
+
+
+ )} + + ) +} + +type ZapButtonWithPopUpProps = { + pubkey: string +} + +const ZapButtonWithPopUp = ({ pubkey }: ZapButtonWithPopUpProps) => { + const [isOpen, setIsOpen] = useState(false) + const [amount, setAmount] = useState(0) + const [message, setMessage] = useState('') + + const [isLoading, setIsLoading] = useState(false) + const [loadingSpinnerDesc, setLoadingSpinnerDesc] = useState('') + + const [paymentRequest, setPaymentRequest] = useState() + + const userState = useAppSelector((state) => state.user) + + const handleClose = useCallback(() => { + setPaymentRequest(undefined) + setIsLoading(false) + setIsOpen(false) + }, []) + + const handleQRExpiry = useCallback(() => { + setPaymentRequest(undefined) + }, []) + + const handleAmountChange = (event: React.ChangeEvent) => { + const unformattedValue = unformatNumber(event.target.value) + setAmount(unformattedValue) + } + + const generatePaymentRequest = + useCallback(async (): Promise => { + let userHexKey: string + + setIsLoading(true) + setLoadingSpinnerDesc('Getting user pubkey') + + if (userState.auth && userState.user?.pubkey) { + userHexKey = userState.user.pubkey as string + } else { + userHexKey = (await window.nostr?.getPublicKey()) as string + } + + if (!userHexKey) { + setIsLoading(false) + toast.error('Could not get pubkey') + return null + } + + setLoadingSpinnerDesc('Getting admin metadata') + const metadataController = await MetadataController.getInstance() + + const authorMetadata = await metadataController.findMetadata(pubkey) + + if (!authorMetadata?.lud16) { + setIsLoading(false) + toast.error('Lighting address (lud16) is missing in admin metadata!') + return null + } + + if (!authorMetadata?.pubkey) { + setIsLoading(false) + toast.error('pubkey is missing in admin metadata!') + return null + } + + const zapController = ZapController.getInstance() + + setLoadingSpinnerDesc('Creating zap request') + return await zapController + .getLightningPaymentRequest( + authorMetadata.lud16, + amount, + authorMetadata.pubkey as string, + userHexKey, + message + ) + .catch((err) => { + toast.error(err.message || err) + return null + }) + .finally(() => { + setIsLoading(false) + }) + }, [amount, message, userState, pubkey]) + + const handleSend = useCallback(async () => { + const pr = await generatePaymentRequest() + + if (!pr) return + + setIsLoading(true) + setLoadingSpinnerDesc('Sending payment!') + + const zapController = ZapController.getInstance() + + if (await zapController.isWeblnProviderExists()) { + await zapController + .sendPayment(pr.pr) + .then(() => { + toast.success(`Successfully sent ${amount} sats!`) + handleClose() + }) + .catch((err) => { + toast.error(err.message || err) + }) + } else { + toast.warn('Webln is not present. Use QR code to send zap.') + setPaymentRequest(pr) + } + + setIsLoading(false) + }, [amount, handleClose, generatePaymentRequest]) + + const handleGenerateQRCode = async () => { + const pr = await generatePaymentRequest() + + if (!pr) return + + setPaymentRequest(pr) + } + + return ( + <> +
setIsOpen(true)} + > + + + +
+ {isOpen && ( +
+
+
+
+
+
+

Tip/Zap

+
+
+ + + +
+
+
+
+
+
+ + +
+
+ +
+
+
+ + setMessage(e.target.value)} + /> +
+ + {paymentRequest && ( + + )} +
+
+
+
+
+
+ )} + {isLoading && } + + ) +} + +type FollowButtonProps = { + pubkey: string +} + +const FollowButton = ({ pubkey }: FollowButtonProps) => { + const [isFollowing, setIsFollowing] = useState(false) + + const [isLoading, setIsLoading] = useState(false) + const [loadingSpinnerDesc, setLoadingSpinnerDesc] = useState('') + + const userState = useAppSelector((state) => state.user) + + useDidMount(async () => { + if (userState.auth && userState.user?.pubkey) { + const userHexKey = userState.user.pubkey as string + const { isFollowing: isAlreadyFollowing } = await checkIfFollowing( + userHexKey, + pubkey + ) + setIsFollowing(isAlreadyFollowing) + } + }) + + const getUserPubKey = async (): Promise => { + if (userState.auth && userState.user?.pubkey) { + return userState.user.pubkey as string + } else { + return (await window.nostr?.getPublicKey()) as string + } + } + + const checkIfFollowing = async ( + userHexKey: string, + pubkey: string + ): Promise<{ + isFollowing: boolean + tags: string[][] + }> => { + const filter: Filter = { + kinds: [kinds.Contacts], + authors: [userHexKey] + } + + const contactListEvent = + await RelayController.getInstance().fetchEventFromUserRelays( + filter, + userHexKey, + UserRelaysType.Both + ) + + if (!contactListEvent) + return { + isFollowing: false, + tags: [] + } + + return { + isFollowing: contactListEvent.tags.some( + (t) => t[0] === 'p' && t[1] === pubkey + ), + tags: contactListEvent.tags + } + } + + const signAndPublishEvent = async ( + unsignedEvent: UnsignedEvent + ): Promise => { + const signedEvent = await window.nostr + ?.signEvent(unsignedEvent) + .then((event) => event as Event) + .catch((err) => { + toast.error('Failed to sign the event!') + log(true, LogType.Error, 'Failed to sign the event!', err) + return null + }) + + if (!signedEvent) return false + + const publishedOnRelays = await RelayController.getInstance().publish( + signedEvent as Event + ) + + if (publishedOnRelays.length === 0) { + toast.error('Failed to publish event on any relay') + return false + } + + toast.success( + `Event published successfully to the following relays\n\n${publishedOnRelays.join( + '\n' + )}` + ) + return true + } + + const handleFollow = async () => { + setIsLoading(true) + setLoadingSpinnerDesc('Processing follow request') + + const userHexKey = await getUserPubKey() + if (!userHexKey) { + setIsLoading(false) + toast.error('Could not get pubkey') + return + } + + const { isFollowing: isAlreadyFollowing, tags } = await checkIfFollowing( + userHexKey, + pubkey + ) + if (isAlreadyFollowing) { + toast.info('Already following!') + setIsFollowing(true) + setIsLoading(false) + return + } + + const unsignedEvent: UnsignedEvent = { + content: '', + created_at: now(), + kind: kinds.Contacts, + pubkey: userHexKey, + tags: [...tags, ['p', pubkey]] + } + + setLoadingSpinnerDesc('Signing and publishing follow event') + const success = await signAndPublishEvent(unsignedEvent) + setIsFollowing(success) + setIsLoading(false) + } + + const handleUnFollow = async () => { + setIsLoading(true) + setLoadingSpinnerDesc('Processing unfollow request') + + const userHexKey = await getUserPubKey() + if (!userHexKey) { + setIsLoading(false) + toast.error('Could not get pubkey') + return + } + + const filter: Filter = { + kinds: [kinds.Contacts], + authors: [userHexKey] + } + + const contactListEvent = + await RelayController.getInstance().fetchEventFromUserRelays( + filter, + userHexKey, + UserRelaysType.Both + ) + + if ( + !contactListEvent || + !contactListEvent.tags.some((t) => t[0] === 'p' && t[1] === pubkey) + ) { + // could not found target pubkey in user's follow list + // so, just update the status and return + setIsFollowing(false) + setIsLoading(false) + return + } + + const unsignedEvent: UnsignedEvent = { + content: '', + created_at: now(), + kind: kinds.Contacts, + pubkey: userHexKey, + tags: contactListEvent.tags.filter( + (t) => !(t[0] === 'p' && t[1] === pubkey) + ) + } + + setLoadingSpinnerDesc('Signing and publishing unfollow event') + const success = await signAndPublishEvent(unsignedEvent) + setIsFollowing(!success) + setIsLoading(false) + } + + return ( + <> + + {isLoading && } + + ) +} diff --git a/src/pages/innerMod.tsx b/src/pages/innerMod.tsx index 9df1856..797e439 100644 --- a/src/pages/innerMod.tsx +++ b/src/pages/innerMod.tsx @@ -199,7 +199,7 @@ export const InnerModPage = () => {
- + diff --git a/src/pages/profile.tsx b/src/pages/profile.tsx new file mode 100644 index 0000000..3ecd47a --- /dev/null +++ b/src/pages/profile.tsx @@ -0,0 +1,3 @@ +export const ProfilePage = () => { + return

WIP

+} diff --git a/src/routes/index.tsx b/src/routes/index.tsx index 46d6ea7..7efc2b6 100644 --- a/src/routes/index.tsx +++ b/src/routes/index.tsx @@ -4,6 +4,7 @@ import { GamesPage } from '../pages/games' import { HomePage } from '../pages/home' import { InnerModPage } from '../pages/innerMod' import { ModsPage } from '../pages/mods' +import { ProfilePage } from '../pages/profile' import { SettingsPage } from '../pages/settings' import { SubmitModPage } from '../pages/submitMod' import { WritePage } from '../pages/write' @@ -22,7 +23,8 @@ export const appRoutes = { settingsProfile: '/settings-profile', settingsRelays: '/settings-relays', settingsPreferences: '/settings-preferences', - settingsAdmin: '/settings-admin' + settingsAdmin: '/settings-admin', + profile: '/profile/:nprofile' } export const getModsInnerPageRoute = (eventId: string) => @@ -31,6 +33,9 @@ export const getModsInnerPageRoute = (eventId: string) => export const getModsEditPageRoute = (eventId: string) => appRoutes.editMod.replace(':naddr', eventId) +export const getProfilePageRoute = (nprofile: string) => + appRoutes.profile.replace(':nprofile', nprofile) + export const routes = [ { path: appRoutes.index, @@ -87,5 +92,9 @@ export const routes = [ { path: appRoutes.settingsAdmin, element: + }, + { + path: appRoutes.profile, + element: } ] -- 2.34.1 From 8fea6fa27ff4f21d94b48d3d58d52a207e905f23 Mon Sep 17 00:00:00 2001 From: daniyal Date: Tue, 3 Sep 2024 13:15:47 +0500 Subject: [PATCH 3/5] chore: quick fix --- src/pages/settings.tsx | 14 ++++++++------ src/pages/submitMod.tsx | 8 ++++++-- src/pages/write.tsx | 11 +++++++++-- 3 files changed, 23 insertions(+), 10 deletions(-) diff --git a/src/pages/settings.tsx b/src/pages/settings.tsx index 6bfa938..a86a6eb 100644 --- a/src/pages/settings.tsx +++ b/src/pages/settings.tsx @@ -17,6 +17,7 @@ import { copyTextToClipboard } from '../utils' export const SettingsPage = () => { const location = useLocation() + const userState = useAppSelector((state) => state.user) return (
@@ -34,7 +35,9 @@ export const SettingsPage = () => { )} {location.pathname === appRoutes.settingsAdmin && } - + {userState.auth && userState.user?.pubkey && ( + + )}
@@ -268,9 +271,7 @@ const ProfileSettings = () => { -
+
{ fill='currentColor' className='IBMSMSMSSS_Author_Top_Icon' > - +

- user bio, this is a long string of temporary text that would be replaced with the user bio from their metada address + user bio, this is a long string of temporary text that would + be replaced with the user bio from their metada address

{ const location = useLocation() @@ -19,6 +19,8 @@ export const SubmitModPage = () => { const [modData, setModData] = useState() const [isFetching, setIsFetching] = useState(false) + const userState = useAppSelector((state) => state.user) + const title = location.pathname.startsWith('/edit-mod') ? 'Edit Mod' : 'Submit a mod' @@ -74,7 +76,9 @@ export const SubmitModPage = () => { )}
- + {userState.auth && userState.user?.pubkey && ( + + )} diff --git a/src/pages/write.tsx b/src/pages/write.tsx index 2f29851..230edd7 100644 --- a/src/pages/write.tsx +++ b/src/pages/write.tsx @@ -1,10 +1,13 @@ import { CheckboxField, InputField } from '../components/Inputs' import { ProfileSection } from '../components/ProfileSection' +import { useAppSelector } from '../hooks' import '../styles/innerPage.css' import '../styles/styles.css' import '../styles/write.css' export const WritePage = () => { + const userState = useAppSelector((state) => state.user) + return (
@@ -12,7 +15,9 @@ export const WritePage = () => {
-

Write a blog post (WIP)

+

+ Write a blog post (WIP) +

{
- + {userState.auth && userState.user?.pubkey && ( + + )}
-- 2.34.1 From c44a28f7558f871b84647ab386be01626ba78e7a Mon Sep 17 00:00:00 2001 From: daniyal Date: Tue, 3 Sep 2024 14:37:54 +0500 Subject: [PATCH 4/5] fix: fixed profile picture and bio in profile box --- src/components/ProfileSection.tsx | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/components/ProfileSection.tsx b/src/components/ProfileSection.tsx index 8277205..8dbc1c9 100644 --- a/src/components/ProfileSection.tsx +++ b/src/components/ProfileSection.tsx @@ -82,8 +82,9 @@ export const ProfileSection = ({ pubkey }: Props) => {
@@ -132,7 +133,9 @@ export const ProfileSection = ({ pubkey }: Props) => {
-

{profile.bio}

+

+ {profile.bio || profile.about} +

Date: Tue, 3 Sep 2024 14:59:23 +0500 Subject: [PATCH 5/5] fix: sort the mods in by published_at before displaying in latest mods section of landing page --- src/pages/home.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pages/home.tsx b/src/pages/home.tsx index eabe56b..7c697a9 100644 --- a/src/pages/home.tsx +++ b/src/pages/home.tsx @@ -251,6 +251,7 @@ const DisplayLatestMods = () => { useDidMount(() => { fetchMods({ source: window.location.host, limit: 4 }) .then((res) => { + res.sort((a, b) => b.published_at - a.published_at) setLatestMods(res) }) .finally(() => { -- 2.34.1