mirror of
				https://github.com/crater-invoice/crater.git
				synced 2025-11-03 14:03:18 -05:00 
			
		
		
		
	
		
			
				
	
	
		
			1031 lines
		
	
	
		
			32 KiB
		
	
	
	
		
			Vue
		
	
	
	
	
	
			
		
		
	
	
			1031 lines
		
	
	
		
			32 KiB
		
	
	
	
		
			Vue
		
	
	
	
	
	
<template>
 | 
						|
  <ModulePlaceholder v-if="isFetchingInitialData" />
 | 
						|
  <BasePage v-else class="bg-white">
 | 
						|
    <BasePageHeader :title="moduleData.name">
 | 
						|
      <BaseBreadcrumb>
 | 
						|
        <BaseBreadcrumbItem :title="$t('general.home')" to="dashboard" />
 | 
						|
        <BaseBreadcrumbItem :title="$t('modules.title')" to="/admin/modules" />
 | 
						|
        <BaseBreadcrumbItem :title="moduleData.name" to="#" active />
 | 
						|
      </BaseBreadcrumb>
 | 
						|
    </BasePageHeader>
 | 
						|
    <!-- Main Content -->
 | 
						|
    <div
 | 
						|
      class="
 | 
						|
        lg:grid lg:grid-rows-1 lg:grid-cols-7 lg:gap-x-8 lg:gap-y-10
 | 
						|
        xl:gap-x-16
 | 
						|
        mt-6
 | 
						|
      "
 | 
						|
    >
 | 
						|
      <!-- Product image -->
 | 
						|
      <div class="lg:row-end-1 lg:col-span-4">
 | 
						|
        <div class="flex flex-col-reverse">
 | 
						|
          <div
 | 
						|
            class="hidden mt-6 w-full max-w-2xl mx-auto sm:block lg:max-w-none"
 | 
						|
          >
 | 
						|
            <div
 | 
						|
              class="grid grid-cols-3 xl:grid-cols-4 gap-6"
 | 
						|
              aria-orientation="horizontal"
 | 
						|
              role="tablist"
 | 
						|
            >
 | 
						|
              <button
 | 
						|
                v-if="thumbnail && videoUrl"
 | 
						|
                :class="[
 | 
						|
                  'relative  md:h-24 lg:h-36 rounded hover:bg-gray-50',
 | 
						|
                  {
 | 
						|
                    'outline-none ring ring-offset-1 ring-primary-500':
 | 
						|
                      displayVideo,
 | 
						|
                  },
 | 
						|
                ]"
 | 
						|
                type="button"
 | 
						|
                @click="setDisplayVideo"
 | 
						|
              >
 | 
						|
                <span class="absolute inset-0 rounded-md overflow-hidden">
 | 
						|
                  <img
 | 
						|
                    :src="thumbnail"
 | 
						|
                    alt=""
 | 
						|
                    class="w-full h-full object-center object-cover"
 | 
						|
                  />
 | 
						|
                </span>
 | 
						|
                <span
 | 
						|
                  class="
 | 
						|
                    ring-transparent
 | 
						|
                    absolute
 | 
						|
                    inset-0
 | 
						|
                    rounded-md
 | 
						|
                    ring-2 ring-offset-2
 | 
						|
                    pointer-events-none
 | 
						|
                  "
 | 
						|
                  aria-hidden="true"
 | 
						|
                ></span>
 | 
						|
              </button>
 | 
						|
 | 
						|
              <button
 | 
						|
                v-for="(screenshot, ssIndx) in displayImages"
 | 
						|
                id="tabs-1-tab-1"
 | 
						|
                :key="ssIndx"
 | 
						|
                :class="[
 | 
						|
                  'relative  md:h-24 lg:h-36 rounded hover:bg-gray-50',
 | 
						|
                  {
 | 
						|
                    'outline-none ring ring-offset-1 ring-primary-500':
 | 
						|
                      displayImage === screenshot.url,
 | 
						|
                  },
 | 
						|
                ]"
 | 
						|
                type="button"
 | 
						|
                @click="setDisplayImage(screenshot.url)"
 | 
						|
              >
 | 
						|
                <span class="absolute inset-0 rounded-md overflow-hidden">
 | 
						|
                  <img
 | 
						|
                    :src="screenshot.url"
 | 
						|
                    alt=""
 | 
						|
                    class="w-full h-full object-center object-cover"
 | 
						|
                  />
 | 
						|
                </span>
 | 
						|
                <span
 | 
						|
                  class="
 | 
						|
                    ring-transparent
 | 
						|
                    absolute
 | 
						|
                    inset-0
 | 
						|
                    rounded-md
 | 
						|
                    ring-2 ring-offset-2
 | 
						|
                    pointer-events-none
 | 
						|
                  "
 | 
						|
                  aria-hidden="true"
 | 
						|
                ></span>
 | 
						|
              </button>
 | 
						|
            </div>
 | 
						|
          </div>
 | 
						|
 | 
						|
          <div v-if="displayVideo" class="aspect-w-4 aspect-h-3">
 | 
						|
            <iframe
 | 
						|
              :src="videoUrl"
 | 
						|
              class="sm:rounded-lg"
 | 
						|
              frameborder="0"
 | 
						|
              allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
 | 
						|
              allowfullscreen
 | 
						|
            >
 | 
						|
            </iframe>
 | 
						|
          </div>
 | 
						|
 | 
						|
          <div
 | 
						|
            v-else
 | 
						|
            class="aspect-w-4 aspect-h-3 rounded-lg bg-gray-100 overflow-hidden"
 | 
						|
          >
 | 
						|
            <img
 | 
						|
              :src="displayImage"
 | 
						|
              alt="Module Images"
 | 
						|
              class="w-full h-full object-center object-cover sm:rounded-lg"
 | 
						|
            />
 | 
						|
          </div>
 | 
						|
        </div>
 | 
						|
      </div>
 | 
						|
 | 
						|
      <!-- Product details -->
 | 
						|
      <div
 | 
						|
        class="
 | 
						|
          max-w-2xl
 | 
						|
          mx-auto
 | 
						|
          mt-10
 | 
						|
          lg:max-w-none lg:mt-0 lg:row-end-2 lg:row-span-2 lg:col-span-3
 | 
						|
          w-full
 | 
						|
        "
 | 
						|
      >
 | 
						|
        <!-- Average Rating -->
 | 
						|
 | 
						|
        <h3 class="sr-only">Reviews</h3>
 | 
						|
 | 
						|
        <div class="flex items-center">
 | 
						|
          <BaseRating :rating="averageRating" />
 | 
						|
        </div>
 | 
						|
        <p class="sr-only">4 out of 5 stars</p>
 | 
						|
 | 
						|
        <!-- Module Name and Version -->
 | 
						|
        <div class="flex flex-col-reverse">
 | 
						|
          <div class="mt-4">
 | 
						|
            <h1
 | 
						|
              class="
 | 
						|
                text-2xl
 | 
						|
                font-extrabold
 | 
						|
                tracking-tight
 | 
						|
                text-gray-900
 | 
						|
                sm:text-3xl
 | 
						|
              "
 | 
						|
            >
 | 
						|
              {{ moduleData.name }}
 | 
						|
            </h1>
 | 
						|
 | 
						|
            <h2 id="information-heading" class="sr-only">
 | 
						|
              Product information
 | 
						|
            </h2>
 | 
						|
 | 
						|
            <p
 | 
						|
              v-if="moduleData.latest_module_version"
 | 
						|
              class="text-sm text-gray-500 mt-2"
 | 
						|
            >
 | 
						|
              {{ $t('modules.version') }}
 | 
						|
              {{ moduleVersion }} ({{ $t('modules.last_updated') }}
 | 
						|
              {{ updatedAt }})
 | 
						|
            </p>
 | 
						|
          </div>
 | 
						|
        </div>
 | 
						|
 | 
						|
        <!-- Module Description  -->
 | 
						|
        <div
 | 
						|
          class="prose prose-sm max-w-none text-gray-500 text-sm my-10"
 | 
						|
          v-html="moduleData.long_description"
 | 
						|
        />
 | 
						|
 | 
						|
        <!-- Module Pricing -->
 | 
						|
        <div v-if="!moduleData.purchased">
 | 
						|
          <RadioGroup v-model="selectedPlan">
 | 
						|
            <RadioGroupLabel class="sr-only"> Pricing plans </RadioGroupLabel>
 | 
						|
            <div class="relative bg-white rounded-md -space-y-px">
 | 
						|
              <RadioGroupOption
 | 
						|
                v-for="(size, sizeIdx) in modulePrice"
 | 
						|
                :key="size.name"
 | 
						|
                v-slot="{ checked, active }"
 | 
						|
                as="template"
 | 
						|
                :value="size"
 | 
						|
              >
 | 
						|
                <div
 | 
						|
                  :class="[
 | 
						|
                    sizeIdx === 0 ? 'rounded-tl-md rounded-tr-md' : '',
 | 
						|
                    sizeIdx === modulePrice.length - 1
 | 
						|
                      ? 'rounded-bl-md rounded-br-md'
 | 
						|
                      : '',
 | 
						|
                    checked
 | 
						|
                      ? 'bg-primary-50 border-primary-200 z-10'
 | 
						|
                      : 'border-gray-200',
 | 
						|
                    'relative border p-4 flex flex-col cursor-pointer md:pl-4 md:pr-6 md:grid md:grid-cols-2 focus:outline-none',
 | 
						|
                  ]"
 | 
						|
                >
 | 
						|
                  <div class="flex items-center text-sm">
 | 
						|
                    <span
 | 
						|
                      :class="[
 | 
						|
                        checked
 | 
						|
                          ? 'bg-primary-600 border-transparent'
 | 
						|
                          : 'bg-white border-gray-300',
 | 
						|
                        active ? 'ring-2 ring-offset-2 ring-primary-500' : '',
 | 
						|
                        'h-4 w-4 rounded-full border flex items-center justify-center',
 | 
						|
                      ]"
 | 
						|
                      aria-hidden="true"
 | 
						|
                    >
 | 
						|
                      <span class="rounded-full bg-white w-1.5 h-1.5" />
 | 
						|
                    </span>
 | 
						|
                    <RadioGroupLabel
 | 
						|
                      as="span"
 | 
						|
                      :class="[
 | 
						|
                        checked ? 'text-primary-900' : 'text-gray-900',
 | 
						|
                        'ml-3 font-medium',
 | 
						|
                      ]"
 | 
						|
                    >
 | 
						|
                      {{ size.name }}
 | 
						|
                    </RadioGroupLabel>
 | 
						|
                  </div>
 | 
						|
                  <RadioGroupDescription
 | 
						|
                    class="ml-6 pl-1 text-base md:ml-0 md:pl-0 md:text-center"
 | 
						|
                  >
 | 
						|
                    <span
 | 
						|
                      :class="[
 | 
						|
                        checked ? 'text-primary-900' : 'text-gray-900',
 | 
						|
                        'font-medium',
 | 
						|
                      ]"
 | 
						|
                    >
 | 
						|
                      $ {{ size.price }}
 | 
						|
                    </span>
 | 
						|
                  </RadioGroupDescription>
 | 
						|
                </div>
 | 
						|
              </RadioGroupOption>
 | 
						|
            </div>
 | 
						|
          </RadioGroup>
 | 
						|
        </div>
 | 
						|
 | 
						|
        <!-- Button Section  -->
 | 
						|
 | 
						|
        <!-- If Module is not purchased -->
 | 
						|
        <a
 | 
						|
          v-if="!moduleData.purchased"
 | 
						|
          :href="`${globalStore.config.base_url}/modules/${moduleData.slug}`"
 | 
						|
          target="_blank"
 | 
						|
          class="grid grid-cols-1 gap-x-6 gap-y-4 sm:grid-cols-2"
 | 
						|
        >
 | 
						|
          <BaseButton
 | 
						|
            size="xl"
 | 
						|
            class="items-center flex justify-center text-base mt-10"
 | 
						|
          >
 | 
						|
            <BaseIcon name="ShoppingCartIcon" class="mr-2" />
 | 
						|
            {{ $t('modules.buy_now') }}
 | 
						|
          </BaseButton>
 | 
						|
        </a>
 | 
						|
 | 
						|
        <!-- When module is Purchased -->
 | 
						|
        <div v-else>
 | 
						|
          <!-- Module not installed -->
 | 
						|
          <div
 | 
						|
            v-if="!moduleData.installed"
 | 
						|
            class="grid grid-cols-1 gap-x-6 gap-y-4 sm:grid-cols-2"
 | 
						|
          >
 | 
						|
            <BaseButton
 | 
						|
              v-if="moduleData.latest_module_version"
 | 
						|
              size="xl"
 | 
						|
              variant="primary-outline"
 | 
						|
              outline
 | 
						|
              :loading="isInstalling"
 | 
						|
              :disabled="isInstalling"
 | 
						|
              class="mr-4 flex items-center justify-center text-base"
 | 
						|
              @click="installModule()"
 | 
						|
            >
 | 
						|
              <BaseIcon v-if="!isInstalling" name="DownloadIcon" class="mr-2" />
 | 
						|
              {{ $t('modules.install') }}
 | 
						|
            </BaseButton>
 | 
						|
          </div>
 | 
						|
 | 
						|
          <!-- Module already installed -->
 | 
						|
          <div v-else-if="isModuleInstalled">
 | 
						|
            <!-- When new module version is available -->
 | 
						|
 | 
						|
            <div class="grid grid-cols-1 gap-x-6 gap-y-4 sm:grid-cols-2">
 | 
						|
              <BaseButton
 | 
						|
                v-if="moduleData.update_available"
 | 
						|
                variant="primary"
 | 
						|
                size="xl"
 | 
						|
                :loading="isInstalling"
 | 
						|
                :disabled="isInstalling"
 | 
						|
                class="mr-4 flex items-center justify-center text-base"
 | 
						|
                @click="installModule()"
 | 
						|
              >
 | 
						|
                {{ $t('modules.update_to') }}
 | 
						|
                <span class="ml-2">{{ moduleData.latest_module_version }}</span>
 | 
						|
              </BaseButton>
 | 
						|
 | 
						|
              <BaseButton
 | 
						|
                v-if="moduleData.enabled"
 | 
						|
                variant="danger"
 | 
						|
                size="xl"
 | 
						|
                :loading="isDisabling"
 | 
						|
                :disabled="isDisabling"
 | 
						|
                class="mr-4 flex items-center justify-center text-base"
 | 
						|
                @click="disableModule"
 | 
						|
              >
 | 
						|
                <BaseIcon v-if="!isDisabling" name="BanIcon" class="mr-2" />
 | 
						|
                {{ $t('modules.disable') }}
 | 
						|
              </BaseButton>
 | 
						|
              <BaseButton
 | 
						|
                v-else
 | 
						|
                variant="primary-outline"
 | 
						|
                size="xl"
 | 
						|
                :loading="isEnabling"
 | 
						|
                :disabled="isEnabling"
 | 
						|
                class="mr-4 flex items-center justify-center text-base"
 | 
						|
                @click="enableModule"
 | 
						|
              >
 | 
						|
                <BaseIcon v-if="!isEnabling" name="CheckIcon" class="mr-2" />
 | 
						|
                {{ $t('modules.enable') }}
 | 
						|
              </BaseButton>
 | 
						|
            </div>
 | 
						|
          </div>
 | 
						|
        </div>
 | 
						|
 | 
						|
        <div class="mt-10"></div>
 | 
						|
 | 
						|
        <!-- HighLights  -->
 | 
						|
        <div class="border-t border-gray-200 mt-10 pt-10">
 | 
						|
          <h3 class="text-sm font-medium text-gray-900">
 | 
						|
            {{ $t('modules.what_you_get') }}
 | 
						|
          </h3>
 | 
						|
          <div class="mt-4 prose prose-sm max-w-none text-gray-500">
 | 
						|
            <div
 | 
						|
              class="prose prose-sm max-w-none text-gray-500 text-sm"
 | 
						|
              v-html="moduleData.highlights"
 | 
						|
            />
 | 
						|
          </div>
 | 
						|
        </div>
 | 
						|
        <div class="border-t border-gray-200 mt-10 pt-10">
 | 
						|
          <div
 | 
						|
            v-for="(link, key) in moduleData.links"
 | 
						|
            :key="key"
 | 
						|
            class="mb-4 last:mb-0 flex"
 | 
						|
          >
 | 
						|
            <BaseIcon :name="link.icon" class="mr-4" />
 | 
						|
            <a :href="link.link" class="text-primary-500" target="_blank">
 | 
						|
              {{ link.label }}
 | 
						|
            </a>
 | 
						|
          </div>
 | 
						|
        </div>
 | 
						|
        <!-- Installation Steps  -->
 | 
						|
        <div v-if="isInstalling" class="border-t border-gray-200 mt-10 pt-10">
 | 
						|
          <ul class="w-full p-0 list-none">
 | 
						|
            <li
 | 
						|
              v-for="step in installationSteps"
 | 
						|
              :key="step.stepUrl"
 | 
						|
              class="
 | 
						|
                flex
 | 
						|
                justify-between
 | 
						|
                w-full
 | 
						|
                py-3
 | 
						|
                border-b border-gray-200 border-solid
 | 
						|
                last:border-b-0
 | 
						|
              "
 | 
						|
            >
 | 
						|
              <p class="m-0 text-sm leading-8">
 | 
						|
                {{ $t(step.translationKey) }}
 | 
						|
              </p>
 | 
						|
              <div class="flex flex-row items-center">
 | 
						|
                <span v-if="step.time" class="mr-3 text-xs text-gray-500">
 | 
						|
                  {{ step.time }}
 | 
						|
                </span>
 | 
						|
                <span
 | 
						|
                  :class="statusClass(step)"
 | 
						|
                  class="block py-1 text-sm text-center uppercase rounded-full"
 | 
						|
                  style="width: 88px"
 | 
						|
                >
 | 
						|
                  {{ getStatus(step) }}
 | 
						|
                </span>
 | 
						|
              </div>
 | 
						|
            </li>
 | 
						|
          </ul>
 | 
						|
        </div>
 | 
						|
 | 
						|
        <!-- Social Share  -->
 | 
						|
        <!-- <div class="border-t border-gray-200 mt-10 pt-10">
 | 
						|
          <h3 class="text-sm font-medium text-gray-900">Share</h3>
 | 
						|
          <ul role="list" class="flex items-center space-x-6 mt-4">
 | 
						|
            <li>
 | 
						|
              <a
 | 
						|
                href="#"
 | 
						|
                class="
 | 
						|
                  flex
 | 
						|
                  items-center
 | 
						|
                  justify-center
 | 
						|
                  w-6
 | 
						|
                  h-6
 | 
						|
                  text-gray-400
 | 
						|
                  hover:text-gray-500
 | 
						|
                "
 | 
						|
              >
 | 
						|
                <span class="sr-only">Share on Facebook</span>
 | 
						|
                <svg
 | 
						|
                  class="w-5 h-5"
 | 
						|
                  fill="currentColor"
 | 
						|
                  viewBox="0 0 20 20"
 | 
						|
                  aria-hidden="true"
 | 
						|
                >
 | 
						|
                  <path
 | 
						|
                    fill-rule="evenodd"
 | 
						|
                    d="M20 10c0-5.523-4.477-10-10-10S0 4.477 0 10c0 4.991 3.657 9.128 8.438 9.878v-6.987h-2.54V10h2.54V7.797c0-2.506 1.492-3.89 3.777-3.89 1.094 0 2.238.195 2.238.195v2.46h-1.26c-1.243 0-1.63.771-1.63 1.562V10h2.773l-.443 2.89h-2.33v6.988C16.343 19.128 20 14.991 20 10z"
 | 
						|
                    clip-rule="evenodd"
 | 
						|
                  />
 | 
						|
                </svg>
 | 
						|
              </a>
 | 
						|
            </li>
 | 
						|
            <li>
 | 
						|
              <a
 | 
						|
                href="#"
 | 
						|
                class="
 | 
						|
                  flex
 | 
						|
                  items-center
 | 
						|
                  justify-center
 | 
						|
                  w-6
 | 
						|
                  h-6
 | 
						|
                  text-gray-400
 | 
						|
                  hover:text-gray-500
 | 
						|
                "
 | 
						|
              >
 | 
						|
                <span class="sr-only">Share on Instagram</span>
 | 
						|
                <svg
 | 
						|
                  class="w-6 h-6"
 | 
						|
                  fill="currentColor"
 | 
						|
                  viewBox="0 0 24 24"
 | 
						|
                  aria-hidden="true"
 | 
						|
                >
 | 
						|
                  <path
 | 
						|
                    fill-rule="evenodd"
 | 
						|
                    d="M12.315 2c2.43 0 2.784.013 3.808.06 1.064.049 1.791.218 2.427.465a4.902 4.902 0 011.772 1.153 4.902 4.902 0 011.153 1.772c.247.636.416 1.363.465 2.427.048 1.067.06 1.407.06 4.123v.08c0 2.643-.012 2.987-.06 4.043-.049 1.064-.218 1.791-.465 2.427a4.902 4.902 0 01-1.153 1.772 4.902 4.902 0 01-1.772 1.153c-.636.247-1.363.416-2.427.465-1.067.048-1.407.06-4.123.06h-.08c-2.643 0-2.987-.012-4.043-.06-1.064-.049-1.791-.218-2.427-.465a4.902 4.902 0 01-1.772-1.153 4.902 4.902 0 01-1.153-1.772c-.247-.636-.416-1.363-.465-2.427-.047-1.024-.06-1.379-.06-3.808v-.63c0-2.43.013-2.784.06-3.808.049-1.064.218-1.791.465-2.427a4.902 4.902 0 011.153-1.772A4.902 4.902 0 015.45 2.525c.636-.247 1.363-.416 2.427-.465C8.901 2.013 9.256 2 11.685 2h.63zm-.081 1.802h-.468c-2.456 0-2.784.011-3.807.058-.975.045-1.504.207-1.857.344-.467.182-.8.398-1.15.748-.35.35-.566.683-.748 1.15-.137.353-.3.882-.344 1.857-.047 1.023-.058 1.351-.058 3.807v.468c0 2.456.011 2.784.058 3.807.045.975.207 1.504.344 1.857.182.466.399.8.748 1.15.35.35.683.566 1.15.748.353.137.882.3 1.857.344 1.054.048 1.37.058 4.041.058h.08c2.597 0 2.917-.01 3.96-.058.976-.045 1.505-.207 1.858-.344.466-.182.8-.398 1.15-.748.35-.35.566-.683.748-1.15.137-.353.3-.882.344-1.857.048-1.055.058-1.37.058-4.041v-.08c0-2.597-.01-2.917-.058-3.96-.045-.976-.207-1.505-.344-1.858a3.097 3.097 0 00-.748-1.15 3.098 3.098 0 00-1.15-.748c-.353-.137-.882-.3-1.857-.344-1.023-.047-1.351-.058-3.807-.058zM12 6.865a5.135 5.135 0 110 10.27 5.135 5.135 0 010-10.27zm0 1.802a3.333 3.333 0 100 6.666 3.333 3.333 0 000-6.666zm5.338-3.205a1.2 1.2 0 110 2.4 1.2 1.2 0 010-2.4z"
 | 
						|
                    clip-rule="evenodd"
 | 
						|
                  />
 | 
						|
                </svg>
 | 
						|
              </a>
 | 
						|
            </li>
 | 
						|
            <li>
 | 
						|
              <a
 | 
						|
                href="#"
 | 
						|
                class="
 | 
						|
                  flex
 | 
						|
                  items-center
 | 
						|
                  justify-center
 | 
						|
                  w-6
 | 
						|
                  h-6
 | 
						|
                  text-gray-400
 | 
						|
                  hover:text-gray-500
 | 
						|
                "
 | 
						|
              >
 | 
						|
                <span class="sr-only">Share on Twitter</span>
 | 
						|
                <svg
 | 
						|
                  class="w-5 h-5"
 | 
						|
                  fill="currentColor"
 | 
						|
                  viewBox="0 0 20 20"
 | 
						|
                  aria-hidden="true"
 | 
						|
                >
 | 
						|
                  <path
 | 
						|
                    d="M6.29 18.251c7.547 0 11.675-6.253 11.675-11.675 0-.178 0-.355-.012-.53A8.348 8.348 0 0020 3.92a8.19 8.19 0 01-2.357.646 4.118 4.118 0 001.804-2.27 8.224 8.224 0 01-2.605.996 4.107 4.107 0 00-6.993 3.743 11.65 11.65 0 01-8.457-4.287 4.106 4.106 0 001.27 5.477A4.073 4.073 0 01.8 7.713v.052a4.105 4.105 0 003.292 4.022 4.095 4.095 0 01-1.853.07 4.108 4.108 0 003.834 2.85A8.233 8.233 0 010 16.407a11.616 11.616 0 006.29 1.84"
 | 
						|
                  />
 | 
						|
                </svg>
 | 
						|
              </a>
 | 
						|
            </li>
 | 
						|
          </ul>
 | 
						|
        </div> -->
 | 
						|
      </div>
 | 
						|
 | 
						|
      <div
 | 
						|
        class="
 | 
						|
          w-full
 | 
						|
          max-w-2xl
 | 
						|
          mx-auto
 | 
						|
          mt-16
 | 
						|
          lg:max-w-none lg:mt-0 lg:col-span-4
 | 
						|
        "
 | 
						|
      >
 | 
						|
        <TabGroup as="div">
 | 
						|
          <TabList class="-mb-px flex space-x-8 border-b border-gray-200">
 | 
						|
            <Tab v-slot="{ selected }" as="template">
 | 
						|
              <button
 | 
						|
                :class="[
 | 
						|
                  selected
 | 
						|
                    ? 'border-primary-600 text-primary-600'
 | 
						|
                    : 'border-transparent text-gray-700 hover:text-gray-800 hover:border-gray-300',
 | 
						|
                  'whitespace-nowrap py-6 border-b-2 font-medium text-sm',
 | 
						|
                ]"
 | 
						|
              >
 | 
						|
                {{ $t('modules.customer_reviews') }}
 | 
						|
              </button>
 | 
						|
            </Tab>
 | 
						|
            <Tab v-slot="{ selected }" as="template">
 | 
						|
              <button
 | 
						|
                :class="[
 | 
						|
                  selected
 | 
						|
                    ? 'border-primary-600 text-primary-600'
 | 
						|
                    : 'border-transparent text-gray-700 hover:text-gray-800 hover:border-gray-300',
 | 
						|
                  'whitespace-nowrap py-6 border-b-2 font-medium text-sm',
 | 
						|
                ]"
 | 
						|
              >
 | 
						|
                {{ $t('modules.faq') }}
 | 
						|
              </button>
 | 
						|
            </Tab>
 | 
						|
            <Tab v-slot="{ selected }" as="template">
 | 
						|
              <button
 | 
						|
                :class="[
 | 
						|
                  selected
 | 
						|
                    ? 'border-primary-600 text-primary-600'
 | 
						|
                    : 'border-transparent text-gray-700 hover:text-gray-800 hover:border-gray-300',
 | 
						|
                  'whitespace-nowrap py-6 border-b-2 font-medium text-sm',
 | 
						|
                ]"
 | 
						|
              >
 | 
						|
                {{ $t('modules.license') }}
 | 
						|
              </button>
 | 
						|
            </Tab>
 | 
						|
          </TabList>
 | 
						|
          <TabPanels as="template">
 | 
						|
            <!-- Customer Reviews  -->
 | 
						|
            <TabPanel class="-mb-10">
 | 
						|
              <h3 class="sr-only">Customer Reviews</h3>
 | 
						|
              <div v-if="moduleData.reviews.length">
 | 
						|
                <div
 | 
						|
                  v-for="(review, reviewIdx) in moduleData.reviews"
 | 
						|
                  :key="reviewIdx"
 | 
						|
                  class="flex text-sm text-gray-500 space-x-4"
 | 
						|
                >
 | 
						|
                  <div class="flex-none py-10">
 | 
						|
                    <span
 | 
						|
                      class="
 | 
						|
                        inline-flex
 | 
						|
                        items-center
 | 
						|
                        justify-center
 | 
						|
                        h-12
 | 
						|
                        w-12
 | 
						|
                        rounded-full
 | 
						|
                        bg-gray-500
 | 
						|
                      "
 | 
						|
                    >
 | 
						|
                      <span
 | 
						|
                        class="
 | 
						|
                          text-lg
 | 
						|
                          font-medium
 | 
						|
                          leading-none
 | 
						|
                          text-white
 | 
						|
                          uppercase
 | 
						|
                        "
 | 
						|
                        >{{ review.customer.name[0] }}</span
 | 
						|
                      >
 | 
						|
                    </span>
 | 
						|
                  </div>
 | 
						|
                  <div
 | 
						|
                    :class="[
 | 
						|
                      reviewIdx === 0 ? '' : 'border-t border-gray-200',
 | 
						|
                      'py-10',
 | 
						|
                    ]"
 | 
						|
                  >
 | 
						|
                    <h3 class="font-medium text-gray-900">
 | 
						|
                      {{ review.customer.name }}
 | 
						|
                    </h3>
 | 
						|
                    <p>
 | 
						|
                      {{ moment(review.created_at).format('MMMM Do YYYY') }}
 | 
						|
                    </p>
 | 
						|
 | 
						|
                    <div class="flex items-center mt-4">
 | 
						|
                      <BaseRating :rating="review.rating" />
 | 
						|
                    </div>
 | 
						|
 | 
						|
                    <div
 | 
						|
                      class="mt-4 prose prose-sm max-w-none text-gray-500"
 | 
						|
                      v-html="review.feedback"
 | 
						|
                    />
 | 
						|
                  </div>
 | 
						|
                </div>
 | 
						|
              </div>
 | 
						|
              <div v-else class="flex w-full items-center justify-center">
 | 
						|
                <p class="text-gray-500 mt-10 text-sm">
 | 
						|
                  {{ $t('modules.no_reviews_found') }}
 | 
						|
                </p>
 | 
						|
              </div>
 | 
						|
            </TabPanel>
 | 
						|
 | 
						|
            <!-- FAQs  -->
 | 
						|
            <TabPanel as="dl" class="text-sm text-gray-500">
 | 
						|
              <h3 class="sr-only">Frequently Asked Questions</h3>
 | 
						|
 | 
						|
              <template v-for="faq in moduleData.faq" :key="faq.question">
 | 
						|
                <dt class="mt-10 font-medium text-gray-900">
 | 
						|
                  {{ faq.question }}
 | 
						|
                </dt>
 | 
						|
                <dd class="mt-2 prose prose-sm max-w-none text-gray-500">
 | 
						|
                  <p>{{ faq.answer }}</p>
 | 
						|
                </dd>
 | 
						|
              </template>
 | 
						|
            </TabPanel>
 | 
						|
 | 
						|
            <!-- License  -->
 | 
						|
            <TabPanel class="pt-10">
 | 
						|
              <h3 class="sr-only">License</h3>
 | 
						|
 | 
						|
              <div
 | 
						|
                class="prose prose-sm max-w-none text-gray-500"
 | 
						|
                v-html="moduleData.license"
 | 
						|
              />
 | 
						|
            </TabPanel>
 | 
						|
          </TabPanels>
 | 
						|
        </TabGroup>
 | 
						|
      </div>
 | 
						|
    </div>
 | 
						|
 | 
						|
    <!-- Other Modules -->
 | 
						|
    <div
 | 
						|
      v-if="otherModules && otherModules.length"
 | 
						|
      class="mt-24 sm:mt-32 lg:max-w-none"
 | 
						|
    >
 | 
						|
      <div class="flex items-center justify-between space-x-4">
 | 
						|
        <h2 class="text-lg font-medium text-gray-900">
 | 
						|
          {{ $t('modules.other_modules') }}
 | 
						|
        </h2>
 | 
						|
        <a
 | 
						|
          href="/admin/modules"
 | 
						|
          class="
 | 
						|
            whitespace-nowrap
 | 
						|
            text-sm
 | 
						|
            font-medium
 | 
						|
            text-primary-600
 | 
						|
            hover:text-primary-500
 | 
						|
          "
 | 
						|
          >{{ $t('modules.view_all')
 | 
						|
          }}<span aria-hidden="true"> →</span></a
 | 
						|
        >
 | 
						|
      </div>
 | 
						|
      <div
 | 
						|
        class="
 | 
						|
          mt-6
 | 
						|
          grid grid-cols-1
 | 
						|
          gap-x-8 gap-y-8
 | 
						|
          sm:grid-cols-2 sm:gap-y-10
 | 
						|
          lg:grid-cols-4
 | 
						|
        "
 | 
						|
      >
 | 
						|
        <div v-for="(other, moduleIdx) in otherModules" :key="moduleIdx">
 | 
						|
          <RecentModuleCard :data="other" />
 | 
						|
        </div>
 | 
						|
      </div>
 | 
						|
    </div>
 | 
						|
 | 
						|
    <div class="p-6"></div>
 | 
						|
  </BasePage>
 | 
						|
