Show dialog to accept following

This commit is contained in:
Maximilian Harz 2025-12-04 00:13:33 +01:00
parent 293001b721
commit 68fb201907
5 changed files with 124 additions and 75 deletions

View File

@ -3,7 +3,7 @@ import { MapContainer } from 'react-leaflet'
import { ContextWrapper } from '#components/AppShell/ContextWrapper'
import { useRedeemInvite } from './hooks/useRedeemInvite'
import { useStoredInviteCode } from './hooks/useStoredInviteCode'
import { UtopiaMapInner } from './UtopiaMapInner'
import type { InviteApi } from '#types/InviteApi'
@ -112,8 +112,8 @@ function UtopiaMap({
/** tile size (default 256) */
tileSize?: number
}) {
// Check for invite code in localStorage and loaded profile, redeem if possible
useRedeemInvite(inviteApi)
// Check for invite code in localStorage
useStoredInviteCode()
return (
<ContextWrapper>
<MapContainer

View File

@ -1,10 +1,13 @@
import { useAuth } from '#components/Auth/useAuth'
import { useItems } from './useItems'
import { useItems, useAddItem } from './useItems'
import { useLayers } from './useLayers'
export const useMyProfile = () => {
const items = useItems()
const { user } = useAuth()
const layers = useLayers()
const addItem = useAddItem()
// Find the user's profile item
const myProfile = items.find(
@ -16,5 +19,31 @@ export const useMyProfile = () => {
// allItemsLoaded is not reliable
const isMyProfileLoaded = isAnyUserProfileLoaded && !!user
return { myProfile, isMyProfileLoaded }
const createEmptyProfile = async () => {
if (!user) return
const userLayer = layers.find((l) => l.userProfileLayer === true)
if (!userLayer?.api?.createItem) {
throw new Error('User profile layer or create API not available')
}
const newProfile = {
id: crypto.randomUUID(),
name: user.first_name ?? 'User',
}
const result = await userLayer.api.createItem(newProfile)
// Use server response for local state update
addItem({
...result,
user_created: user,
layer: userLayer,
public_edit: false,
})
return result
}
return { myProfile, isMyProfileLoaded, createEmptyProfile }
}

View File

@ -1,35 +0,0 @@
import { useEffect, useState } from 'react'
import { useNavigate } from 'react-router-dom'
import { toast } from 'react-toastify'
import { useMyProfile } from './useMyProfile'
import type { InviteApi } from '#types/InviteApi'
import type { Item } from '#types/Item'
export const useRedeemInvite = (inviteApi: InviteApi) => {
const inviteCode = localStorage.getItem('inviteCode')
const { myProfile } = useMyProfile()
const navigate = useNavigate()
const [isRedeemingDone, setRedeemingDone] = useState(false)
useEffect(() => {
async function redeemInvite(inviteCode: string, myProfile: Item) {
const invitingProfileId = await inviteApi.redeemInvite(inviteCode, myProfile.id)
if (invitingProfileId) {
toast.success('Invite redeemed successfully!')
localStorage.removeItem('inviteCode')
navigate(`/item/${invitingProfileId}`)
} else {
toast.error('Failed to redeem invite')
}
}
if (!inviteCode || !myProfile || isRedeemingDone) return
void redeemInvite(inviteCode, myProfile)
setRedeemingDone(true)
}, [inviteApi, inviteCode, isRedeemingDone, myProfile, navigate])
}

View File

@ -0,0 +1,17 @@
import { useEffect } from 'react'
import { useNavigate } from 'react-router-dom'
import { useAuth } from '#components/Auth/useAuth'
export const useStoredInviteCode = () => {
const inviteCode = localStorage.getItem('inviteCode')
const { user } = useAuth()
const navigate = useNavigate()
useEffect(() => {
if (!inviteCode || !user) return
navigate(`/invite/${inviteCode}`)
}, [inviteCode, navigate, user])
}

View File

@ -23,7 +23,7 @@ export function InvitePage({ inviteApi, itemsApi }: Props) {
const { id } = useParams<{ id: string }>()
const navigate = useNavigate()
const { myProfile, isMyProfileLoaded } = useMyProfile()
const { myProfile, isMyProfileLoaded, createEmptyProfile } = useMyProfile()
if (!id) throw new Error('Invite ID is required')
@ -31,20 +31,42 @@ export function InvitePage({ inviteApi, itemsApi }: Props) {
const [isRedeemingDone, setRedeemingDone] = useState(false)
const [isValidationDone, setValidationDone] = useState(false)
useEffect(() => {
async function redeemInvite(id: string, myProfileId: string) {
const invitingProfileId = await inviteApi.redeemInvite(id, myProfileId)
async function redeemInvite(id: string, myProfileId: string) {
const invitingProfileId = await inviteApi.redeemInvite(id, myProfileId)
if (invitingProfileId) {
toast.success('Invite redeemed successfully!')
setRedeemingDone(true)
navigate(`/item/${invitingProfileId}`)
} else {
toast.error('Failed to redeem invite')
navigate('/')
}
if (invitingProfileId) {
toast.success('Invite redeemed successfully!')
localStorage.removeItem('inviteCode')
setRedeemingDone(true)
navigate(`/item/${invitingProfileId}`)
} else {
toast.error('Failed to redeem invite')
navigate('/')
}
}
const confirmFollowAsync = async () => {
if (!isAuthenticated) return
if (!isMyProfileLoaded || isRedeemingDone) return
const myActualProfile = myProfile ?? (await createEmptyProfile())
if (!myActualProfile) {
toast.error('Failed to create profile')
return
}
await redeemInvite(id, myActualProfile.id)
setRedeemingDone(true)
}
const confirmFollow = () => {
void confirmFollowAsync()
}
useEffect(() => {
async function validateInvite(id: string) {
const invitingProfileId = await inviteApi.validateInvite(id)
@ -72,23 +94,14 @@ export function InvitePage({ inviteApi, itemsApi }: Props) {
if (!isAuthenticationInitialized) return
if (isAuthenticated) {
if (!isMyProfileLoaded || isRedeemingDone) return
if (!myProfile) {
toast.error('Could not find your profile to redeem the invite.')
} else {
void redeemInvite(id, myProfile.id)
}
setRedeemingDone(true)
} else {
if (isValidationDone) return
if (isValidationDone) return
if (!isAuthenticated) {
// Save invite code in local storage
localStorage.setItem('inviteCode', id)
void validateInvite(id)
}
void validateInvite(id)
}, [
id,
isAuthenticated,
@ -100,6 +113,7 @@ export function InvitePage({ inviteApi, itemsApi }: Props) {
itemsApi,
isRedeemingDone,
isValidationDone,
createEmptyProfile,
])
const goToSignup = () => {
@ -110,6 +124,35 @@ export function InvitePage({ inviteApi, itemsApi }: Props) {
navigate('/login')
}
const goToStart = () => {
navigate('/')
}
if (isAuthenticated) {
return (
<MapOverlayPage backdrop className='tw:max-w-xs tw:h-fit'>
<h2 className='tw-text-2xl tw-font-semibold tw-mb-2 tw-text-center'>Confirmation</h2>
{invitingProfile ? (
<div className='tw-text-center tw-mb-4'>
<p className='tw-text-sm tw-text-gray-600'>
Do you want to follow <strong>{invitingProfile.name}</strong>?
</p>
<div className='tw-flex tw:justify-center tw:mt-4'>
<button className='tw-btn tw-btn-primary' onClick={confirmFollow}>
Yes
</button>
<button className='tw-btn tw-btn-secondary' onClick={goToStart}>
No
</button>
</div>
</div>
) : (
<p className='tw-text-center'>Validating invite...</p>
)}
</MapOverlayPage>
)
}
return (
<MapOverlayPage backdrop className='tw:max-w-xs tw:h-fit'>
<h2 className='tw:text-2xl tw:font-semibold tw:mb-2 tw:text-center'>Invitation</h2>
@ -121,17 +164,12 @@ export function InvitePage({ inviteApi, itemsApi }: Props) {
community.
</p>
<div className='tw-flex tw:justify-center tw:mt-4'>
<button
className='tw-btn tw-btn-primary'
onClick={isAuthenticated ? () => navigate('/') : goToSignup}
>
{isAuthenticated ? 'Go to Dashboard' : 'Sign Up'}
<button className='tw-btn tw-btn-primary' onClick={goToSignup}>
{'Sign Up'}
</button>
<button className='tw-btn tw-btn-secondary' onClick={goToLogin}>
Login
</button>
{!isAuthenticated && (
<button className='tw-btn tw-btn-secondary' onClick={goToLogin}>
Login
</button>
)}
</div>
</div>
) : (