kodadot/nft-gallery

View on GitHub
components/codeChecker/CodeChecker.vue

Summary

Maintainability
Test Coverage
<template>
  <div class="flex flex-wrap pb-44 gap-10 lg:gap-0">
    <div class="lg:w-1/2 flex flex-col gap-10">
      <!-- Content of the first column -->
      <div class="">
        <h1 class="title is-3 mb-4">
          {{ $t('codeChecker.title') }}
        </h1>
        <div class="w-2/3">
          {{ $t('codeChecker.description') }}
        </div>
      </div>

      <div class="py-4 px-5 border border-neutral-5">
        <h2 class="mb-3">
          {{ $t('codeChecker.resources') }}:
        </h2>
        <div class="pl-3 flex flex-col gap-3">
          <a
            v-for="item in RESOURCES_LIST"
            :key="item.title"
            v-safe-href="item.url"
            class="flex items-center w-fit"
            target="_blank"
            rel="nofollow noopener noreferrer"
          >
            <div
              class="text-k-blue hover:text-k-blue-hover flex items-center mr-2"
            >
              <NeoIcon
                icon="circle"
                pack="fas"
                class="text-[4px] mr-2"
              />
              {{ $t(item.title) }}
            </div>
            <NeoIcon
              icon="arrow-up-right"
              class="text-neutral-7 text-sm"
            />
          </a>
        </div>
      </div>

      <div class="">
        <h2 class="mb-3 title is-4">
          {{ $t('codeChecker.upload') }}
        </h2>
        <p class="mb-4">
          {{ $t('codeChecker.uploadInstructions') }}
        </p>
        <CodeCheckerFileUploader
          v-model="selectedFile"
          :file-name="fileName"
          @file-selected="onFileSelected"
          @clear="clear"
        />
      </div>

      <div class="">
        <h2 class="mb-3 title is-4">
          {{ $t('codeChecker.codeValidation') }}
        </h2>
        <p
          v-if="!selectedFile"
          class="text-neutral-7"
        >
          {{ $t('codeChecker.uploadPrompt') }}
        </p>
        <div v-else>
          <div
            v-if="errorMessage"
            class="text-red-500"
          >
            {{ $t('error') }}: {{ errorMessage }}
          </div>
          <div v-else>
            <div class="flex justify-between items-center">
              <span class="text-neutral-7">{{
                $t('codeChecker.canvasSize')
              }}</span>
              <span>{{ fileValidity.canvasSize }}</span>
            </div>
            <div class="flex justify-between items-center">
              <span class="text-neutral-7">
                {{ $t('codeChecker.artName') }}
              </span>
              <span>{{ fileValidity.title }}</span>
            </div>

            <div class="flex justify-between items-center">
              <span class="text-neutral-7">{{ $t('codeChecker.local') }} p5js</span>
              <span>{{ fileValidity.localP5jsUsed ? 'Yes' : 'No' }}</span>
            </div>
          </div>
        </div>
      </div>
      <div
        v-if="selectedFile && !errorMessage"
        class="border-t border-k-shade pt-5 flex flex-col gap-5"
      >
        <CodeCheckerTestItem
          :passed="fileValidity.validTitle"
          :description="$t('codeChecker.correctHTMLName')"
        >
          <template #modalContent>
            <CodeCheckerIssueHintCorrectHTMLName />
          </template>
        </CodeCheckerTestItem>
        <CodeCheckerTestItem
          :passed="fileValidity.kodaRendererUsed"
          :description="$t('codeChecker.usingKodaHash')"
        >
          <template #modalContent>
            <CodeCheckerIssueHintUsingKodaHash />
          </template>
        </CodeCheckerTestItem>
        <CodeCheckerTestItem
          :passed="fileValidity.kodaRendererCalledOnce"
          :description="$t('codeChecker.kodaHashCalledOnce')"
        >
          <template #modalContent>
            <CodeCheckerIssueHintKodaHashCalledOnce />
          </template>
        </CodeCheckerTestItem>
        <CodeCheckerTestItem
          :passed="fileValidity.externalResourcesNotUsed"
          :description="$t('codeChecker.notUsingExternalResources')"
        >
          <template #modalContent>
            <CodeCheckerIssueHintNotUsingExternalResources />
          </template>
        </CodeCheckerTestItem>
        <CodeCheckerTestItem
          :passed="fileValidity.usesHashParam"
          :description="$t('codeChecker.usingParamHash')"
        >
          <template #modalContent>
            <CodeCheckerIssueHintUsingParamHash />
          </template>
        </CodeCheckerTestItem>

        <CodeCheckerTestItem
          :passed="fileValidity.validKodaRenderPayload"
          :description="$t('codeChecker.validImage')"
        >
          <template #modalContent>
            <CodeCheckerIssueHintValidImage />
          </template>
        </CodeCheckerTestItem>
        <CodeCheckerTestItem
          :passed="fileValidity.consistent"
          :description="$t('codeChecker.consistentArt')"
        >
          <template #modalContent>
            <CodeCheckerIssueHintConsistentArt />
          </template>
        </CodeCheckerTestItem>
        <CodeCheckerTestItem
          :passed="fileValidity.resizerUsed"
          :description="$t('codeChecker.automaticResize')"
          optional
        >
          <template #modalContent>
            <CodeCheckerIssueHintAutomaticResize />
          </template>
        </CodeCheckerTestItem>
        <CodeCheckerTestItem
          :passed="fileValidity.renderDurationValid"
          :description="
            $t('codeChecker.variationLoadingTime')
          "
          optional
        >
          <template #modalContent>
            <CodeCheckerIssueHintVariationLoadingTime />
          </template>
        </CodeCheckerTestItem>
      </div>
    </div>

    <div
      class="w-full lg:w-1/2 flex flex-col items-center lg:mt-4 lg:items-end"
    >
      <!-- Content of the second column -->
      <CodeCheckerPreviewCard
        :selected-file="selectedFile"
        :file-name="fileName"
        :assets="assets"
        :render="Boolean(selectedFile)"
        :koda-renderer-used="fileValidity.kodaRendererUsed"
        :reload-trigger="reloadTrigger"
        :index-url="indexUrl"
        @reload="startClock"
        @hash:update="(hash) => (previewHash = hash)"
      />

      <CodeCheckerMassPreview
        v-if="selectedFile && indexContent"
        class="!mt-11"
        :assets="assets"
        :index-content="indexContent"
        @upload="(value) => (indexUrl = value)"
      />

      <div class="max-w-[490px] mt-11">
        <hr
          v-if="selectedFile"
          class="my-2 bg-k-shade2 w-full !mb-11"
        >

        <div class="flex items-center gap-5">
          <NeoIcon
            icon="shield"
            class="!block text-k-grey"
            size="large"
          />
          <p class="capitalize text-k-grey">
            {{ $t('codeChecker.confidentialCode') }}
          </p>
        </div>
      </div>
    </div>
  </div>