</template>
 | 
						|
 | 
						|
<script setup>
 | 
						|
import { Tab, TabGroup, TabList, TabPanel, TabPanels } from '@headlessui/vue'
 | 
						|
import {
 | 
						|
  RadioGroup,
 | 
						|
  RadioGroupDescription,
 | 
						|
  RadioGroupLabel,
 | 
						|
  RadioGroupOption,
 | 
						|
} from '@headlessui/vue'
 | 
						|
import { useModuleStore } from '@/scripts/admin/stores/module'
 | 
						|
import { computed, onMounted, ref, watch, reactive } from 'vue'
 | 
						|
import { required, minLength, maxLength, helpers } from '@vuelidate/validators'
 | 
						|
import { useVuelidate } from '@vuelidate/core'
 | 
						|
import { useRoute } from 'vue-router'
 | 
						|
import { useDialogStore } from '@/scripts/stores/dialog'
 | 
						|
import { useI18n } from 'vue-i18n'
 | 
						|
import moment from 'moment'
 | 
						|
import axios from 'axios'
 | 
						|
import ModulePlaceholder from './partials/ModulePlaceholder.vue'
 | 
						|
import RecentModuleCard from './partials/RecentModuleCard.vue'
 | 
						|
import { useNotificationStore } from '@/scripts/stores/notification'
 | 
						|
