kirillgroshkov/kg-releases

View on GitHub
src/pages/ReleasesPage.vue

Summary

Maintainability
Test Coverage
<script setup lang="ts">
import { mdiChevronUp, mdiChevronDown } from '@mdi/js'
import { IsoDateString, LocalDate, localDate } from '@naturalcycles/js-lib'
import { useEventListener } from '@vueuse/core'
import { computed, onMounted, ref } from 'vue'
import { withProgress } from '@/decorators/decorators'
import { timeHM, unixtimePretty } from '@/filters/filters'
import { ReleasesByDay } from '@/srv/model'
import { releasesService } from '@/srv/releases.service'
import { useStore } from '@/store'

const expandedRows = ref(new Set<string>())
const maxReleases = ref(30)
const dayFirst = ref<IsoDateString>('')
const dayLast = ref<IsoDateString | null>('')
const dayLoading = ref<IsoDateString>('')
const dayMax = ref<IsoDateString>('')
const releasesByDay = ref<ReleasesByDay>({})

// const dayNext = computed((): IsoDateString => {
//   if (!dayLast.value) return ''
//
//   return localDate(dayLast.value).subtract(1, 'day').toISODate()
// })

const days = computed((): IsoDateString[] => {
  if (!dayFirst.value || !dayLast.value) return []

  return localDate
    .range(dayLast.value, dayFirst.value, '[]')
    .map(d => d.toISODate())
    .reverse()
})

useEventListener(document, 'visibilitychange', async () => {
  if (document.visibilityState !== 'visible') return
  await reload()
})

onMounted(async () => {
  await reload()
})

const store = useStore()
const loading = ref(false)

async function reload(): Promise<void> {
  if (loading.value) return // don't start new request
  loading.value = true // a bit naive now
  await withProgress(async () => {
    maxReleases.value = 30
    const today = localDate.todayInUTC()
    const todayStr = today.toISODate()
    dayMax.value = today.minus(30, 'day').toISODate()
    releasesByDay.value = store.getReleasesByDay()
    dayFirst.value = todayStr
    dayLast.value = store.getReleasesLastDay() || todayStr

    // await pDelay(1000) // give time for animations to finish
    dayLast.value = await loadDay(today, 0)
    dayLoading.value = ''
    // console.log('dayLast end: ' + this.dayLast)

    // cleanAfterLastDay
    store.cleanAfterLastDay(dayLast.value)
  })
  loading.value = false
}

async function loadDay(day: LocalDate, loaded: number): Promise<string> {
  const dayStr = day.toISODate()
  dayLoading.value = dayStr
  const nextDay = day.plus(1, 'day')
  const nextDayStr = nextDay.toISODate()
  // this.loading = 'loading...'
  // _this.dayLast = dayStr
  dayLast.value = store.getReleasesLastDay()
  // console.log('dayLast: ' + this.dayLast)

  const { releases = [] } = await releasesService.fetchReleases(dayStr, nextDayStr)
  releasesByDay.value = store.getReleasesByDay()
  // this.loading = ''
  // const releasesCount = Object.keys(st().releases).length
  loaded += releases.length
  // console.log('loaded: ' + loaded)

  if (loaded < maxReleases.value && dayStr > dayMax.value) {
    const yesterday = day.minus(1, 'day')
    return await loadDay(yesterday, loaded)
  }

  return dayStr
}

async function loadMore(): Promise<void> {
  const releasesCount = Object.keys(store.releases).length
  maxReleases.value = releasesCount + 30
  dayMax.value = localDate(dayMax.value).minus(30, 'day').toISODate()
  const dayNext = localDate(dayLast.value!).minus(1, 'day')
  dayLast.value = await loadDay(dayNext, releasesCount)
  dayLoading.value = ''
}

function toggleClick(id: string, _$event: MouseEvent): void {
  // console.log($event)

  // if (($event?.target as any)?.nodeName === 'A') alert('target A!')
  if (expandedRows.value.has(id)) {
    expandedRows.value.delete(id)
  } else {
    expandedRows.value.add(id)
  }
  expandedRows.value = new Set(expandedRows.value)
}

function descrClick($event: MouseEvent): void {
  // $event.preventDefault()
  $event.stopImmediatePropagation()
}
</script>

<template>
  <div>
    <div>
      <div class="container releases">
        <table style="width: 100%; max-width: 500px">
          <tr>
            <td>
              <pre>
