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

ENG-4079 feat(portal): add share button modal detail pages #847

Merged
merged 6 commits into from
Sep 17, 2024
Merged
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
32 changes: 32 additions & 0 deletions apps/portal/app/assets/intuition-qr-icon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
50 changes: 50 additions & 0 deletions apps/portal/app/components/share-cta.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { Button, Text } from '@0xintuition/1ui'

const DEFAULT_SHARE_CTA =
'Share this content with your network using a direct link or QR code.'
export interface ShareCtaProps {
title?: string
onShareClick: () => void
}

// TODO: [ENG-4080] Add QR Code icon to 1ui

export default function ShareCta({
title = DEFAULT_SHARE_CTA,
onShareClick,
}: ShareCtaProps) {
return (
<div className="flex flex-col w-full justify-between rounded-lg theme-border items-center gap-3 p-5">
<div className="flex gap-2 items-center">
<div className="p-0">
<svg
width="36"
height="36"
viewBox="0 0 36 36"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M20.625 20.625V24.375H24.375M26.625 20.625H30.375M30.375 26.625H26.625V30.375M20.625 29.625V30.375M8.625 30.375H12.375C14.0319 30.375 15.375 29.0319 15.375 27.375V23.625C15.375 21.9681 14.0319 20.625 12.375 20.625H8.625C6.96814 20.625 5.625 21.9681 5.625 23.625V27.375C5.625 29.0319 6.96814 30.375 8.625 30.375ZM23.625 15.375H27.375C29.0319 15.375 30.375 14.0319 30.375 12.375V8.625C30.375 6.96814 29.0319 5.625 27.375 5.625H23.625C21.9681 5.625 20.625 6.96814 20.625 8.625V12.375C20.625 14.0319 21.9681 15.375 23.625 15.375ZM8.625 15.375H12.375C14.0319 15.375 15.375 14.0319 15.375 12.375V8.625C15.375 6.96814 14.0319 5.625 12.375 5.625H8.625C6.96814 5.625 5.625 6.96814 5.625 8.625V12.375C5.625 14.0319 6.96814 15.375 8.625 15.375Z"
stroke="white"
strokeOpacity="0.7"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</div>
<Text
variant="caption"
weight="medium"
className="text-secondary-foreground/70"
>
{title}
</Text>
</div>
<Button variant="secondary" className="w-full" onClick={onShareClick}>
Share
</Button>
</div>
)
}
125 changes: 125 additions & 0 deletions apps/portal/app/components/share-modal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import { useEffect, useRef, useState } from 'react'

import {
Button,
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
Text,
} from '@0xintuition/1ui'

import intuitionIcon from '@assets/intuition-qr-icon.svg'
import logger from '@lib/utils/logger'
import QRCodeStyling from '@solana/qr-code-styling'
import { ClientOnly } from 'remix-utils/client-only'

const SHARE_MODAL_MESSAGE =
'Easily share this with others. Copy the link or use the QR code for quick access.'

export interface ShareModalProps {
currentPath: string
open?: boolean
onClose: () => void
}

function getShareableUrl(path: string): string {
return `${window.location.origin}${path.replace('/app/', '/readonly/')}`
}

function createShareQRCode(path: string) {
return new QRCodeStyling({
width: 256,
height: 256,
type: 'svg',
// data: `${window.location.origin}${path.replace('/app/', '/readonly/')}`,
data: getShareableUrl(path),
dotsOptions: { type: 'classy-rounded', color: '#000000' },
backgroundOptions: { color: '#ffffff' },
cornersSquareOptions: { type: 'dot', color: '#007AFF' },
cornersDotOptions: { type: 'dot', color: '#000000' },
imageOptions: { hideBackgroundDots: true, imageSize: 0.5, margin: 2 },
image: intuitionIcon,
})
}