import { useGlobalStore } from '@/scripts/admin/stores/global'
 | 
						|
const globalStore = useGlobalStore()
 | 
						|
 | 
						|
const moduleStore = useModuleStore()
 | 
						|
const notificationStore = useNotificationStore()
 | 
						|
const dialogStore = useDialogStore()
 | 
						|
 | 
						|
const route = useRoute()
 | 
						|
const { t } = useI18n()
 | 
						|
let isInstalling = ref(false)
 | 
						|
let isFetchingInitialData = ref(true)
 | 
						|
let displayImage = ref('')
 | 
						|
let isEnabling = ref(false)
 | 
						|
let isDisabling = ref(false)
 | 
						|
let isUpdating = ref(false)
 | 
						|
 | 
						|
loadData()
 | 
						|
 | 
						|
watch(
 | 
						|
  () => route.params.slug,
 | 
						|
  async (newSlug) => {
 | 
						|
    loadData()
 | 
						|
  }
 | 
						|
)
 | 
						|
 | 
						|
const moduleData = computed(() => {
 | 
						|
  return moduleStore.currentModule.data
 | 
						|
})
 | 
						|
 | 
						|
const modulePrice = computed(() => {
 | 
						|
  let priceList = []
 | 
						|
 | 
						|
  let monthlyPrice = reactive({
 | 
						|
    name: t('modules.monthly'),
 | 
						|
    price: moduleData?.value?.monthly_price / 100,
 | 
						|
  })
 | 
						|
 | 
						|
  let yearlyPrice = reactive({
 | 
						|
    name: t('modules.yearly'),
 | 
						|
    price: moduleData?.value?.yearly_price / 100,
 | 
						|
  })
 | 
						|
 | 
						|
  if (typeYearly.value) {
 | 
						|
    priceList.push(yearlyPrice)
 | 
						|
  } else if (typeMonthly.value) {
 | 
						|
    priceList.push(monthlyPrice)
 | 
						|
  } else {
 | 
						|
    priceList.push(monthlyPrice)
 | 
						|
    priceList.push(yearlyPrice)
 | 
						|
  }
 | 
						|
 | 
						|
  return priceList
 | 
						|
})
 | 
						|
 | 
						|