</template>

<script lang="ts" setup>
import { NeoIcon } from '@kodadot1/brick'
import { validate } from './validate'
import { createSandboxAssets, extractAssetsFromZip } from './utils'
import config from './codechecker.config'
import type { AssetMessage, Validity } from './types'

const RESOURCES_LIST = [
  {
    title: 'codeChecker.kodahashTemplate',
    url: 'https://github.com/vikiival/kodahash',
  },
  {
    title: 'codeChecker.learnAboutGenArt',
    url: 'https://hello.kodadot.xyz/tutorial/generative-art',
  },
  {
    title: 'codeChecker.codeChecker',
    url: 'https://hello.kodadot.xyz/tutorial/generative-art/code-checker',
  },
]

const validtyDefault: Validity = {
  canvasSize: '',
  localP5jsUsed: false,
  kodaRendererUsed: 'unknown',
  kodaRendererCalledOnce: 'unknown',
  resizerUsed: 'unknown',
  usesHashParam: 'unknown',
  validTitle: 'unknown',
  renderDurationValid: 'loading',
  title: '-',
  validKodaRenderPayload: 'loading',
  consistent: 'loading',
  externalResourcesNotUsed: 'unknown',
}

const selectedFile = ref<File | null>(null)
const assets = ref<AssetMessage[]>([])
const indexContent = ref<string>()
const indexUrl = ref<string>()
const fileName = computed(() => selectedFile.value?.name)
const fileValidity = reactive<Validity>({ ...validtyDefault })
const errorMessage = ref('')
const renderStartTime = ref(0)
const renderEndTime = ref(0)
const renderCount = ref(0)
const reloadTrigger = ref(0)
const firstImage = ref<string>()
const previewHash = ref()

