Something went wrong. Try again.
A modern frontend for Github repositories (ironically hosted on Tangled)
Something went wrong. Try again.
5.6 kB · 170 lines
Vue
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170<script setup lang="ts">interface GitHubIssue { id: number; number: number; title: string; body?: string | null; state: string; user: { login: string; avatar_url: string; html_url: string; } | null; labels: Array<{ id: number; name: string; color: string; description?: string; }>; assignees: Array<{ login: string; avatar_url: string; html_url: string; }>; comments: number; created_at: string; updated_at: string; html_url: string;}
const props = defineProps<{ issue: GitHubIssue; owner: string; repo: string;}>();
// Format time relative to nowconst formatTimeAgo = (dateString: string) => { const date = new Date(dateString); const now = new Date(); const diffInSeconds = Math.floor((now.getTime() - date.getTime()) / 1000);
const intervals = [ { label: 'year', seconds: 31536000 }, { label: 'month', seconds: 2592000 }, { label: 'day', seconds: 86400 }, { label: 'hour', seconds: 3600 }, { label: 'minute', seconds: 60 }, ];
for (const interval of intervals) { const count = Math.floor(diffInSeconds / interval.seconds); if (count > 0) { return `${count} ${interval.label}${count !== 1 ? 's' : ''} ago`; } }
return 'just now';};
// Get contrasting text color for label backgroundconst getTextColor = (backgroundColor: string) => { const hex = backgroundColor.replace('#', ''); const r = parseInt(hex.substr(0, 2), 16); const g = parseInt(hex.substr(2, 2), 16); const b = parseInt(hex.substr(4, 2), 16); const brightness = (r * 299 + g * 587 + b * 114) / 1000; return brightness > 128 ? '#000000' : '#ffffff';};</script>
<template> <UCard class="hover:bg-gray-50 dark:hover:bg-gray-800/50 transition-colors duration-200"> <div class="flex items-start gap-3"> <!-- Issue state icon --> <div class="flex-shrink-0 mt-1"> <UIcon :name="issue.state === 'open' ? 'i-heroicons-exclamation-circle' : 'i-heroicons-check-circle'" :class="{ 'text-green-600 dark:text-green-500': issue.state === 'open', 'text-purple-600 dark:text-purple-500': issue.state === 'closed' }" class="w-5 h-5" /> </div>
<div class="flex-1 min-w-0"> <!-- Issue title and number --> <div class="flex items-start gap-2 mb-2"> <NuxtLink :to="issue.html_url" target="_blank" class="font-medium text-gray-900 dark:text-white hover:text-blue-600 dark:hover:text-blue-400 line-clamp-2 flex-1 min-w-0" > {{ issue.title }} </NuxtLink> <span class="text-gray-500 dark:text-gray-400 font-mono text-sm flex-shrink-0"> #{{ issue.number }} </span> </div>
<!-- Issue metadata --> <div class="flex items-center gap-4 text-sm text-gray-500 dark:text-gray-400 mb-3"> <div v-if="issue.user" class="flex items-center gap-1"> <UAvatar :src="issue.user.avatar_url" :alt="issue.user.login" size="2xs" /> <span>{{ issue.user.login }}</span> </div> <div v-else class="flex items-center gap-1"> <UIcon name="i-heroicons-user" class="w-4 h-4" /> <span>Unknown user</span> </div> <span>opened {{ formatTimeAgo(issue.created_at) }}</span> <div v-if="issue.comments > 0" class="flex items-center gap-1"> <UIcon name="i-heroicons-chat-bubble-left" class="w-4 h-4" /> <span>{{ issue.comments }}</span> </div> </div>
<!-- Labels --> <div v-if="issue.labels && issue.labels.length > 0" class="flex flex-wrap gap-1 mb-3"> <span v-for="label in issue.labels.slice(0, 5)" :key="label.id" class="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium" :style="{ backgroundColor: `#${label.color}`, color: getTextColor(`#${label.color}`) }" > {{ label.name }} </span> <span v-if="issue.labels.length > 5" class="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-200" > +{{ issue.labels.length - 5 }} more </span> </div>
<!-- Assignees --> <div v-if="issue.assignees && issue.assignees.length > 0" class="flex items-center gap-2"> <span class="text-xs text-gray-500 dark:text-gray-400">Assigned to:</span> <div class="flex -space-x-2"> <UTooltip v-for="assignee in issue.assignees.slice(0, 3)" :key="assignee.login" :text="assignee.login" > <UAvatar :src="assignee.avatar_url" :alt="assignee.login" size="2xs" class="border-2 border-white dark:border-gray-800" /> </UTooltip> <div v-if="issue.assignees.length > 3" class="flex items-center justify-center w-6 h-6 text-xs font-medium bg-gray-100 text-gray-600 dark:bg-gray-700 dark:text-gray-300 rounded-full border-2 border-white dark:border-gray-800" > +{{ issue.assignees.length - 3 }} </div> </div> </div> </div> </div> </UCard></template>