const typeYearly = computed(() => {
 | 
						|
  if (moduleData.value) {
 | 
						|
    return moduleData.value.type === 'YEARLY'
 | 
						|
  }
 | 
						|
 | 
						|
  return false
 | 
						|
})
 | 
						|
 | 
						|
const typeMonthly = computed(() => {
 | 
						|
  if (moduleData.value) {
 | 
						|
    return moduleData.value.type === 'MONTHLY'
 | 
						|
  }
 | 
						|
 | 
						|
  return false
 | 
						|
})
 | 
						|
 | 
						|
const isModuleInstalled = computed(() => {
 | 
						|
  if (moduleData.value.installed && moduleData.value.latest_module_version) {
 | 
						|
    return true
 | 
						|
  }
 | 
						|
  return false
 | 
						|
})
 | 
						|
 | 
						|
const otherModules = computed(() => {
 | 
						|
  return moduleStore.currentModule.meta.modules
 | 
						|
})
 | 
						|
 | 
						|
let updatedAt = computed(() => {
 | 
						|
  let latest = ref(moduleData.value.latest_module_version_updated_at)
 | 
						|
  let installed = ref(moduleData.value.installed_module_version_updated_at)
 | 
						|
 | 
						|
  const date = installed.value ? installed.value : latest.value
 | 
						|
 | 
						|
  return moment(date).format('MMMM Do YYYY')
 | 
						|
})
 | 
						|
 | 
						|
