Github starred repositories

by Mladen Mihajlovic

HTML

<link rel="stylesheet" href="https://cdn.muicss.com/mui-0.10.3/css/mui.min.css">
<script src="https://unpkg.com/[email protected]/dist/vue.global.js"></script>
<div id="app" class="mui-container">
      <div class="mui-panel  search-form">
        <div class="mui-textfield "><input v-model="username" @keyup.enter="go"><label>Github username</label></div>
        <button @click="go" :disabled="busy" class="mui-btn mui-btn--primary">
          <svg v-if="busy" tooltip="Busy downloading pages..." class="spinner" viewBox="0 0 50 50">
            <circle class="path" cx="25" cy="25" r="20" fill="none" stroke-width="5"></circle>
          </svg><span v-else>Go</span></button>
      </div>
      <div class="mui-panel">
        <a v-if="avatarUrl"><img width="32" :src="avatarUrl" style="float:right"></a>
        <table>
          <tr v-for="repo in repos" :key="repo.id">
            <td><svg aria-hidden="true" height="16" viewBox="0 0 16 16" version="1.1" width="16" data-view-component="true" class="octicon octicon-repo mr-2 color-text-secondary flex-shrink-0">
                <path fill-rule="evenodd" d="M2 2.5A2.5 2.5 0 014.5 0h8.75a.75.75 0 01.75.75v12.5a.75.75 0 01-.75.75h-2.5a.75.75 0 110-1.5h1.75v-2h-8a1 1 0 00-.714 1.7.75.75 0 01-1.072 1.05A2.495 2.495 0 012 11.5v-9zm10.5-1V9h-8c-.356 0-.694.074-1 .208V2.5a1 1 0 011-1h8zM5 12.25v3.25a.25.25 0 00.4.2l1.45-1.087a.25.25 0 01.3 0L8.6 15.7a.25.25 0 00.4-.2v-3.25a.25.25 0 00-.25-.25h-3.5a.25.25 0 00-.25.25z"></path>
              </svg> <a :href="repo.html_url">{{repo.name}}</a> <span v-if="repo.description">&nbsp;- {{ repo.description }}</span></td>
          </tr>
        </table>
      </div>
    </div>

CSS

body {
  background: #eee;
}

.search-form {
  margin-top: 15px;
  display: flex;
}

.search-form .mui-textfield {
  width: 100%;
}

.search-form .mui-btn {
  /* flex: 1; */
}

td {
  line-height: 16px;
  display: flex;
  align-items: center;
}

td svg {
  margin-right: 5px;
  flex-shrink: 0;
  align-self: flex-start;
}

td a {
  align-self: flex-start;
}




/* https://codepen.io/supah/pen/BjYLdW */
.spinner {
  animation: rotate 2s linear infinite;
  z-index: 2;
  vertical-align: middle;
  
  width: 16px;
  height: 16px;
}

.spinner .path {
  stroke: #fff;
  stroke-linecap: round;
  animation: dash 1.5s ease-in-out infinite;
}



@keyframes rotate {
  100% {
    transform: rotate(360deg);
  }
}

@keyframes dash {
  0% {
    stroke-dasharray: 1, 150;
    stroke-dashoffset: 0;
  }

  50% {
    stroke-dasharray: 90, 150;
    stroke-dashoffset: -35;
  }

  100% {
    stroke-dasharray: 90, 150;
    stroke-dashoffset: -124;
  }
}

JavaScript

const ref = Vue.ref
const computed = Vue.computed

Vue.createApp({
  setup() {

    const username = ref(window.localStorage.getItem('username') || '')
    const repos = ref([])
    const busy = ref(false)
    const avatarUrl = computed(() => username.value ? `https://github.com/${username.value}.png` : null)

    const go = async () => {

      busy.value = true

      try {
        let _getMore = true
        let _page = 1

        repos.value = []

        while (_getMore) {
          console.log('fetching page ' + _page)

          const response = await fetch(`https://api.github.com/users/${username.value}/starred?per_page=500&page=${_page}`, {
            headers: {
              'Accept': 'application/vnd.github.v3+json'
            }
          });
          const data = await response.json()

          repos.value = repos.value.concat(data);
          
          const _link = response.headers.get('link');
          _page = _page + 1

          _getMore = _link.includes('last'); // last appears on all BUT the last page
        }
        
        window.localStorage.setItem('username', username.value);
      } catch (e) {
        console.log(e.message)
        repos.value = [{name:'error',description:e.message}]
      }

      busy.value = false
    }

    return {
      username,
      go,
      repos,
      busy,
      avatarUrl
    }
  }
}).mount('#app')