function ShareQRInner({
currentPath,
...props
}: { currentPath: string } & React.HTMLAttributes<HTMLDivElement>) {
const qrRef = useRef<HTMLDivElement>(null)
const qrCode = useRef<QRCodeStyling>()
const [, setQrRendered] = useState(false)

useEffect(() => {
if (!qrCode.current) {
qrCode.current = createShareQRCode(currentPath)
}
if (qrRef.current && qrCode.current) {
qrCode.current.append(qrRef.current)
setQrRendered(true)
}
}, [currentPath])

useEffect(() => {
if (qrCode.current) {
qrCode.current.update({
data: `${window.location.origin}${currentPath.replace('/app/', '/readonly/')}`,
})
}
}, [currentPath])

return <div ref={qrRef} {...props} />
}

function ShareModalContent({ currentPath }: ShareModalProps) {
logger('currentPath', currentPath)

const handleCopyLink = () => {
navigator.clipboard
.writeText(getShareableUrl(currentPath))
.then(() => {
// Optionally, you can add some feedback here, like a toast notification
logger('Link copied to clipboard')
})
.catch((err) => {
console.error('Failed to copy link:', err)
})
}

return (
<DialogContent className="bg-neutral-950 rounded-xl shadow border-theme h-[550px] flex flex-col">
<DialogHeader>
<DialogTitle>Share via Link or QR Code</DialogTitle>
</DialogHeader>
<Text variant="caption" weight="regular" className="text-foreground/70">
{SHARE_MODAL_MESSAGE}
</Text>
<div className="flex flex-col items-center justify-center w-full h-full gap-10">
<ClientOnly fallback={<div>Loading QR Code...</div>}>
{() => <ShareQRInner currentPath={currentPath} />}
</ClientOnly>
<Button variant="accent" onClick={handleCopyLink}>
Copy Link
</Button>
</div>
</DialogContent>
)
}

export default function ShareModal({
currentPath,
open,
onClose,
}: ShareModalProps) {
return (
<Dialog
open={open}
onOpenChange={() => {
onClose?.()
}}
>
<ShareModalContent currentPath={currentPath} onClose={onClose} />
</Dialog>
)
}
8 changes: 8 additions & 0 deletions apps/portal/app/lib/state/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,3 +100,11 @@ export const createClaimModalAtom = atom<{
predicate: null,
object: null,
})

