feat(image): add controller, media services and error handling
This commit is contained in:
parent
e3aab5a5dc
commit
0b2d488bbe
@ -1,159 +0,0 @@
|
||||
import React from 'react'
|
||||
import '../styles/styles.css'
|
||||
|
||||
interface InputFieldProps {
|
||||
label: string | React.ReactElement
|
||||
description?: string
|
||||
type?: 'text' | 'textarea'
|
||||
placeholder: string
|
||||
name: string
|
||||
inputMode?: 'url'
|
||||
value: string
|
||||
error?: string
|
||||
onChange: (name: string, value: string) => void
|
||||
}
|
||||
|
||||
export const InputField = React.memo(
|
||||
({
|
||||
label,
|
||||
description,
|
||||
type = 'text',
|
||||
placeholder,
|
||||
name,
|
||||
inputMode,
|
||||
value,
|
||||
error,
|
||||
onChange
|
||||
}: InputFieldProps) => {
|
||||
const handleChange = (
|
||||
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
|
||||
) => {
|
||||
onChange(name, e.target.value)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='inputLabelWrapperMain'>
|
||||
<label className='form-label labelMain'>{label}</label>
|
||||
{description && <p className='labelDescriptionMain'>{description}</p>}
|
||||
{type === 'textarea' ? (
|
||||
<textarea
|
||||
className='inputMain'
|
||||
placeholder={placeholder}
|
||||
name={name}
|
||||
value={value}
|
||||
onChange={handleChange}
|
||||
></textarea>
|
||||
) : (
|
||||
<input
|
||||
type={type}
|
||||
className='inputMain'
|
||||
placeholder={placeholder}
|
||||
name={name}
|
||||
inputMode={inputMode}
|
||||
value={value}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
)}
|
||||
{error && <InputError message={error} />}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
type InputErrorProps = {
|
||||
message: string
|
||||
}
|
||||
|
||||
export const InputError = ({ message }: InputErrorProps) => {
|
||||
if (!message) return null
|
||||
|
||||
return (
|
||||
<div className='errorMain'>
|
||||
<div className='errorMainColor'></div>
|
||||
<p className='errorMainText'>{message}</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
interface CheckboxFieldProps {
|
||||
label: string
|
||||
name: string
|
||||
isChecked: boolean
|
||||
handleChange: (e: React.ChangeEvent<HTMLInputElement>) => void
|
||||
type?: 'default' | 'stylized'
|
||||
}
|
||||
|
||||
export const CheckboxField = React.memo(
|
||||
({
|
||||
label,
|
||||
name,
|
||||
isChecked,
|
||||
handleChange,
|
||||
type = 'default'
|
||||
}: CheckboxFieldProps) => (
|
||||
<div
|
||||
className={`inputLabelWrapperMain inputLabelWrapperMainAlt${
|
||||
type === 'stylized' ? ` inputLabelWrapperMainAltStylized` : ''
|
||||
}`}
|
||||
>
|
||||
<label htmlFor={name} className='form-label labelMain'>
|
||||
{label}
|
||||
</label>
|
||||
<input
|
||||
id={name}
|
||||
type='checkbox'
|
||||
className='CheckboxMain'
|
||||
name={name}
|
||||
checked={isChecked}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
)
|
||||
|
||||
interface InputFieldUncontrolledProps extends React.ComponentProps<'input'> {
|
||||
label: string | React.ReactElement
|
||||
description?: string
|
||||
error?: string
|
||||
}
|
||||
/**
|
||||
* Uncontrolled input component with design classes, label, description and error support
|
||||
*
|
||||
* Extends {@link React.ComponentProps<'input'> React.ComponentProps<'input'>}
|
||||
* @param label
|
||||
* @param description
|
||||
* @param error
|
||||
*
|
||||
* @see {@link React.ComponentProps<'input'>}
|
||||
*/
|
||||
export const InputFieldUncontrolled = ({
|
||||
label,
|
||||
description,
|
||||
error,
|
||||
...rest
|
||||
}: InputFieldUncontrolledProps) => (
|
||||
<div className='inputLabelWrapperMain'>
|
||||
<label htmlFor={rest.id} className='form-label labelMain'>
|
||||
{label}
|
||||
</label>
|
||||
{description && <p className='labelDescriptionMain'>{description}</p>}
|
||||
<input className='inputMain' {...rest} />
|
||||
{error && <InputError message={error} />}
|
||||
</div>
|
||||
)
|
||||
|
||||
interface CheckboxFieldUncontrolledProps extends React.ComponentProps<'input'> {
|
||||
label: string
|
||||
}
|
||||
|
||||
export const CheckboxFieldUncontrolled = ({
|
||||
label,
|
||||
...rest
|
||||
}: CheckboxFieldUncontrolledProps) => (
|
||||
<div className='inputLabelWrapperMain inputLabelWrapperMainAlt inputLabelWrapperMainAltStylized'>
|
||||
<label htmlFor={rest.id} className='form-label labelMain'>
|
||||
{label}
|
||||
</label>
|
||||
<input type='checkbox' className='CheckboxMain' {...rest} />
|
||||
</div>
|
||||
)
|
299
src/components/Inputs/index.tsx
Normal file
299
src/components/Inputs/index.tsx
Normal file
@ -0,0 +1,299 @@
|
||||
import React, { useCallback, useMemo, useState } from 'react'
|
||||
import { useDropzone } from 'react-dropzone'
|
||||
import {
|
||||
ImageController,
|
||||
MEDIA_DROPZONE_OPTIONS,
|
||||
MEDIA_OPTIONS,
|
||||
MediaOption
|
||||
} from '../../controllers'
|
||||
import '../../styles/styles.css'
|
||||
import { errorFeedback } from '../../types'
|
||||
import { InputError } from './Error'
|
||||
import { MediaInputPopover } from './MediaInputPopover'
|
||||
|
||||
interface InputFieldProps {
|
||||
label: string | React.ReactElement
|
||||
description?: string
|
||||
type?: 'text' | 'textarea'
|
||||
placeholder: string
|
||||
name: string
|
||||
inputMode?: 'url'
|
||||
value: string
|
||||
error?: string
|
||||
onChange: (name: string, value: string) => void
|
||||
}
|
||||
|
||||
export const InputField = React.memo(
|
||||
({
|
||||
label,
|
||||
description,
|
||||
type = 'text',
|
||||
placeholder,
|
||||
name,
|
||||
inputMode,
|
||||
value,
|
||||
error,
|
||||
onChange
|
||||
}: InputFieldProps) => {
|
||||
const handleChange = (
|
||||
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
|
||||
) => {
|
||||
onChange(name, e.target.value)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='inputLabelWrapperMain'>
|
||||
<label className='form-label labelMain'>{label}</label>
|
||||
{description && <p className='labelDescriptionMain'>{description}</p>}
|
||||
{type === 'textarea' ? (
|
||||
<textarea
|
||||
className='inputMain'
|
||||
placeholder={placeholder}
|
||||
name={name}
|
||||
value={value}
|
||||
onChange={handleChange}
|
||||
></textarea>
|
||||
) : (
|
||||
<input
|
||||
type={type}
|
||||
className='inputMain'
|
||||
placeholder={placeholder}
|
||||
name={name}
|
||||
inputMode={inputMode}
|
||||
value={value}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
)}
|
||||
{error && <InputError message={error} />}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
interface CheckboxFieldProps {
|
||||
label: string
|
||||
name: string
|
||||
isChecked: boolean
|
||||
handleChange: (e: React.ChangeEvent<HTMLInputElement>) => void
|
||||
type?: 'default' | 'stylized'
|
||||
}
|
||||
|
||||
export const CheckboxField = React.memo(
|
||||
({
|
||||
label,
|
||||
name,
|
||||
isChecked,
|
||||
handleChange,
|
||||
type = 'default'
|
||||
}: CheckboxFieldProps) => (
|
||||
<div
|
||||
className={`inputLabelWrapperMain inputLabelWrapperMainAlt${
|
||||
type === 'stylized' ? ` inputLabelWrapperMainAltStylized` : ''
|
||||
}`}
|
||||
>
|
||||
<label htmlFor={name} className='form-label labelMain'>
|
||||
{label}
|
||||
</label>
|
||||
<input
|
||||
id={name}
|
||||
type='checkbox'
|
||||
className='CheckboxMain'
|
||||
name={name}
|
||||
checked={isChecked}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
)
|
||||
|
||||
interface InputFieldUncontrolledProps extends React.ComponentProps<'input'> {
|
||||
label: string | React.ReactElement
|
||||
description?: string
|
||||
error?: string
|
||||
}
|
||||
/**
|
||||
* Uncontrolled input component with design classes, label, description and error support
|
||||
*
|
||||
* Extends {@link React.ComponentProps<'input'> React.ComponentProps<'input'>}
|
||||
* @param label
|
||||
* @param description
|
||||
* @param error
|
||||
*
|
||||
* @see {@link React.ComponentProps<'input'>}
|
||||
*/
|
||||
export const InputFieldUncontrolled = ({
|
||||
label,
|
||||
description,
|
||||
error,
|
||||
...rest
|
||||
}: InputFieldUncontrolledProps) => (
|
||||
<div className='inputLabelWrapperMain'>
|
||||
<label htmlFor={rest.id} className='form-label labelMain'>
|
||||
{label}
|
||||
</label>
|
||||
{description && <p className='labelDescriptionMain'>{description}</p>}
|
||||
<input className='inputMain' {...rest} />
|
||||
{error && <InputError message={error} />}
|
||||
</div>
|
||||
)
|
||||
|
||||
interface CheckboxFieldUncontrolledProps extends React.ComponentProps<'input'> {
|
||||
label: string
|
||||
}
|
||||
|
||||
export const CheckboxFieldUncontrolled = ({
|
||||
label,
|
||||
...rest
|
||||
}: CheckboxFieldUncontrolledProps) => (
|
||||
<div className='inputLabelWrapperMain inputLabelWrapperMainAlt inputLabelWrapperMainAltStylized'>
|
||||
<label htmlFor={rest.id} className='form-label labelMain'>
|
||||
{label}
|
||||
</label>
|
||||
<input type='checkbox' className='CheckboxMain' {...rest} />
|
||||
</div>
|
||||
)
|
||||
|
||||
interface InputFieldWithImageUpload {
|
||||
label: string | React.ReactElement
|
||||
description?: string
|
||||
multiple?: boolean | undefined
|
||||
placeholder: string
|
||||
name: string
|
||||
inputMode?: 'url'
|
||||
value: string
|
||||
error?: string
|
||||
onChange: (name: string, value: string) => void
|
||||
}
|
||||
export const InputFieldWithImageUpload = React.memo(
|
||||
({
|
||||
label,
|
||||
description,
|
||||
multiple = false,
|
||||
placeholder,
|
||||
name,
|
||||
inputMode,
|
||||
value,
|
||||
error,
|
||||
onChange
|
||||
}: InputFieldWithImageUpload) => {
|
||||
const [mediaOption, setMediaOption] = useState<MediaOption>(
|
||||
MEDIA_OPTIONS[0]
|
||||
)
|
||||
const handleOptionChange = useCallback(
|
||||
(mo: MediaOption) => () => {
|
||||
setMediaOption(mo)
|
||||
},
|
||||
[]
|
||||
)
|
||||
const handleChange = useCallback(
|
||||
(e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
|
||||
onChange(name, e.target.value)
|
||||
},
|
||||
[name, onChange]
|
||||
)
|
||||
const handleUpload = useCallback(
|
||||
async (acceptedFiles: File[]) => {
|
||||
try {
|
||||
const imageController = new ImageController(mediaOption)
|
||||
const url = await imageController.post(acceptedFiles[0])
|
||||
onChange(name, url)
|
||||
} catch (error) {
|
||||
errorFeedback(error)
|
||||
}
|
||||
},
|
||||
[mediaOption, name, onChange]
|
||||
)
|
||||
const {
|
||||
getRootProps,
|
||||
getInputProps,
|
||||
isDragActive,
|
||||
acceptedFiles,
|
||||
isFileDialogActive,
|
||||
isDragAccept,
|
||||
isDragReject,
|
||||
fileRejections
|
||||
} = useDropzone({
|
||||
...MEDIA_DROPZONE_OPTIONS,
|
||||
onDrop: handleUpload,
|
||||
multiple: multiple
|
||||
})
|
||||
|
||||
const dropzoneLabel = useMemo(
|
||||
() =>
|
||||
isFileDialogActive
|
||||
? 'Select files in dialog'
|
||||
: isDragActive
|
||||
? isDragAccept
|
||||
? 'Drop the files here...'
|
||||
: isDragReject
|
||||
? 'Drop the files here (one more more unsupported types)...'
|
||||
: 'TODO'
|
||||
: 'Click or drag files here',
|
||||
[isDragAccept, isDragActive, isDragReject, isFileDialogActive]
|
||||
)
|
||||
|
||||
return (
|
||||
<div className='inputLabelWrapperMain'>
|
||||
<label className='form-label labelMain'>{label}</label>
|
||||
{typeof description !== 'undefined' && (
|
||||
<p className='labelDescriptionMain'>{description}</p>
|
||||
)}
|
||||
|
||||
<div aria-label='upload featuredImageUrl' className='uploadBoxMain'>
|
||||
<MediaInputPopover
|
||||
name={name}
|
||||
acceptedFiles={acceptedFiles}
|
||||
fileRejections={fileRejections}
|
||||
/>
|
||||
<div
|
||||
className='uploadBoxMainInside'
|
||||
{...getRootProps()}
|
||||
tabIndex={-1}
|
||||
>
|
||||
<input id='featuredImageUrl-upload' {...getInputProps()} />
|
||||
|
||||
<span>{dropzoneLabel}</span>
|
||||
<div
|
||||
className='FiltersMainElement'
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div className='dropdown dropdownMain'>
|
||||
<button
|
||||
className='btn dropdown-toggle btnMain btnMainDropdown'
|
||||
aria-expanded='false'
|
||||
data-bs-toggle='dropdown'
|
||||
type='button'
|
||||
>
|
||||
Image Host: {mediaOption.name}
|
||||
</button>
|
||||
<div className='dropdown-menu dropdownMainMenu'>
|
||||
{MEDIA_OPTIONS.map((mo) => {
|
||||
return (
|
||||
<div
|
||||
key={mo.host}
|
||||
onClick={handleOptionChange(mo)}
|
||||
className='dropdown-item dropdownMainMenuItem'
|
||||
>
|
||||
{mo.name}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<input
|
||||
type='text'
|
||||
className='inputMain'
|
||||
placeholder={placeholder}
|
||||
name={name}
|
||||
inputMode={inputMode}
|
||||
value={value}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
{error && <InputError message={error} />}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
)
|
74
src/controllers/image/index.ts
Normal file
74
src/controllers/image/index.ts
Normal file
@ -0,0 +1,74 @@
|
||||
import { DropzoneOptions } from 'react-dropzone'
|
||||
import { NostrCheckServer } from './nostrcheck-server'
|
||||
import { BaseError } from 'types'
|
||||
|
||||
export interface MediaOperations {
|
||||
post: (file: File) => Promise<string>
|
||||
}
|
||||
export type MediaStrategy = Omit<MediaOperations, 'auth'>
|
||||
|
||||
export interface MediaOption {
|
||||
name: string
|
||||
host: string
|
||||
type: 'nostrcheck-server' | 'route96'
|
||||
}
|
||||
|
||||
// nostr.build based dropzone options
|
||||
export const MEDIA_DROPZONE_OPTIONS: DropzoneOptions = {
|
||||
maxSize: 7000000,
|
||||
accept: {
|
||||
'image/*': ['.jpeg', '.png', '.jpg', '.gif', '.webp']
|
||||
}
|
||||
}
|
||||
|
||||
export const MEDIA_OPTIONS: MediaOption[] = [
|
||||
// {
|
||||
// name: 'nostr.build',
|
||||
// host: 'https://nostr.build/',
|
||||
// type: 'nostrcheck-server'
|
||||
// },
|
||||
{
|
||||
name: 'nostrcheck.me',
|
||||
host: 'https://nostrcheck.me/',
|
||||
type: 'nostrcheck-server'
|
||||
},
|
||||
{
|
||||
name: 'nostpic.com',
|
||||
host: 'https://nostpic.com/',
|
||||
type: 'nostrcheck-server'
|
||||
},
|
||||
{
|
||||
name: 'files.sovbit.host',
|
||||
host: 'https://files.sovbit.host/',
|
||||
type: 'nostrcheck-server'
|
||||
}
|
||||
// {
|
||||
// name: 'void.cat',
|
||||
// host: 'https://void.cat/',
|
||||
// type: 'route96'
|
||||
// }
|
||||
]
|
||||
|
||||
enum ImageErrorType {
|
||||
'TYPE_MISSING' = 'Media Option must include a type.'
|
||||
}
|
||||
|
||||
export class ImageController implements MediaStrategy {
|
||||
post: (file: File) => Promise<string>
|
||||
|
||||
constructor(mediaOption: MediaOption) {
|
||||
let strategy: MediaStrategy
|
||||
switch (mediaOption.type) {
|
||||
case 'nostrcheck-server':
|
||||
strategy = new NostrCheckServer(mediaOption.host)
|
||||
this.post = strategy.post
|
||||
break
|
||||
|
||||
case 'route96':
|
||||
throw new Error('Not implemented.')
|
||||
|
||||
default:
|
||||
throw new BaseError(ImageErrorType.TYPE_MISSING)
|
||||
}
|
||||
}
|
||||
}
|
162
src/controllers/image/nostrcheck-server.ts
Normal file
162
src/controllers/image/nostrcheck-server.ts
Normal file
@ -0,0 +1,162 @@
|
||||
import axios, { isAxiosError } from 'axios'
|
||||
import { NostrEvent, NDKKind } from '@nostr-dev-kit/ndk'
|
||||
import { type MediaOperations } from '.'
|
||||
import { store } from 'store'
|
||||
import { now } from 'utils'
|
||||
import { BaseError, handleError } from 'types'
|
||||
|
||||
// https://github.com/quentintaranpino/nostrcheck-server/blob/main/DOCS.md#media-post
|
||||
// Response object (other fields omitted for brevity)
|
||||
// {
|
||||
// "status": "success",
|
||||
// "nip94_event": {
|
||||
// "tags": [
|
||||
// [
|
||||
// "url",
|
||||
// "https://nostrcheck.me/media/62c76eb094369d938f5895442eef7f53ebbf019f69707d64e77d4d182b609309/c35277dbcedebb0e3b80361762c8baadb66dcdfb6396949e50630159a472c3b2.webp"
|
||||
// ],
|
||||
// ],
|
||||
// }
|
||||
// }
|
||||
|
||||
interface Response {
|
||||
status: 'success' | string
|
||||
nip94_event?: {
|
||||
tags?: string[][]
|
||||
}
|
||||
}
|
||||
|
||||
enum HandledErrorType {
|
||||
'PUBKEY' = 'Failed to get public key.',
|
||||
'SIGN' = 'Failed to sign the event.',
|
||||
'AXIOS_REQ' = 'Image upload failed. Try another host from the dropdown.',
|
||||
'AXIOS_RES' = 'Image upload failed. Reason: ',
|
||||
'AXIOS_ERR' = 'Image upload failed.',
|
||||
'NOSTR_CHECK_NO_SUCCESS' = 'Image upload was unsuccesfull.',
|
||||
'NOSTR_CHECK_BAD_EVENT' = 'Image upload failed. Please try again.'
|
||||
}
|
||||
|
||||
export class NostrCheckServer implements MediaOperations {
|
||||
#media = 'api/v2/media'
|
||||
#url: string
|
||||
|
||||
constructor(url: string) {
|
||||
this.#url = url[url.length - 1] === '/' ? url : `${url}/`
|
||||
}
|
||||
|
||||
post = async (file: File) => {
|
||||
const url = `${this.#url}${this.#media}`
|
||||
const auth = await this.auth()
|
||||
try {
|
||||
const response = await axios.postForm<Response>(
|
||||
url,
|
||||
{
|
||||
uploadType: 'media',
|
||||
file: file
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
Authorization: 'Nostr ' + auth,
|
||||
'Content-Type': 'multipart/form-data'
|
||||
},
|
||||
responseType: 'json'
|
||||
}
|
||||
)
|
||||
|
||||
if (response.data.status !== 'success') {
|
||||
throw new BaseError(HandledErrorType.NOSTR_CHECK_NO_SUCCESS, {
|
||||
context: { ...response.data }
|
||||
})
|
||||
}
|
||||
|
||||
if (
|
||||
response.data &&
|
||||
response.data.nip94_event &&
|
||||
response.data.nip94_event.tags &&
|
||||
response.data.nip94_event.tags.length
|
||||
) {
|
||||
// Return first 'url' tag we find on the returned nip94 event
|
||||
const imageUrl = response.data.nip94_event.tags.find(
|
||||
(item) => item[0] === 'url'
|
||||
)
|
||||
|
||||
if (imageUrl) return imageUrl[1]
|
||||
}
|
||||
|
||||
throw new BaseError(HandledErrorType.NOSTR_CHECK_BAD_EVENT, {
|
||||
context: { ...response.data }
|
||||
})
|
||||
} catch (error) {
|
||||
// Handle axios errors
|
||||
if (isAxiosError(error)) {
|
||||
if (error.request) {
|
||||
// The request was made but no response was received
|
||||
throw new BaseError(HandledErrorType.AXIOS_REQ, {
|
||||
cause: error
|
||||
})
|
||||
} else if (error.response) {
|
||||
// The request was made and the server responded with a status code
|
||||
// that falls out of the range of 2xx
|
||||
// nostrcheck-server can return different results, including message or description
|
||||
const data = error.response.data
|
||||
let message = error.message
|
||||
if (data) {
|
||||
message = data?.message || data?.description || error.message
|
||||
}
|
||||
throw new BaseError(HandledErrorType.AXIOS_RES + message, {
|
||||
cause: error
|
||||
})
|
||||
} else {
|
||||
// Something happened in setting up the request that triggered an Error
|
||||
throw new BaseError(HandledErrorType.AXIOS_ERR, {
|
||||
cause: error
|
||||
})
|
||||
}
|
||||
} else if (error instanceof BaseError) {
|
||||
throw error
|
||||
} else {
|
||||
throw handleError(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
auth = async () => {
|
||||
try {
|
||||
const url = `${this.#url}${this.#media}`
|
||||
|
||||
let hexPubkey: string
|
||||
const userState = store.getState().user
|
||||
if (userState.auth && userState.user?.pubkey) {
|
||||
hexPubkey = userState.user.pubkey as string
|
||||
} else {
|
||||
hexPubkey = (await window.nostr?.getPublicKey()) as string
|
||||
}
|
||||
|
||||
if (!hexPubkey) {
|
||||
throw new BaseError(HandledErrorType.PUBKEY)
|
||||
}
|
||||
|
||||
const unsignedEvent: NostrEvent = {
|
||||
content: '',
|
||||
created_at: now(),
|
||||
kind: NDKKind.HttpAuth,
|
||||
pubkey: hexPubkey,
|
||||
tags: [
|
||||
['u', url],
|
||||
['method', 'POST']
|
||||
]
|
||||
}
|
||||
|
||||
const signedEvent = await window.nostr?.signEvent(unsignedEvent)
|
||||
return btoa(JSON.stringify(signedEvent))
|
||||
} catch (error) {
|
||||
if (error instanceof BaseError) {
|
||||
throw error
|
||||
}
|
||||
|
||||
throw new BaseError(HandledErrorType.SIGN, {
|
||||
cause: handleError(error)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
7
src/controllers/image/route96.ts
Normal file
7
src/controllers/image/route96.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import { MediaOperations } from '.'
|
||||
|
||||
export class route96 implements MediaOperations {
|
||||
post = () => {
|
||||
throw new Error('route96 post Not implemented.')
|
||||
}
|
||||
}
|
@ -1 +1,2 @@
|
||||
export * from './zap'
|
||||
export * from './image'
|
||||
|
58
src/types/errors.ts
Normal file
58
src/types/errors.ts
Normal file
@ -0,0 +1,58 @@
|
||||
import { toast } from 'react-toastify'
|
||||
import { log, LogType } from 'utils'
|
||||
|
||||
export type Jsonable =
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| null
|
||||
| undefined
|
||||
| readonly Jsonable[]
|
||||
| { readonly [key: string]: Jsonable }
|
||||
| { toJSON(): Jsonable }
|
||||
|
||||
/**
|
||||
* Handle errors
|
||||
* Wraps the errors without message property and stringify to a message so we can use it later
|
||||
* @param error
|
||||
* @returns
|
||||
*/
|
||||
export function handleError(error: unknown): Error {
|
||||
if (error instanceof Error) return error
|
||||
|
||||
// No message error, wrap it and stringify
|
||||
let stringified = 'Unable to stringify the thrown value'
|
||||
try {
|
||||
stringified = JSON.stringify(error)
|
||||
} catch (error) {
|
||||
console.error(stringified, error)
|
||||
}
|
||||
|
||||
return new Error(`Stringified Error: ${stringified}`)
|
||||
}
|
||||
|
||||
export class BaseError extends Error {
|
||||
public readonly context?: Jsonable
|
||||
|
||||
constructor(
|
||||
message: string,
|
||||
options: { cause?: Error; context?: Jsonable } = {}
|
||||
) {
|
||||
const { cause, context } = options
|
||||
|
||||
super(message, { cause: cause })
|
||||
this.name = this.constructor.name
|
||||
|
||||
this.context = context
|
||||
}
|
||||
}
|
||||
|
||||
export function errorFeedback(error: unknown) {
|
||||
if (error instanceof Error) {
|
||||
toast.error(error.message)
|
||||
log(true, LogType.Error, error)
|
||||
} else {
|
||||
toast.error('Something went wrong.')
|
||||
log(true, LogType.Error, error)
|
||||
}
|
||||
}
|
@ -6,3 +6,4 @@ export * from './zap'
|
||||
export * from './blog'
|
||||
export * from './category'
|
||||
export * from './popup'
|
||||
export * from './errors'
|
||||
|
Loading…
x
Reference in New Issue
Block a user