let moduleVersion = computed(() => {
 | 
						|
  let latest = ref(moduleData.value.latest_module_version)
 | 
						|
  let installed = ref(moduleData.value.installed_module_version)
 | 
						|
 | 
						|
  let data = installed.value ? installed.value : latest.value
 | 
						|
 | 
						|
  return data
 | 
						|
})
 | 
						|
 | 
						|
let averageRating = computed(() => {
 | 
						|
  return parseInt(moduleData.value.average_rating)
 | 
						|
})
 | 
						|
 | 
						|
const displayImages = computed(() => {
 | 
						|
  let images = reactive([])
 | 
						|
 | 
						|
  let cover = reactive({
 | 
						|
    id: null,
 | 
						|
    url: moduleData.value.cover,
 | 
						|
  })
 | 
						|
 | 
						|
  images.push(cover)
 | 
						|
 | 
						|
  if (moduleData.value.screenshots) {
 | 
						|
    moduleData.value.screenshots.forEach((image) => {
 | 
						|
      images.push(image)
 | 
						|
    })
 | 
						|
  }
 | 
						|
 | 
						|
  return images
 | 
						|
})
 | 
						|
 | 
						|
const displayVideo = ref(false)
 | 
						|
 | 
						|
const thumbnail = ref(null)
 | 
						|
 | 
						|
