mirror of
https://github.com/lukaszraczylo/ghtracker.git
synced 2026-10-15 01:07:40 +00:00
feat(ui): add a progress loading screen and lazy project list
The loading screen shows live scan progress from the server, with a pulsing outline of the dashboard. The server now reports how many repositories the running scan has finished. The project list renders 20 rows first and adds more as the end scrolls into view. Groups on very wide screens fill two balanced columns, the fact figures sit in a row under the health strip, and the headline counts match the rows listed below it.
This commit is contained in:
@@ -1,12 +1,63 @@
|
||||
<script setup lang="ts">
|
||||
import { faCodePullRequest, faCircleDot, faGears } from '@fortawesome/free-solid-svg-icons'
|
||||
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
|
||||
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||
import { ageSince, isZeroTime } from '@/lib/format'
|
||||
import type { Repo } from '@/lib/types'
|
||||
|
||||
defineProps<{ repos: Repo[]; total: number; focused: string | null; nowMs: number }>()
|
||||
const props = defineProps<{ repos: Repo[]; total: number; focused: string | null; nowMs: number }>()
|
||||
defineEmits<{ select: [fullName: string] }>()
|
||||
|
||||
/** Rows rendered at first and added each time the list is scrolled near its end. */
|
||||
const PAGE_SIZE = 20
|
||||
|
||||
const shown = ref(PAGE_SIZE)
|
||||
const visible = computed(() => props.repos.slice(0, shown.value))
|
||||
const hasMore = computed(() => shown.value < props.repos.length)
|
||||
|
||||
const scroller = ref<HTMLElement | null>(null)
|
||||
const sentinel = ref<HTMLElement | null>(null)
|
||||
let observer: IntersectionObserver | null = null
|
||||
|
||||
function loadMore(): void {
|
||||
if (hasMore.value) shown.value += PAGE_SIZE
|
||||
}
|
||||
|
||||
// A new search or filter starts the list over from the top.
|
||||
watch(
|
||||
() => props.repos,
|
||||
() => {
|
||||
shown.value = PAGE_SIZE
|
||||
if (scroller.value) scroller.value.scrollTop = 0
|
||||
},
|
||||
)
|
||||
|
||||
// Observe the end marker; a fresh marker element is rendered while more rows remain.
|
||||
watch(
|
||||
sentinel,
|
||||
(el, prev) => {
|
||||
if (prev) observer?.unobserve(prev)
|
||||
if (el) observer?.observe(el)
|
||||
},
|
||||
{ flush: 'post' },
|
||||
)
|
||||
|
||||
onMounted(async () => {
|
||||
await nextTick()
|
||||
if (typeof IntersectionObserver === 'undefined') {
|
||||
shown.value = props.repos.length
|
||||
return
|
||||
}
|
||||
observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
if (entries.some((e) => e.isIntersecting)) loadMore()
|
||||
},
|
||||
{ root: scroller.value, rootMargin: '200px' },
|
||||
)
|
||||
if (sentinel.value) observer.observe(sentinel.value)
|
||||
})
|
||||
onBeforeUnmount(() => observer?.disconnect())
|
||||
|
||||
const DOT = { crit: 'bg-crit', warn: 'bg-warn', ok: 'bg-ok' } as const
|
||||
const LABEL = { crit: 'Critical', warn: 'Warning', ok: 'Healthy' } as const
|
||||
</script>
|
||||
@@ -28,10 +79,11 @@ const LABEL = { crit: 'Critical', warn: 'Warning', ok: 'Healthy' } as const
|
||||
</p>
|
||||
<ul
|
||||
v-else
|
||||
class="border-border bg-surface divide-border max-h-[70vh] divide-y overflow-y-auto rounded-lg border lg:max-h-[calc(100vh-7rem)]"
|
||||
ref="scroller"
|
||||
class="border-border bg-surface divide-border max-h-[70vh] divide-y overflow-y-auto rounded-lg border [scrollbar-color:var(--border)_transparent] [scrollbar-width:thin] lg:max-h-[calc(100vh-7rem)]"
|
||||
>
|
||||
<li
|
||||
v-for="repo in repos"
|
||||
v-for="repo in visible"
|
||||
:key="repo.fullName"
|
||||
class="px-3 py-2.5"
|
||||
:class="focused === repo.fullName ? 'bg-accent' : ''"
|
||||
@@ -89,6 +141,14 @@ const LABEL = { crit: 'Critical', warn: 'Warning', ok: 'Healthy' } as const
|
||||
</span>
|
||||
</div>
|
||||
</li>
|
||||
<li
|
||||
v-if="hasMore"
|
||||
ref="sentinel"
|
||||
class="text-muted-foreground px-3 py-3 text-center text-xs"
|
||||
data-test="more"
|
||||
>
|
||||
Showing {{ visible.length }} of {{ repos.length }}
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user