Last updated: {{ unixtimePretty(store.releasesUpdaterLastFinished) }}
Starred repos: {{ store.userFM.starredReposCount }}
            </pre
              >
            </td>
            <td style="text-align: right; padding-right: 10px">
              <v-btn
                color="primary"
                style="margin-top: -12px"
                :disabled="store.ghostMode"
                @click="reload()"
              >
                reload
              </v-btn>
            </td>
          </tr>
        </table>

        <!--
        <div v-if="false">
          dayFirst={{ dayFirst }}, dayLast={{ dayLast }}
          <div
            v-for="d in days"
            v-if="false"
          >
            {{ d }} {{ (releasesByDay[d] || []).length }}
          </div>
        </div>
        -->

        <div class="tableRow" style="margin: 0 -16px; padding-bottom: 80px">
          <template v-for="day of days">
            <table
              v-if="(releasesByDay[day] || []).length"
              border="0"
              cellspacing="0"
              cellpadding="6"
              class="table1"
            >
              <tr>
                <td colspan="3" style="padding-left: 66px">
                  {{ day }}
                </td>
              </tr>
            </table>

            <table border="0" cellspacing="0" cellpadding="6" class="table1">
              <template v-for="r of releasesByDay[day]">
                <tr class="mainTr" @click="toggleClick(r.id, $event)">
                  <td style="width: 66px; padding: 8px 0 0px 12px; vertical-align: top">
                    <img :src="r.avatarUrl" style="width: 40px; height: 40px" loading="lazy" />
                  </td>
                  <td style="vertical-align: top; padding: 8px 0 0">
                    {{ r.repoFullName }} <br />
                    <span class="ver">{{ r.tagName }}</span>
                  </td>
                  <td style="width: 80px; text-align: right; vertical-align: top; padding-top: 7px">
                    {{ timeHM(r.published) }}
                    <v-icon
                      v-if="expandedRows.has(r.id)"
                      style="opacity: 0.4"
                      :icon="mdiChevronUp"
                    ></v-icon>
                    <v-icon v-else style="opacity: 0.4" :icon="mdiChevronDown"></v-icon>
                  </td>
                </tr>

                <transition name="slide">
                  <tr v-if="expandedRows.has(r.id)" @click="toggleClick(r.id, $event)">
                    <td colspan="3" style="padding: 0 10px 10px 16px; word-wrap: break-word">
                      <div>
                        <v-btn
                          style="margin-left: -4px; margin-top: 10px"
                          :href="`https://github.com/${r.repoFullName}/releases/tag/${
                            r.tagName || 'v' + r.v
                          }`"
                          target="_blank"
                        >
                          view on github
                        </v-btn>
                      </div>

                      <!-- eslint-disable-next-line vue/no-v-html -->
                      <div class="md" @click="descrClick($event)" v-html="r.descrHtml" />
                    </td>
                  </tr>
                </transition>
              </template>
            </table>
          </template>

          <table
            v-if="!dayLoading && !store.getReleasesCount()"
            border="0"
            cellspacing="0"
            cellpadding="6"
            class="table1"
          >
            <tr>
              <td colspan="3">
                You have 0 releases in last 30 days. Either you have too few starred projects or
                maybe there's a glitch in the system, so check back in 10 minutes.
              </td>
            </tr>
          </table>

          <table
            border="0"
            cellspacing="0"
            cellpadding="6"
            class="table1"
            style="padding: 16px 16px"
          >
            <tr v-if="dayLoading">
              <td colspan="3">loading {{ dayLoading }}...</td>
            </tr>
            <tr v-else>
              <td colspan="3">
                <v-btn color="primary" @click="loadMore()"> load more </v-btn>
              </td>
            </tr>
          </table>
        </div>
      </div>
    </div>
  </div>
</template>

<style lang="scss" scoped>
@import '../scss/var';

.releases {
  padding: 16px;
  // padding-top: 20px;
}

.ver {
  font-family: 'Courier New';
  font-size: 12px;
  font-weight: bold;
  color: #888;
  line-height: 1;
}

.mainTr {
  transition: all 0.3s ease-out;

  #{$active} {
    background-color: rgba(0, 0, 0, 0.05);
    cursor: pointer;
    transition: all 0.1s ease-in;
  }
}

.table1 {
  width: 100% !important;
  max-width: 500px;
  table-layout: fixed;
}

@media (max-width: 800px) {
  .titleRow h1 {
    // color: pink;
    font-size: 24px;
  }
  .titleRow img {
    width: 40px;
    height: 40px;
  }
}

@media (max-width: 500px) {
  .titleRow h1 {
    // color: pink;
    font-size: 18px;
  }
  .titleRow img {
    width: 40px;
    height: 40px;
  }

  .descrRow {
    font-size: 15px;
  }
}
</style>