const videoUrl = ref(null)
 | 
						|
 | 
						|
const selectedPlan = ref(modulePrice.value[0])
 | 
						|
 | 
						|
const installationSteps = reactive([
 | 
						|
  {
 | 
						|
    translationKey: 'modules.download_zip_file',
 | 
						|
    stepUrl: '/api/v1/modules/download',
 | 
						|
    time: null,
 | 
						|
    started: false,
 | 
						|
    completed: false,
 | 
						|
  },
 | 
						|
  {
 | 
						|
    translationKey: 'modules.unzipping_package',
 | 
						|
    stepUrl: '/api/v1/modules/unzip',
 | 
						|
    time: null,
 | 
						|
    started: false,
 | 
						|
    completed: false,
 | 
						|
  },
 | 
						|
  {
 | 
						|
    translationKey: 'modules.copying_files',
 | 
						|
    stepUrl: '/api/v1/modules/copy',
 | 
						|
    time: null,
 | 
						|
    started: false,
 | 
						|
    completed: false,
 | 
						|
  },
 | 
						|
  {
 | 
						|
    translationKey: 'modules.completing_installation',
 | 
						|
    stepUrl: '/api/v1/modules/complete',
 | 
						|
    time: null,
 | 
						|
    started: false,
 | 
						|
    completed: false,
 | 
						|
  },
 | 
						|
])
 | 
						|
 | 
						|