export const shareModalAtom = atom<{
isOpen: boolean
currentPath: string | null
}>({
isOpen: false,
currentPath: null,
})
23 changes: 22 additions & 1 deletion apps/portal/app/routes/app+/claim+/$id.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,13 @@ import {
import { DetailInfoCard } from '@components/detail-info-card'
import { ErrorPage } from '@components/error-page'
import NavigationButton from '@components/navigation-link'
import ShareCta from '@components/share-cta'
import ShareModal from '@components/share-modal'
import StakeModal from '@components/stake/stake-modal'
import { useGoBack } from '@lib/hooks/useGoBack'
import { useLiveLoader } from '@lib/hooks/useLiveLoader'
import { getClaimOrPending } from '@lib/services/claims'
import { stakeModalAtom } from '@lib/state/store'
import { shareModalAtom, stakeModalAtom } from '@lib/state/store'
import { getSpecialPredicate } from '@lib/utils/app'
import {
calculatePercentageOfTvl,
Expand Down Expand Up @@ -115,6 +117,7 @@ export default function ClaimDetails() {
isPending: boolean
}>(['create', 'attest'])
const [stakeModalActive, setStakeModalActive] = useAtom(stakeModalAtom)
const [shareModalActive, setShareModalActive] = useAtom(shareModalAtom)

const direction: 'for' | 'against' = isPending
? 'for'
Expand Down Expand Up @@ -308,6 +311,14 @@ export default function ClaimDetails() {
timestamp={claim.created_at}
className="w-full"
/>
<ShareCta
onShareClick={() =>
setShareModalActive({
isOpen: true,
currentPath: location.pathname,
})
}
/>
</div>
)

Expand Down Expand Up @@ -350,6 +361,16 @@ export default function ClaimDetails() {
}}
/>
)}
<ShareModal
currentPath={location.pathname}
open={shareModalActive.isOpen}
onClose={() =>
setShareModalActive({
...shareModalActive,
isOpen: false,
})
}
/>
</>
)
}
Expand Down
22 changes: 22 additions & 0 deletions apps/portal/app/routes/app+/identity+/$id.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,16 @@ import { ErrorPage } from '@components/error-page'
import NavigationButton from '@components/navigation-link'
import ImageModal from '@components/profile/image-modal'
import SaveListModal from '@components/save-list/save-list-modal'
import ShareCta from '@components/share-cta'
import ShareModal from '@components/share-modal'
import StakeModal from '@components/stake/stake-modal'
import TagsModal from '@components/tags/tags-modal'
import { useLiveLoader } from '@lib/hooks/useLiveLoader'
import { getIdentityOrPending } from '@lib/services/identities'
import {
imageModalAtom,
saveListModalAtom,
shareModalAtom,
stakeModalAtom,
tagsModalAtom,
} from '@lib/state/store'
Expand Down Expand Up @@ -145,6 +148,7 @@ export default function IdentityDetails() {
useAtom(saveListModalAtom)
const [imageModalActive, setImageModalActive] = useAtom(imageModalAtom)
const [selectedTag, setSelectedTag] = useState<TagEmbeddedPresenter>()
const [shareModalActive, setShareModalActive] = useAtom(shareModalAtom)

useEffect(() => {
if (saveListModalActive.tag) {
Expand Down Expand Up @@ -271,6 +275,14 @@ export default function IdentityDetails() {
timestamp={identity.created_at}
className="w-full"
/>
<ShareCta
onShareClick={() =>
setShareModalActive({
isOpen: true,
currentPath: location.pathname,
})
}
/>
</div>
)

Expand Down Expand Up @@ -351,6 +363,16 @@ export default function IdentityDetails() {
})
}
/>
<ShareModal
currentPath={location.pathname}
open={shareModalActive.isOpen}
onClose={() =>
setShareModalActive({
...shareModalActive,
isOpen: false,
})
}
/>
</TwoPanelLayout>
)
}
Expand Down
28 changes: 27 additions & 1 deletion apps/portal/app/routes/app+/list+/$id.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,15 @@ import AddIdentitiesListModal from '@components/lists/add-identities-list-modal'
import { ListIdentityDisplayCard } from '@components/lists/list-identity-display-card'
import NavigationButton from '@components/navigation-link'
import ImageModal from '@components/profile/image-modal'
import ShareCta from '@components/share-cta'
import ShareModal from '@components/share-modal'
import { useGoBack } from '@lib/hooks/useGoBack'
import { useLiveLoader } from '@lib/hooks/useLiveLoader'
import { addIdentitiesListModalAtom, imageModalAtom } from '@lib/state/store'
import {
addIdentitiesListModalAtom,
imageModalAtom,
shareModalAtom,
} from '@lib/state/store'
import logger from '@lib/utils/logger'
import { invariant } from '@lib/utils/misc'
import { json, LoaderFunctionArgs } from '@remix-run/node'
Expand Down Expand Up @@ -78,6 +84,8 @@ export default function ListDetails() {
const [addIdentitiesListModalActive, setAddIdentitiesListModalActive] =
useAtom(addIdentitiesListModalAtom)
const [imageModalActive, setImageModalActive] = useAtom(imageModalAtom)
const [shareModalActive, setShareModalActive] = useAtom(shareModalAtom)

const navigate = useNavigate()
const handleGoBack = useGoBack({ fallbackRoute: PATHS.EXPLORE_LISTS })

Expand Down Expand Up @@ -141,6 +149,14 @@ export default function ListDetails() {
>
View Identity <Icon name={'arrow-up-right'} className="h-3 w-3" />{' '}
</Button>
<ShareCta
onShareClick={() =>
setShareModalActive({
isOpen: true,
currentPath: location.pathname,
})
}
/>
</div>
)

Expand Down Expand Up @@ -171,6 +187,16 @@ export default function ListDetails() {
}
/>
)}
<ShareModal
currentPath={location.pathname}
open={shareModalActive.isOpen}
onClose={() =>
setShareModalActive({
...shareModalActive,
isOpen: false,
})
}
/>
</>
)
}
Expand Down
Loading
Loading