kodadot/nft-gallery

View on GitHub
services/profile.ts

Summary

Maintainability
C
1 day
Test Coverage
import type { FetchError } from 'ofetch'
import { $fetch } from 'ofetch'
import { isEthereumAddress } from '@polkadot/util-crypto'
import { isProduction } from '@/utils/env'

const BASE_URL = isProduction
  ? 'https://profile.kodadot.workers.dev/'
  : 'https://profile-beta.kodadot.workers.dev/'

const api = $fetch.create({
  baseURL: BASE_URL,
})

// Types for API request and response objects
export type Profile = {
  address: string
  name: string
  description: string
  image: string
  banner: string | null
  socials: SocialLink[]
}

export type Follower = {
  address: string
  name: string
  description: string
  image: string
}

export type SocialLink = {
  handle?: string
  platform: string
  link: string
}

export type ProfileResponse = {
  success: boolean
  message: string
  data?: Profile
  profileId?: string
}

export type CreateProfileRequest = {
  signature: string
  message: string
  address: string
  name: string
  description: string
  image: string
  banner: string | undefined
  socials: SocialLink[]
}

export type UpdateProfileRequest = {
  signature: string
  message: string
  address: string
  name?: string
  description?: string
  image?: string
  banner: string | null
  socials: SocialLink[]
}

export type FollowRequest = {
  initiatorAddress: string
  targetAddress: string
  signature: string
  message: string
}

const invalidSignatureErrorHandler = (error: FetchError) => {
  if (error.status === 401) {
    useWalletStore().setSignedMessage(undefined)
    throw new Error((error as FetchError)?.data?.message)
  }
}

export const toSubstrateAddress = (address: string) => {
  if (!address) {
    return ''
  }

  return isEthereumAddress(address) ? address : formatAddress(address, 42)
}

const convertToSubstrateAddress = (body: FollowRequest): FollowRequest => ({
  initiatorAddress: toSubstrateAddress(body.initiatorAddress),
  targetAddress: toSubstrateAddress(body.targetAddress),
  signature: body.signature,
  message: body.message,
})

// API methods

export const fetchProfilesByIds = (ids?: string[]) =>
  api<Profile[]>('/profiles', {
    method: 'GET',
    query: { ids: ids?.map(toSubstrateAddress).join(',') },
  })

export const fetchProfileByAddress = (address: string) =>
  api<Profile>(`/profiles/${toSubstrateAddress(address)}`, {
    method: 'GET',
  })

export const searchProfiles = (query: string, limit = 5, offset = 0) =>
  api<{ data: Profile[] }>('/profiles/search', {
    method: 'GET',
    query: { q: query, limit, offset },
  })

export const fetchFollowersOf = (
  address: string,
  options?: { limit?: number, offset?: number, exclude?: string[] },
) =>
  api<{ followers: Follower[], totalCount: number }>(
    `/follow/${toSubstrateAddress(address)}/followers`,
    {
      method: 'GET',
      query: {
        limit: options?.limit,
        offset: options?.offset,
        exclude: options?.exclude?.map(toSubstrateAddress).join(','),
      },
    },
  )

export const fetchFollowing = (
  address: string,
  options?: { limit?: number, offset?: number },
) =>
  api<{ following: Follower[], totalCount: number }>(
    `/follow/${toSubstrateAddress(address)}/following`,
    {
      method: 'GET',
      query: { limit: options?.limit, offset: options?.offset },
    },
  )

export const createProfile = async (profileData: CreateProfileRequest) => {
  try {
    const response = await api<ProfileResponse>('/profiles', {
      method: 'POST',
      body: profileData,
    })
    return response
  }
  catch (error) {
    invalidSignatureErrorHandler(error as FetchError)
    throw new Error(
      `[PROFILE::CREATE] ERROR: ${(error as FetchError)?.data?.error?.issues[0]?.message}`,
    )
  }
}

export const updateProfile = async (updates: UpdateProfileRequest) => {
  try {
    const response = await api<ProfileResponse>(
      `/profiles/${updates.address}`,
      {
        method: 'PATCH',
        body: updates,
      },
    )
    return response
  }
  catch (error) {
    invalidSignatureErrorHandler(error as FetchError)
    throw new Error(
      `[PROFILE::UPDATE] ERROR: ${(error as FetchError)?.data?.error?.issues[0]?.message}`,
    )
  }
}

type DeleteProfile = {
  message: string
  signature: string
  address: string
}

export const deleteProfile = async ({
  address,
  message,
  signature,
}: DeleteProfile) => {
  try {
    const response = await api<ProfileResponse>(`/profiles/${address}`, {
      method: 'DELETE',
      body: {
        message,
        signature,
        address,
      },
    })
    return response
  }
  catch (error) {
    throw new Error(
      `[PROFILE::DELETE] ERROR: ${(error as FetchError)?.data?.error?.issues[0]?.message}`,
    )
  }
}

export const follow = async (followRequest: FollowRequest) => {
  try {
    const response = await api<ProfileResponse>('/follow', {
      method: 'POST',
      body: convertToSubstrateAddress(followRequest),
    })
    return response
  }
  catch (error) {
    invalidSignatureErrorHandler(error as FetchError)

    throw new Error(`[PROFILE::FOLLOW] ERROR: ${(error as FetchError).data}`)
  }
}

export const unfollow = async (unFollowRequest: FollowRequest) => {
  try {
    const response = await api<ProfileResponse>('/follow', {
      method: 'DELETE',
      body: convertToSubstrateAddress(unFollowRequest),
    })
    return response
  }
  catch (error) {
    invalidSignatureErrorHandler(error as FetchError)

    throw new Error(`[PROFILE::UNFOLLOW] ERROR: ${(error as FetchError).data}`)
  }
}

export const isFollowing = async (
  follower: string,
  target: string,
): Promise<boolean> => {
  try {
    const response = await api<{ isFollowing: boolean }>(
      `/follow/${toSubstrateAddress(follower)}/follows/${toSubstrateAddress(target)}`,
      {
        method: 'GET',
      },
    )
    return response.isFollowing
  }
  catch (error) {
    throw new Error(
      `[PROFILE::IS_FOLLOWING] ERROR: ${(error as FetchError).data}`,
    )
  }
}

type UploadImage = {
  file: File
  type: string
  address: string
  signature: string
  message: string
}

export const uploadImage = async ({
  file,
  type,
  address,
  signature,
  message,
}: UploadImage) => {
  try {
    address = toSubstrateAddress(address)

    const form = new FormData()
    form.append('file', file)
    form.append('address', address)
    form.append('type', type)
    form.append('signature', signature)
    form.append('message', message)

    const response = await api<{ url: string }>(`/profiles/${address}/image`, {
      method: 'POST',
      body: form,
    })

    return response
  }
  catch (error) {
    throw new Error(
      `[PROFILE::UPLOAD_IMAGE] ERROR: ${(error as FetchError).data}`,
    )
  }
}