async function installModule() {
 | 
						|
  let path = null
 | 
						|
 | 
						|
  for (let index = 0; index < installationSteps.length; index++) {
 | 
						|
    let currentStep = installationSteps[index]
 | 
						|
 | 
						|
    try {
 | 
						|
      isInstalling.value = true
 | 
						|
      currentStep.started = true
 | 
						|
      let updateParams = {
 | 
						|
        version: moduleData.value.latest_module_version,
 | 
						|
        path: path || null,
 | 
						|
        module: moduleData.value.module_name,
 | 
						|
      }
 | 
						|
 | 
						|
      let requestResponse = await axios.post(currentStep.stepUrl, updateParams)
 | 
						|
 | 
						|
      currentStep.completed = true
 | 
						|
      if (requestResponse.data) {
 | 
						|
        path = requestResponse.data.path
 | 
						|
      }
 | 
						|
 | 
						|
      if (!requestResponse.data.success) {
 | 
						|
        notificationStore.showNotification({
 | 
						|
          type: 'error',
 | 
						|
          message: getErrorMessage(requestResponse.data.message),
 | 
						|
        })
 | 
						|
 | 
						|
        isInstalling.value = false
 | 
						|
        currentStep.started = false
 | 
						|
        currentStep.completed = true
 | 
						|
        return false
 | 
						|
      }
 | 
						|
      if (currentStep.translationKey == 'modules.completing_installation') {
 | 
						|
        isInstalling.value = false
 | 
						|
        notificationStore.showNotification({
 | 
						|
          type: 'success',
 | 
						|
          message: t('modules.install_success'),
 | 
						|
        })
 | 
						|
 | 
						|
        setTimeout(() => {
 | 
						|
          location.reload()
 | 
						|
        }, 1500)
 | 
						|
      }
 | 
						|
    } catch (error) {
 | 
						|
      isInstalling.value = false
 | 
						|
      currentStep.started = false
 | 
						|
      currentStep.completed = true
 | 
						|
      return false
 | 
						|
    }
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
function getErrorMessage(message) {
 | 
						|
  let msg = ref('')
 | 
						|
 | 
						|
  switch (message) {
 | 
						|
    case 'module_not_found':
 | 
						|
      msg = t('modules.module_not_found')
 | 
						|
      break
 | 
						|
 | 
						|
    case 'module_not_purchased':
 | 
						|
      msg = t('modules.module_not_purchased')
 | 
						|
      break
 | 
						|
 | 
						|
    case 'version_not_supported':
 | 
						|
      msg = t('modules.version_not_supported')
 | 
						|
      break
 | 
						|
 | 
						|
    default:
 | 
						|
      msg = message
 | 
						|
      break
 | 
						|
  }
 | 
						|
 | 
						|
  return msg
 | 
						|
}
 | 
						|
 | 
						|
async function loadData() {
 | 
						|
  if (!route.params.slug) {
 | 
						|
    return
 | 
						|
  }
 | 
						|
 | 
						|
  isFetchingInitialData.value = true
 | 
						|
  await moduleStore.fetchModule(route.params.slug).then((response) => {
 | 
						|
    selectedPlan.value = modulePrice.value[0]
 | 
						|
 | 
						|
    videoUrl.value = moduleData.value.video_link
 | 
						|
    thumbnail.value = moduleData.value.video_thumbnail
 | 
						|
 | 
						|
    if (videoUrl.value) {
 | 
						|
      setDisplayVideo()
 | 
						|
      isFetchingInitialData.value = false
 | 
						|
      return
 | 
						|
    }
 | 
						|
    displayImage.value = moduleData.value.cover
 | 
						|
    isFetchingInitialData.value = false
 | 
						|
    return
 | 
						|
  })
 | 
						|
}
 | 
						|
 | 
						|
function statusClass(step) {
 | 
						|
  const status = getStatus(step)
 | 
						|
 | 
						|
  switch (status) {
 | 
						|
    case 'pending':
 | 
						|
      return 'text-primary-800 bg-gray-200'
 | 
						|
    case 'finished':
 | 
						|
      return 'text-teal-500 bg-teal-100'
 | 
						|
    case 'running':
 | 
						|
      return 'text-blue-400 bg-blue-100'
 | 
						|
    case 'error':
 | 
						|
      return 'text-danger bg-red-200'
 | 
						|
    default:
 | 
						|
      return ''
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
function disableModule() {
 | 
						|
  dialogStore
 | 
						|
    .openDialog({
 | 
						|
      title: t('general.are_you_sure'),
 | 
						|
      message: t('modules.disable_warning'),
 | 
						|
      yesLabel: t('general.ok'),
 | 
						|
      noLabel: t('general.cancel'),
 | 
						|
      variant: 'danger',
 | 
						|
      hideNoButton: false,
 | 
						|
      size: 'lg',
 | 
						|
    })
 | 
						|
    .then(async (res) => {
 | 
						|
      if (res) {
 | 
						|
        isDisabling.value = true
 | 
						|
        await moduleStore
 | 
						|
          .disableModule(moduleData.value.module_name)
 | 
						|
          .then((res) => {
 | 
						|
            if (res.data.success) {
 | 
						|
              moduleData.value.enabled = 0
 | 
						|
              isDisabling.value = false
 | 
						|
 | 
						|
              setTimeout(() => {
 | 
						|
                location.reload()
 | 
						|
              }, 1500)
 | 
						|
              return
 | 
						|
            }
 | 
						|
          })
 | 
						|
        isDisabling.value = false
 | 
						|
        return
 | 
						|
      }
 | 
						|
    })
 | 
						|
}
 | 
						|
 | 
						|
async function enableModule() {
 | 
						|
  isEnabling.value = true
 | 
						|
 | 
						|
  await moduleStore.enableModule(moduleData.value.module_name).then((res) => {
 | 
						|
    if (res.data.success) {
 | 
						|
      moduleData.value.enabled = 1
 | 
						|
 | 
						|
      setTimeout(() => {
 | 
						|
        location.reload()
 | 
						|
      }, 1500)
 | 
						|
    }
 | 
						|
    isEnabling.value = false
 | 
						|
    return
 | 
						|
  })
 | 
						|
  isEnabling.value = false
 | 
						|
  return
 | 
						|
}
 | 
						|
 | 
						|
function getStatus(step) {
 | 
						|
  if (step.started && step.completed) {
 | 
						|
    return 'finished'
 | 
						|
  } else if (step.started && !step.completed) {
 | 
						|
    return 'running'
 | 
						|
  } else if (!step.started && !step.completed) {
 | 
						|
    return 'pending'
 | 
						|
  } else {
 | 
						|
    return 'error'
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
function setDisplayImage(url) {
 | 
						|
  displayVideo.value = false
 | 
						|
  displayImage.value = url
 | 
						|
}
 | 
						|
 | 
						|
function setDisplayVideo() {
 | 
						|
  displayVideo.value = true
 | 
						|
  displayImage.value = null
 | 
						|
}
 | 
						|
</script>
 |