const onFileSelected = async (file: File) => {
  clear()
  startClock()
  selectedFile.value = file
  const { indexFile, sketchFile, p5File, entries } = await extractAssetsFromZip(file)

  if (!indexFile) {
    errorMessage.value = `Index file not found: Please make sure that “index.html” is in the root directory`
    return
  }

  if (!sketchFile) {
    errorMessage.value = `Sketch file not found: ${config.sketchFile}`
    return
  }

  if (!p5File) {
    errorMessage.value = `p5 file not found: Please make sure that “p5.min.js” is in the root directory`
    return
  }

  const valid = validate(indexFile.content, sketchFile.content)
  if (!valid.isSuccess) {
    errorMessage.value = valid.error ?? 'Unknown error'
  }
  else {
    Object.assign(fileValidity, valid.value)
  }

  if (!fileValidity.kodaRendererUsed) {
    fileValidity.renderDurationValid = 'unknown'
    fileValidity.validKodaRenderPayload = 'unknown'
    fileValidity.consistent = 'unknown'
  }

  indexContent.value = indexFile.content
  assets.value = await createSandboxAssets(indexFile, entries)
}

const clear = () => {
  selectedFile.value = null
  assets.value = []
  errorMessage.value = ''
  reloadTrigger.value = 0
  renderCount.value = 0
  indexUrl.value = undefined
  indexContent.value = undefined
  Object.assign(fileValidity, validtyDefault)
}

const startClock = () => {
  renderCount.value = 0
  renderStartTime.value = performance.now()
  fileValidity.renderDurationValid = 'loading'
}

function hasImage(dataURL: string): boolean {
  const regex = /^data:image\/png;base64,([A-Za-z0-9+/]+={0,2})$/
  return regex.test(dataURL)
}

const consistencyField = (payload) => {
  const version: number = parseFloat(payload?.version ?? '0')
  return version >= 1.0 ? payload.base64Details : payload.image
}

useEventListener(window, 'message', async (res) => {
  if (
    res.data?.type === 'kodahash/render/completed'
    && previewHash.value === res.data.payload.hash
  ) {
    renderCount.value++
    fileValidity.kodaRendererCalledOnce = renderCount.value === 1

    const payload = res.data?.payload
    renderEndTime.value = performance.now()
    const duration = renderEndTime.value - renderStartTime.value
    fileValidity.renderDurationValid = duration < config.maxAllowedLoadTime
    fileValidity.validKodaRenderPayload
      = Boolean(payload?.image) && hasImage(payload.image)
    if (fileValidity.validKodaRenderPayload) {
      if (reloadTrigger.value === 0) {
        firstImage.value = consistencyField(payload)
        reloadTrigger.value = 1
      }
      else if (
        fileValidity.consistent === 'loading'
        || fileValidity.consistent === 'unknown'
      ) {
        fileValidity.consistent = firstImage.value === consistencyField(payload)
      }
    }
    else {
      fileValidity.consistent = 'unknown'
    }
  }
})
</script>