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

Tick import error #969

Merged
merged 6 commits into from
Aug 31, 2023
Merged
Show file tree
Hide file tree
Changes from 5 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
2 changes: 1 addition & 1 deletion src/components/edit/RecentChangeHistory.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ const UpdatedFields = ({ fields, doc }: UpdatedFieldsProps): JSX.Element | null

// double access - doc[parent][child]
if (field.includes('.')) {
var [parent, child] = field.split('.')
let [parent, child] = field.split('.')
if (parent === 'content' && doc.__typename === DocumentTypeName.Area) {
parent = 'areaContent' // I had to alias this in the query bc of the overlap with ClimbType
}
Expand Down
11 changes: 11 additions & 0 deletions src/components/ui/Spinner.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
const Spinner: React.FC = (): JSX.Element => (
<div role='status'>
<svg aria-hidden='true' className='w-6 h-6 m-2 text-gray-200 animate-spin dark:text-gray-600 fill-blue-600' viewBox='0 0 100 101' fill='none' xmlns='http://www.w3.org/2000/svg'>
<path d='M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z' fill='currentColor' />
<path d='M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z' fill='currentFill' />
</svg>
<span className='sr-only'>Loading...</span>
</div>
)

export default Spinner
14 changes: 11 additions & 3 deletions src/components/ui/micro/AlertDialogue.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,8 @@ interface LeanAlertProps {
description?: ReactNode
children?: ReactNode
className?: string
stackChildren?: boolean
onEscapeKeyDown?: () => void
}
/**
* A reusable popup alert
Expand All @@ -150,12 +152,18 @@ interface LeanAlertProps {
* @param cancelAction A button of type `AlertDialogPrimitive.Action` that closes the alert on click. You can register an `onClick()` to perform some action.
* @param noncancelAction Any kind of React component/button that doesn't close the alert on click. Use this if you want to perform an action on click and keep the alert open.
*/
export const LeanAlert = ({ icon = null, title = null, description = null, children = DefaultOkButton, closeOnEsc = true, className = '' }: LeanAlertProps): JSX.Element => {
export const LeanAlert = ({ icon = null, title = null, description = null, children = DefaultOkButton, closeOnEsc = true, onEscapeKeyDown = () => {}, className = '', stackChildren = false }: LeanAlertProps): JSX.Element => {
return (
<AlertDialogPrimitive.Root defaultOpen>
<AlertDialogPrimitive.Overlay className='fixed inset-0 bg-black/60 z-50' />
<AlertDialogPrimitive.Content
onEscapeKeyDown={e => !closeOnEsc && e.preventDefault()}
onEscapeKeyDown={e => {
if (!closeOnEsc) {
e.preventDefault()
} else {
onEscapeKeyDown()
}
}}
className='z-50 fixed h-screen inset-0 mx-auto flex items-center justify-center px-2 lg:px-0 text-center overflow-y-auto max-w-xs md:max-w-md lg:max-w-lg'
>
<div className={`p-4 rounded-box bg-base-100 w-full ${className}`}>
Expand All @@ -164,7 +172,7 @@ export const LeanAlert = ({ icon = null, title = null, description = null, child
{title}
</AlertDialogPrimitive.Title>
<AlertDialogPrimitive.Description className='my-8 text-inherit'>{description}</AlertDialogPrimitive.Description>
<div className='flex items-center justify-center gap-x-6'>
<div className={stackChildren ? 'flex-col items-center justify-center gap-x-6' : 'flex items-center justify-center gap-x-6'}>
{children}
</div>
</div>
Expand Down
276 changes: 127 additions & 149 deletions src/components/users/ImportFromMtnProj.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Fragment, useEffect, useState } from 'react'
import { useState } from 'react'
import { useRouter } from 'next/router'
import { Transition } from '@headlessui/react'
import { FolderArrowDownIcon, XMarkIcon } from '@heroicons/react/24/outline'
import * as AlertDialogPrimitive from '@radix-ui/react-alert-dialog'
import { FolderArrowDownIcon } from '@heroicons/react/24/outline'
import { useMutation } from '@apollo/client'
import { signIn, useSession } from 'next-auth/react'
import { toast } from 'react-toastify'
Expand All @@ -10,23 +10,22 @@ import clx from 'classnames'
import { graphqlClient } from '../../js/graphql/Client'
import { MUTATION_IMPORT_TICKS } from '../../js/graphql/gql/fragments'
import { INPUT_DEFAULT_CSS } from '../ui/form/TextArea'
import Spinner from '../ui/Spinner'
import { LeanAlert } from '../ui/micro/AlertDialogue'

interface Props {
isButton: boolean
username: string
}
// regex pattern to validate mountain project input
const pattern = /^https:\/\/www.mountainproject.com\/user\/\d{9}\/[a-zA-Z-]*/

/**
*
* @prop isButton -- a true or false value
* @prop username -- the openbeta username of the user
*
* if the isButton prop is true, the component will be rendered as a button
* if the isButton prop is false, the component will be rendered as a modal
* @returns JSX element
*/
export function ImportFromMtnProj ({ isButton, username }: Props): JSX.Element {
export function ImportFromMtnProj ({ username }: Props): JSX.Element {
const router = useRouter()
const [mpUID, setMPUID] = useState('')
const session = useSession()
Expand All @@ -40,19 +39,35 @@ export function ImportFromMtnProj ({ isButton, username }: Props): JSX.Element {
errorPolicy: 'none'
})

// this function updates the users metadata
async function dontShowAgain (): Promise<void> {
setLoading(true)
const res = await fetch('/api/user/ticks', {
method: 'PUT',
body: ''
})
if (res.status === 200) {
setShow(false)
} else {
setErrors(['Sorry, something went wrong. Please try again later'])
async function fetchMPData (url: string, method: 'GET' | 'POST' | 'PUT' = 'GET', body?: string): Promise<any> {
try {
const headers = {
'Content-Type': 'application/json'
}
const config: RequestInit = {
method,
headers
}

if (body !== null && body !== undefined && body !== '') {
config.body = JSON.stringify(body)
}

const response = await fetch(url, config)

if (!response.ok) {
const errorData = await response.json()
throw new Error(errorData.statusText)
}

return await response.json()
} catch (error) {
if (error instanceof Error) {
console.error('Fetch error:', error.message)
throw error
}
throw new Error('An unexpected error occurred')
}
setLoading(false)
}

// this function is for when the component is rendered as a button and sends the user straight to the input form
Expand All @@ -67,26 +82,41 @@ export function ImportFromMtnProj ({ isButton, username }: Props): JSX.Element {

async function getTicks (): Promise<void> {
// get the ticks and add it to the database
setErrors([])
if (pattern.test(mpUID)) {
setLoading(true)
const res = await fetch('/api/user/ticks', {
method: 'POST',
body: JSON.stringify(mpUID)
})
if (res.status === 200) {
setShow(false)
const { ticks } = await res.json()
await addTicks({
variables: {
input: ticks
}
})

const ticksCount: number = ticks?.length ?? 0
toast.info(`${ticksCount} ticks have been imported!`)
await router.replace(`/u2/${username}`)
} else {
setErrors(['Sorry, something went wrong. Please try again later'])

try {
const response = await fetchMPData('/api/user/ticks', 'POST', JSON.stringify(mpUID))

if (response.ticks[0] !== undefined) {
await addTicks({
variables: {
input: response.ticks
}
})
// Add a delay before rerouting to the new page
const ticksCount: number = response.ticks?.length ?? 0
toast.info(
<>
{ticksCount} ticks have been imported! 🎉 <br />
Redirecting in a few seconds...`
</>
)

setTimeout(() => {
void router.replace(`/u2/${username}`)
}, 2000)
setShow(false)
} else {
setErrors(['Sorry, no ticks were found for that user. Please check your Mountain Project ID and try again.'])
toast.error('Sorry, no ticks were found for that user. Please check your Mountain Project ID and try again.')
}
} catch (error) {
toast.error('Sorry, something went wrong. Please check your network and try again.')
setErrors(['Sorry, something went wrong. Please check your network and try again.'])
} finally {
setLoading(false)
}
} else {
// handle errors
Expand All @@ -95,120 +125,68 @@ export function ImportFromMtnProj ({ isButton, username }: Props): JSX.Element {
setLoading(false)
}

useEffect(() => {
// if we aren't rendering this component as a button
// and the user is authenticated we want to show the import your ticks modal
// then we check to see if they have a ticks imported flag set
// if it is, set show to the opposite of whatever it is
// otherwise don't show the modal
if (!isButton) {
fetch('/api/user/profile')
.then(async res => await res.json())
.then((profile) => {
if (profile?.ticksImported !== null) {
setShow(profile.ticksImported !== true)
} else if (session.status === 'authenticated') {
setShow(true)
} else {
setShow(false)
}
}).catch(console.error)
}
}, [session])

// if the isButton prop is passed to this component as true, the component will be rendered as a button, otherwise it will be a modal
return (
<>
{isButton && <button onClick={straightToInput} className='btn btn-xs md:btn-sm btn-primary'>Import ticks</button>}
<div
aria-live='assertive'
className='fixed inset-0 z-10 flex items-end px-4 py-6 mt-24 pointer-events-none sm:p-6 sm:items-start'
>
<div className='w-full flex flex-col items-center space-y-4 sm:items-end'>
<Transition.Root
show={show}
as={Fragment}
enter='transform ease-out duration-300 transition'
enterFrom='translate-y-2 opacity-0 sm:translate-y-0 sm:translate-x-2'
enterTo='translate-y-0 opacity-100 sm:translate-x-0'
leave='transition ease-in duration-100'
leaveFrom='opacity-100'
leaveTo='opacity-0'
>
<div className='max-w-xl w-full bg-white shadow-lg rounded-lg pointer-events-auto ring-1 ring-black ring-opacity-5 overflow-hidden'>
<div className='p-4'>
<div className='flex items-start'>
<div className='flex-shrink-0'>
<FolderArrowDownIcon className='h-6 w-6 text-gray-400' aria-hidden='true' />
</div>
<div className='ml-3 w-0 flex-1 pt-0.5'>
{(errors != null) && errors.length > 0 && errors.map((err, i) => <p className='mt-2 text-ob-primary' key={i}>{err}</p>)}
<p className='text-sm font-medium text-gray-900'>{showInput ? 'Input your Mountain Project profile link' : 'Import your ticks from Mountain Project'}</p>
{!showInput &&
<p className='mt-1 text-sm text-gray-500'>
Don't lose your progress, bring it over to Open Beta.
</p>}
{showInput &&
<div>
<div className='mt-1 relative rounded-md shadow-sm'>
<input
type='text'
name='website'
id='website'
value={mpUID}
onChange={(e) => setMPUID(e.target.value)}
className={clx(INPUT_DEFAULT_CSS, 'w-full')}
placeholder='https://www.mountainproject.com/user/123456789/username'
/>
</div>
</div>}
<div className='mt-3 flex space-x-7'>
{!showInput &&
<button
type='button'
onClick={() => setShowInput(true)}
className='text-center p-2 border-2 rounded-xl border-ob-primary transition
text-ob-primary hover:bg-ob-primary hover:ring hover:ring-ob-primary ring-offset-2
hover:text-white w-32 font-bold'
>
{loading ? 'Working...' : 'Show me how'}
</button>}
{showInput &&
<button
type='button'
onClick={getTicks}
className='btn btn-primary'
>
Get my ticks!
</button>}
{!isButton &&
<button
type='button'
onClick={dontShowAgain}
className='bg-white rounded-md text-sm font-medium text-gray-700 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500'
>
{loading ? 'Working...' : "Don't show again"}
</button>}
</div>
</div>
<div className='ml-4 flex-shrink-0 flex'>
<button
type='button'
className='bg-white rounded-md inline-flex text-gray-400 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500'
onClick={() => {
setShow(false)
}}
>
<span className='sr-only'>Close</span>
<XMarkIcon className='h-5 w-5' aria-hidden='true' />
</button>
</div>
</div>
</div>
<button onClick={straightToInput} className='btn btn-xs md:btn-sm btn-primary'>Import ticks</button>

{show && (
<LeanAlert
icon={<FolderArrowDownIcon className='h-6 w-6 text-gray-400' aria-hidden='true' />}
title={showInput ? 'Input your Mountain Project profile link' : 'Import your ticks from Mountain Project'}
description={!showInput ? "Don't lose your progress, bring it over to Open Beta." : null}
closeOnEsc
stackChildren
>
{(errors != null) && errors.length > 0 && errors.map((err, i) => <p className='mt-2 text-ob-primary' key={i}>{err}</p>)}

{showInput && (
<div className='mt-1 relative rounded-md shadow-sm'>
<input
type='text'
name='website'
id='website'
value={mpUID}
onChange={(e) => setMPUID(e.target.value)}
className={clx(INPUT_DEFAULT_CSS, 'w-full')}
placeholder='https://www.mountainproject.com/user/123456789/username'
/>
</div>
</Transition.Root>
</div>
</div>
)}

<div className='mt-3 flex space-x-7 justify-center'>
{!showInput && (
<button
type='button'
onClick={() => setShowInput(true)}
className='text-center p-2 border-2 rounded-xl border-ob-primary transition
text-ob-primary hover:bg-ob-primary hover:ring hover:ring-ob-primary ring-offset-2
hover:text-white w-32 font-bold'
>
{loading ? 'Working...' : 'Show me how'}
</button>
)}

{showInput && (
<button
type='button'
onClick={getTicks}
className='btn btn-primary'
>
{loading ? <Spinner /> : 'Get my ticks!'}
</button>
)}
Copy link
Contributor

@vnugent vnugent Aug 27, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd disable the button if loading is in progress

 <button
   disabled={loading}
   ... 


<AlertDialogPrimitive.Cancel
asChild onClick={() => {
setShow(false)
setErrors([])
}}
>
<button className='Button mauve'>Cancel</button>
</AlertDialogPrimitive.Cancel>
</div>
</LeanAlert>
)}
</>
)
}
Expand Down
2 changes: 1 addition & 1 deletion src/components/users/PublicProfile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ export default function PublicProfile ({ userProfile }: PublicProfileProps): JSX
<div className='btn btn-outline btn-xs md:btn-sm'> View ticks</div>
</a>
</Link>}
{username != null && isAuthorized && <ImportFromMtnProj isButton username={username} />}
{username != null && isAuthorized && <ImportFromMtnProj username={username} />}
{userProfile != null && <EditProfileButton userUuid={userProfile?.userUuid} />}
{userProfile != null && <APIKeyCopy userUuid={userProfile.userUuid} />}
</div>
Expand Down
Loading
Loading