|
|
|
|
@ -7,6 +7,7 @@
@@ -7,6 +7,7 @@
|
|
|
|
|
import type { NostrEvent } from '../../../lib/types/nostr.js'; |
|
|
|
|
import { nip19 } from 'nostr-tools'; |
|
|
|
|
import { fetchGitRepo, extractGitUrls, type GitRepoInfo, type GitFile } from '../../../lib/services/content/git-repo-fetcher.js'; |
|
|
|
|
import FileExplorer from '../../../lib/components/content/FileExplorer.svelte'; |
|
|
|
|
import { marked } from 'marked'; |
|
|
|
|
import Asciidoctor from 'asciidoctor'; |
|
|
|
|
import { KIND } from '../../../lib/types/kind-lookup.js'; |
|
|
|
|
@ -16,12 +17,15 @@
@@ -16,12 +17,15 @@
|
|
|
|
|
import MarkdownRenderer from '../../../lib/components/content/MarkdownRenderer.svelte'; |
|
|
|
|
import { signAndPublish } from '../../../lib/services/nostr/auth-handler.js'; |
|
|
|
|
import { sessionManager } from '../../../lib/services/auth/session-manager.js'; |
|
|
|
|
import { cacheEvent } from '../../../lib/services/cache/event-cache.js'; |
|
|
|
|
import { cacheEvent, getEventsByKind } from '../../../lib/services/cache/event-cache.js'; |
|
|
|
|
import EventMenu from '../../../lib/components/EventMenu.svelte'; |
|
|
|
|
import { fetchProfiles } from '../../../lib/services/user-data.js'; |
|
|
|
|
|
|
|
|
|
let naddr = $derived($page.params.naddr); |
|
|
|
|
let repoEvent = $state<NostrEvent | null>(null); |
|
|
|
|
let gitRepo = $state<GitRepoInfo | null>(null); |
|
|
|
|
let loading = $state(true); |
|
|
|
|
let loadingGitRepo = $state(false); |
|
|
|
|
let activeTab = $state<'metadata' | 'about' | 'repository' | 'issues' | 'documentation'>('metadata'); |
|
|
|
|
let issues = $state<NostrEvent[]>([]); |
|
|
|
|
let issueComments = $state<Map<string, NostrEvent[]>>(new Map()); |
|
|
|
|
@ -29,28 +33,132 @@
@@ -29,28 +33,132 @@
|
|
|
|
|
let documentationEvents = $state<Map<string, NostrEvent>>(new Map()); |
|
|
|
|
let changingStatus = $state<Map<string, boolean>>(new Map()); // Track which issues are having status changed |
|
|
|
|
let statusFilter = $state<string | null>(null); // Filter issues by status: null = all, 'open', 'resolved', 'closed', 'draft' |
|
|
|
|
let issuesPage = $state(1); // Current page for pagination |
|
|
|
|
const ISSUES_PER_PAGE = 20; // Number of issues to show per page |
|
|
|
|
|
|
|
|
|
const asciidoctor = Asciidoctor(); |
|
|
|
|
|
|
|
|
|
let loadingRepo = $state(false); // Guard to prevent concurrent loads |
|
|
|
|
|
|
|
|
|
onMount(async () => { |
|
|
|
|
await nostrClient.initialize(); |
|
|
|
|
await loadRepo(); |
|
|
|
|
// Don't call loadRepo here - let $effect handle it |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
$effect(() => { |
|
|
|
|
if (naddr) { |
|
|
|
|
if (naddr && !loadingRepo) { |
|
|
|
|
loadCachedRepo(); |
|
|
|
|
loadRepo(); |
|
|
|
|
} |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
// Load git repo when repository tab is clicked |
|
|
|
|
$effect(() => { |
|
|
|
|
if (activeTab === 'repository' && repoEvent && !gitRepo && !loadingGitRepo) { |
|
|
|
|
loadGitRepo(); |
|
|
|
|
} |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
async function loadGitRepo() { |
|
|
|
|
if (!repoEvent || loadingGitRepo || gitRepo) return; |
|
|
|
|
|
|
|
|
|
loadingGitRepo = true; |
|
|
|
|
try { |
|
|
|
|
const gitUrls = extractGitUrls(repoEvent); |
|
|
|
|
console.log('Git URLs found:', gitUrls); |
|
|
|
|
|
|
|
|
|
if (gitUrls.length > 0) { |
|
|
|
|
// Try each URL until one works |
|
|
|
|
for (const url of gitUrls) { |
|
|
|
|
try { |
|
|
|
|
console.log('Attempting to fetch git repo from:', url); |
|
|
|
|
const repo = await fetchGitRepo(url); |
|
|
|
|
if (repo) { |
|
|
|
|
gitRepo = repo; |
|
|
|
|
console.log('Git repo loaded:', repo.name); |
|
|
|
|
break; // Success, stop trying other URLs |
|
|
|
|
} |
|
|
|
|
} catch (error) { |
|
|
|
|
console.warn('Failed to fetch git repo from', url, error); |
|
|
|
|
// Continue to next URL |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} else { |
|
|
|
|
console.log('No git URLs found in repo event'); |
|
|
|
|
} |
|
|
|
|
} catch (error) { |
|
|
|
|
console.error('Error loading git repo:', error); |
|
|
|
|
} finally { |
|
|
|
|
loadingGitRepo = false; |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
async function loadCachedRepo() { |
|
|
|
|
if (!naddr) return; |
|
|
|
|
|
|
|
|
|
try { |
|
|
|
|
// Decode naddr |
|
|
|
|
let decoded; |
|
|
|
|
try { |
|
|
|
|
decoded = nip19.decode(naddr); |
|
|
|
|
} catch (decodeError) { |
|
|
|
|
return; // Can't decode, skip cache check |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
if (decoded.type !== 'naddr') { |
|
|
|
|
return; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
const naddrData = decoded.data as { kind: number; pubkey: string; identifier?: string; relays?: string[] }; |
|
|
|
|
const kind = naddrData.kind; |
|
|
|
|
const pubkey = naddrData.pubkey; |
|
|
|
|
const dTag = naddrData.identifier || ''; |
|
|
|
|
|
|
|
|
|
// Check cache for repo events of this kind |
|
|
|
|
const cachedEvents = await getEventsByKind(kind, 1000); |
|
|
|
|
|
|
|
|
|
// Find the matching repo event (by pubkey and d-tag) |
|
|
|
|
const matchingEvent = cachedEvents.find(event => { |
|
|
|
|
if (event.pubkey !== pubkey) return false; |
|
|
|
|
const eventDTag = event.tags.find(t => t[0] === 'd')?.[1] || ''; |
|
|
|
|
return eventDTag === dTag; |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
if (matchingEvent) { |
|
|
|
|
repoEvent = matchingEvent; |
|
|
|
|
loading = false; // Show cached data immediately |
|
|
|
|
|
|
|
|
|
// Load issues and documentation in background (but not git repo - wait for tab click) |
|
|
|
|
Promise.all([ |
|
|
|
|
loadIssues(), |
|
|
|
|
loadDocumentation() |
|
|
|
|
]).catch(err => { |
|
|
|
|
console.error('Error loading repo data from cache:', err); |
|
|
|
|
}); |
|
|
|
|
} |
|
|
|
|
} catch (error) { |
|
|
|
|
console.error('Error loading cached repo:', error); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
async function loadRepo() { |
|
|
|
|
if (!naddr) { |
|
|
|
|
console.warn('No naddr parameter provided'); |
|
|
|
|
loading = false; |
|
|
|
|
if (!naddr || loadingRepo) { |
|
|
|
|
if (!naddr) { |
|
|
|
|
console.warn('No naddr parameter provided'); |
|
|
|
|
} |
|
|
|
|
if (!repoEvent) { |
|
|
|
|
loading = false; |
|
|
|
|
} |
|
|
|
|
return; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
loading = true; |
|
|
|
|
// Only show loading spinner if we don't have cached data |
|
|
|
|
const hasCachedData = repoEvent !== null; |
|
|
|
|
if (!hasCachedData) { |
|
|
|
|
loading = true; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
loadingRepo = true; |
|
|
|
|
try { |
|
|
|
|
// Decode naddr |
|
|
|
|
let decoded; |
|
|
|
|
@ -58,13 +166,17 @@
@@ -58,13 +166,17 @@
|
|
|
|
|
decoded = nip19.decode(naddr); |
|
|
|
|
} catch (decodeError) { |
|
|
|
|
console.error('Failed to decode naddr:', decodeError); |
|
|
|
|
loading = false; |
|
|
|
|
if (!hasCachedData) { |
|
|
|
|
loading = false; |
|
|
|
|
} |
|
|
|
|
return; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
if (decoded.type !== 'naddr') { |
|
|
|
|
console.error('Invalid naddr type:', decoded.type); |
|
|
|
|
loading = false; |
|
|
|
|
if (!hasCachedData) { |
|
|
|
|
loading = false; |
|
|
|
|
} |
|
|
|
|
return; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
@ -83,6 +195,7 @@
@@ -83,6 +195,7 @@
|
|
|
|
|
|
|
|
|
|
console.log('Using relays:', relays); |
|
|
|
|
|
|
|
|
|
// Step 1: Fetch the repo event by ID (using kind, author, and d-tag) |
|
|
|
|
const events = await nostrClient.fetchEvents( |
|
|
|
|
[{ kinds: [kind], authors: [pubkey], '#d': [dTag], limit: 1 }], |
|
|
|
|
relays, |
|
|
|
|
@ -95,40 +208,30 @@
@@ -95,40 +208,30 @@
|
|
|
|
|
repoEvent = events[0]; |
|
|
|
|
console.log('Repo event loaded:', repoEvent.id); |
|
|
|
|
|
|
|
|
|
// Extract git URLs and fetch repo data |
|
|
|
|
const gitUrls = extractGitUrls(repoEvent); |
|
|
|
|
console.log('Git URLs found:', gitUrls); |
|
|
|
|
if (gitUrls.length > 0) { |
|
|
|
|
// Try each URL until one works |
|
|
|
|
for (const url of gitUrls) { |
|
|
|
|
try { |
|
|
|
|
console.log('Attempting to fetch git repo from:', url); |
|
|
|
|
const repo = await fetchGitRepo(url); |
|
|
|
|
if (repo) { |
|
|
|
|
gitRepo = repo; |
|
|
|
|
console.log('Git repo loaded:', repo.name); |
|
|
|
|
break; |
|
|
|
|
} |
|
|
|
|
} catch (error) { |
|
|
|
|
console.warn('Failed to fetch git repo from', url, error); |
|
|
|
|
// Continue to next URL |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} else { |
|
|
|
|
console.log('No git URLs found in repo event'); |
|
|
|
|
// Don't fetch git repo here - wait until user clicks on repository tab |
|
|
|
|
// This prevents rate limiting from GitHub/GitLab/Gitea |
|
|
|
|
|
|
|
|
|
// Step 2: Batch load all related data in parallel (only if not already loaded from cache) |
|
|
|
|
if (issues.length === 0 && documentationEvents.size === 0) { |
|
|
|
|
await Promise.all([ |
|
|
|
|
loadIssues(), // Batch fetch issues, statuses, comments, and profiles |
|
|
|
|
loadDocumentation() // Load documentation events |
|
|
|
|
]); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// Load issues (events that reference this repo) |
|
|
|
|
await loadIssues(); |
|
|
|
|
// Load documentation events |
|
|
|
|
await loadDocumentation(); |
|
|
|
|
} else { |
|
|
|
|
console.warn('No repo event found for naddr:', naddr); |
|
|
|
|
if (!hasCachedData) { |
|
|
|
|
loading = false; |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} catch (error) { |
|
|
|
|
console.error('Error loading repo:', error); |
|
|
|
|
if (!hasCachedData) { |
|
|
|
|
loading = false; |
|
|
|
|
} |
|
|
|
|
} finally { |
|
|
|
|
loading = false; |
|
|
|
|
loadingRepo = false; |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
@ -139,38 +242,35 @@
@@ -139,38 +242,35 @@
|
|
|
|
|
const gitUrls = extractGitUrls(repoEvent); |
|
|
|
|
const relays = relayManager.getProfileReadRelays(); |
|
|
|
|
|
|
|
|
|
// Fetch kind 1621 issues that reference this repo |
|
|
|
|
const issueEvents: NostrEvent[] = []; |
|
|
|
|
// Batch fetch all issues that reference this repo |
|
|
|
|
const filters: any[] = []; |
|
|
|
|
|
|
|
|
|
// Search for issues that reference the repo event ID |
|
|
|
|
const referencedIssues = await nostrClient.fetchEvents( |
|
|
|
|
[{ '#e': [repoEvent.id], kinds: [KIND.ISSUE], limit: 50 }], |
|
|
|
|
relays, |
|
|
|
|
{ useCache: true, cacheResults: true } |
|
|
|
|
); |
|
|
|
|
|
|
|
|
|
issueEvents.push(...referencedIssues); |
|
|
|
|
filters.push({ '#e': [repoEvent.id], kinds: [KIND.ISSUE], limit: 100 }); |
|
|
|
|
|
|
|
|
|
// Search for issues that reference the repo using 'a' tag (NIP-34 format: kind:pubkey:d-tag) |
|
|
|
|
const dTag = repoEvent.tags.find(t => Array.isArray(t) && t[0] === 'd')?.[1] || ''; |
|
|
|
|
if (dTag) { |
|
|
|
|
const aTagValue = `${repoEvent.kind}:${repoEvent.pubkey}:${dTag}`; |
|
|
|
|
const aTagIssues = await nostrClient.fetchEvents( |
|
|
|
|
[{ '#a': [aTagValue], kinds: [KIND.ISSUE], limit: 50 }], |
|
|
|
|
relays, |
|
|
|
|
{ useCache: true, cacheResults: true } |
|
|
|
|
); |
|
|
|
|
issueEvents.push(...aTagIssues); |
|
|
|
|
filters.push({ '#a': [aTagValue], kinds: [KIND.ISSUE], limit: 100 }); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// Also search for issues with git URLs in tags |
|
|
|
|
for (const url of gitUrls) { |
|
|
|
|
const urlIssues = await nostrClient.fetchEvents( |
|
|
|
|
[{ '#r': [url], kinds: [KIND.ISSUE], limit: 20 }], |
|
|
|
|
relays, |
|
|
|
|
{ useCache: true, cacheResults: true } |
|
|
|
|
); |
|
|
|
|
issueEvents.push(...urlIssues); |
|
|
|
|
// Also search for issues with git URLs in tags (batch all URLs in one filter) |
|
|
|
|
if (gitUrls.length > 0) { |
|
|
|
|
filters.push({ '#r': gitUrls, kinds: [KIND.ISSUE], limit: 100 }); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// Batch fetch all issues in parallel |
|
|
|
|
const issueEventsArrays = await Promise.all( |
|
|
|
|
filters.map(filter => |
|
|
|
|
nostrClient.fetchEvents([filter], relays, { useCache: true, cacheResults: true }) |
|
|
|
|
) |
|
|
|
|
); |
|
|
|
|
|
|
|
|
|
// Flatten and deduplicate |
|
|
|
|
const issueEvents: NostrEvent[] = []; |
|
|
|
|
for (const events of issueEventsArrays) { |
|
|
|
|
issueEvents.push(...events); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// Deduplicate and sort |
|
|
|
|
@ -179,9 +279,12 @@
@@ -179,9 +279,12 @@
|
|
|
|
|
|
|
|
|
|
console.log(`Loaded ${issues.length} issues for repo ${repoEvent.id}`); |
|
|
|
|
|
|
|
|
|
// Load statuses and comments for each issue |
|
|
|
|
await loadIssueStatuses(); |
|
|
|
|
await loadIssueComments(); |
|
|
|
|
// Batch load statuses, comments, and profiles |
|
|
|
|
await Promise.all([ |
|
|
|
|
loadIssueStatuses(), |
|
|
|
|
loadIssueComments(), |
|
|
|
|
loadAllProfiles() |
|
|
|
|
]); |
|
|
|
|
} catch (error) { |
|
|
|
|
console.error('Error loading issues:', error); |
|
|
|
|
} |
|
|
|
|
@ -383,10 +486,29 @@
@@ -383,10 +486,29 @@
|
|
|
|
|
|
|
|
|
|
// Filter issues by status |
|
|
|
|
let filteredIssues = $derived.by(() => { |
|
|
|
|
if (!statusFilter) { |
|
|
|
|
return issues; |
|
|
|
|
let filtered = issues; |
|
|
|
|
if (statusFilter) { |
|
|
|
|
filtered = issues.filter(issue => getCurrentStatus(issue.id) === statusFilter); |
|
|
|
|
} |
|
|
|
|
return filtered; |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
// Paginated issues |
|
|
|
|
let paginatedIssues = $derived.by(() => { |
|
|
|
|
const start = (issuesPage - 1) * ISSUES_PER_PAGE; |
|
|
|
|
const end = start + ISSUES_PER_PAGE; |
|
|
|
|
return filteredIssues.slice(start, end); |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
let totalPages = $derived.by(() => { |
|
|
|
|
return Math.ceil(filteredIssues.length / ISSUES_PER_PAGE); |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
// Reset to page 1 when filter changes |
|
|
|
|
$effect(() => { |
|
|
|
|
if (statusFilter !== null) { |
|
|
|
|
issuesPage = 1; |
|
|
|
|
} |
|
|
|
|
return issues.filter(issue => getCurrentStatus(issue.id) === statusFilter); |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
async function loadIssueComments() { |
|
|
|
|
@ -396,8 +518,9 @@
@@ -396,8 +518,9 @@
|
|
|
|
|
const issueIds = issues.map(i => i.id); |
|
|
|
|
const relays = relayManager.getCommentReadRelays(); |
|
|
|
|
|
|
|
|
|
// Batch fetch all comments for all issues |
|
|
|
|
const comments = await nostrClient.fetchEvents( |
|
|
|
|
[{ '#e': issueIds, kinds: [KIND.COMMENT], limit: 200 }], |
|
|
|
|
[{ '#e': issueIds, kinds: [KIND.COMMENT], limit: 500 }], |
|
|
|
|
relays, |
|
|
|
|
{ useCache: true, cacheResults: true } |
|
|
|
|
); |
|
|
|
|
@ -416,11 +539,57 @@
@@ -416,11 +539,57 @@
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
issueComments = commentsMap; |
|
|
|
|
console.log(`Loaded ${comments.length} comments for ${issueIds.length} issues`); |
|
|
|
|
} catch (error) { |
|
|
|
|
console.error('Error loading issue comments:', error); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
async function loadAllProfiles() { |
|
|
|
|
if (issues.length === 0) return; |
|
|
|
|
|
|
|
|
|
try { |
|
|
|
|
// Collect all unique pubkeys from: |
|
|
|
|
// 1. Issue authors |
|
|
|
|
// 2. Comment authors |
|
|
|
|
// 3. Status authors |
|
|
|
|
// 4. Repository owner and maintainers |
|
|
|
|
const pubkeys = new Set<string>(); |
|
|
|
|
|
|
|
|
|
// Add repo owner and maintainers |
|
|
|
|
if (repoEvent) { |
|
|
|
|
pubkeys.add(repoEvent.pubkey); |
|
|
|
|
const maintainers = getMaintainers(); |
|
|
|
|
maintainers.forEach(m => pubkeys.add(m)); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// Add issue authors |
|
|
|
|
issues.forEach(issue => pubkeys.add(issue.pubkey)); |
|
|
|
|
|
|
|
|
|
// Add comment authors |
|
|
|
|
for (const comments of issueComments.values()) { |
|
|
|
|
comments.forEach(comment => pubkeys.add(comment.pubkey)); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// Add status authors |
|
|
|
|
for (const status of issueStatuses.values()) { |
|
|
|
|
pubkeys.add(status.pubkey); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
const uniquePubkeys = Array.from(pubkeys); |
|
|
|
|
console.log(`Batch fetching ${uniquePubkeys.length} profiles`); |
|
|
|
|
|
|
|
|
|
// Batch fetch all profiles at once |
|
|
|
|
const relays = relayManager.getProfileReadRelays(); |
|
|
|
|
await fetchProfiles(uniquePubkeys, relays); |
|
|
|
|
|
|
|
|
|
console.log(`Pre-fetched ${uniquePubkeys.length} profiles`); |
|
|
|
|
} catch (error) { |
|
|
|
|
console.error('Error loading profiles:', error); |
|
|
|
|
// Don't throw - profile loading is best effort |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
async function loadDocumentation() { |
|
|
|
|
if (!repoEvent) return; |
|
|
|
|
|
|
|
|
|
@ -725,9 +894,14 @@
@@ -725,9 +894,14 @@
|
|
|
|
|
</div> |
|
|
|
|
{:else} |
|
|
|
|
<div class="repo-header mb-6"> |
|
|
|
|
<h1 class="text-3xl font-bold text-fog-text dark:text-fog-dark-text font-mono mb-2"> |
|
|
|
|
{getRepoName()} |
|
|
|
|
</h1> |
|
|
|
|
<div class="repo-title-row"> |
|
|
|
|
<h1 class="text-3xl font-bold text-fog-text dark:text-fog-dark-text font-mono mb-2"> |
|
|
|
|
{getRepoName()} |
|
|
|
|
</h1> |
|
|
|
|
{#if repoEvent} |
|
|
|
|
<EventMenu event={repoEvent} showContentActions={true} /> |
|
|
|
|
{/if} |
|
|
|
|
</div> |
|
|
|
|
{#if getRepoDescription()} |
|
|
|
|
<p class="text-fog-text-light dark:text-fog-dark-text-light mb-4"> |
|
|
|
|
{getRepoDescription()} |
|
|
|
|
@ -877,7 +1051,11 @@
@@ -877,7 +1051,11 @@
|
|
|
|
|
</div> |
|
|
|
|
{:else if activeTab === 'repository'} |
|
|
|
|
<div class="repository-tab"> |
|
|
|
|
{#if gitRepo} |
|
|
|
|
{#if loadingGitRepo} |
|
|
|
|
<div class="loading-state"> |
|
|
|
|
<p class="text-fog-text dark:text-fog-dark-text">Loading repository data...</p> |
|
|
|
|
</div> |
|
|
|
|
{:else if gitRepo} |
|
|
|
|
<!-- Branch and Commit Info --> |
|
|
|
|
<div class="repo-info-section mb-6"> |
|
|
|
|
<h2 class="text-xl font-bold text-fog-text dark:text-fog-dark-text mb-4"> |
|
|
|
|
@ -919,9 +1097,7 @@
@@ -919,9 +1097,7 @@
|
|
|
|
|
File Structure |
|
|
|
|
</h2> |
|
|
|
|
{#if gitRepo.files.length > 0} |
|
|
|
|
<div class="file-tree"> |
|
|
|
|
<pre class="file-tree-content">{renderFileTree(getFileTree(gitRepo.files))}</pre> |
|
|
|
|
</div> |
|
|
|
|
<FileExplorer files={gitRepo.files} repoInfo={gitRepo} /> |
|
|
|
|
{:else} |
|
|
|
|
<div class="empty-state"> |
|
|
|
|
<p class="text-fog-text dark:text-fog-dark-text">No files found.</p> |
|
|
|
|
@ -963,7 +1139,7 @@
@@ -963,7 +1139,7 @@
|
|
|
|
|
</div> |
|
|
|
|
<div class="issues-list"> |
|
|
|
|
{#if filteredIssues.length > 0} |
|
|
|
|
{#each filteredIssues as issue} |
|
|
|
|
{#each paginatedIssues as issue} |
|
|
|
|
{@const currentStatus = getCurrentStatus(issue.id)} |
|
|
|
|
{@const isChanging = changingStatus.get(issue.id) || false} |
|
|
|
|
<div class="issue-item"> |
|
|
|
|
@ -1013,6 +1189,31 @@
@@ -1013,6 +1189,31 @@
|
|
|
|
|
{/if} |
|
|
|
|
</div> |
|
|
|
|
{/each} |
|
|
|
|
|
|
|
|
|
<!-- Pagination controls --> |
|
|
|
|
{#if totalPages > 1} |
|
|
|
|
<div class="pagination"> |
|
|
|
|
<button |
|
|
|
|
onclick={() => issuesPage = Math.max(1, issuesPage - 1)} |
|
|
|
|
disabled={issuesPage === 1} |
|
|
|
|
class="pagination-button" |
|
|
|
|
aria-label="Previous page" |
|
|
|
|
> |
|
|
|
|
Previous |
|
|
|
|
</button> |
|
|
|
|
<span class="pagination-info"> |
|
|
|
|
Page {issuesPage} of {totalPages} ({filteredIssues.length} {filteredIssues.length === 1 ? 'issue' : 'issues'}) |
|
|
|
|
</span> |
|
|
|
|
<button |
|
|
|
|
onclick={() => issuesPage = Math.min(totalPages, issuesPage + 1)} |
|
|
|
|
disabled={issuesPage === totalPages} |
|
|
|
|
class="pagination-button" |
|
|
|
|
aria-label="Next page" |
|
|
|
|
> |
|
|
|
|
Next |
|
|
|
|
</button> |
|
|
|
|
</div> |
|
|
|
|
{/if} |
|
|
|
|
{:else} |
|
|
|
|
<div class="empty-state"> |
|
|
|
|
<p class="text-fog-text dark:text-fog-dark-text">No issues found with status "{statusFilter}".</p> |
|
|
|
|
@ -1032,10 +1233,10 @@
@@ -1032,10 +1233,10 @@
|
|
|
|
|
{#each Array.from(documentationEvents.entries()) as [docNaddr, docEvent]} |
|
|
|
|
<div class="documentation-item"> |
|
|
|
|
<div class="doc-header"> |
|
|
|
|
<h3 class="doc-title">Documentation: {docNaddr.slice(0, 20)}...</h3> |
|
|
|
|
<div class="doc-meta"> |
|
|
|
|
<span class="doc-kind">Kind {docEvent.kind}</span> |
|
|
|
|
<a href="/event/{docEvent.id}" class="doc-event-link">View Event</a> |
|
|
|
|
<EventMenu event={docEvent} showContentActions={true} /> |
|
|
|
|
</div> |
|
|
|
|
</div> |
|
|
|
|
<div class="doc-content"> |
|
|
|
|
@ -1084,18 +1285,101 @@
@@ -1084,18 +1285,101 @@
|
|
|
|
|
border-bottom-color: var(--fog-dark-border, #374151); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
.repo-title-row { |
|
|
|
|
display: flex; |
|
|
|
|
align-items: flex-start; |
|
|
|
|
gap: 1rem; |
|
|
|
|
justify-content: space-between; |
|
|
|
|
flex-wrap: wrap; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
.repo-title-row h1 { |
|
|
|
|
flex: 1; |
|
|
|
|
min-width: 0; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.tabs { |
|
|
|
|
display: flex; |
|
|
|
|
gap: 0.5rem; |
|
|
|
|
border-bottom: 1px solid var(--fog-border, #e5e7eb); |
|
|
|
|
flex-wrap: wrap; |
|
|
|
|
overflow-x: auto; |
|
|
|
|
-webkit-overflow-scrolling: touch; |
|
|
|
|
scrollbar-width: thin; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
:global(.dark) .tabs { |
|
|
|
|
border-bottom-color: var(--fog-dark-border, #374151); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
.tab-button { |
|
|
|
|
padding: 0.75rem 1.25rem; |
|
|
|
|
border: none; |
|
|
|
|
border-bottom: 2px solid transparent; |
|
|
|
|
background: transparent; |
|
|
|
|
color: var(--fog-text-light, #6b7280); |
|
|
|
|
font-size: 0.875rem; |
|
|
|
|
font-weight: 500; |
|
|
|
|
cursor: pointer; |
|
|
|
|
transition: all 0.2s; |
|
|
|
|
white-space: nowrap; |
|
|
|
|
min-height: 2.75rem; |
|
|
|
|
display: flex; |
|
|
|
|
align-items: center; |
|
|
|
|
justify-content: center; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
:global(.dark) .tab-button { |
|
|
|
|
color: var(--fog-dark-text-light, #9ca3af); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
.tab-button:hover { |
|
|
|
|
color: var(--fog-text, #1f2937); |
|
|
|
|
background: var(--fog-highlight, #f3f4f6); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
:global(.dark) .tab-button:hover { |
|
|
|
|
color: var(--fog-dark-text, #f9fafb); |
|
|
|
|
background: var(--fog-dark-highlight, #475569); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
.tab-button.active { |
|
|
|
|
color: var(--fog-text, #1f2937); |
|
|
|
|
border-bottom-color: var(--fog-accent, #64748b); |
|
|
|
|
background: var(--fog-highlight, #f3f4f6); |
|
|
|
|
font-weight: 600; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
:global(.dark) .tab-button.active { |
|
|
|
|
color: var(--fog-dark-text, #f9fafb); |
|
|
|
|
border-bottom-color: var(--fog-dark-accent, #94a3b8); |
|
|
|
|
background: var(--fog-dark-highlight, #475569); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
.tab-button:focus { |
|
|
|
|
outline: 2px solid var(--fog-accent, #64748b); |
|
|
|
|
outline-offset: 2px; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
:global(.dark) .tab-button:focus { |
|
|
|
|
outline-color: var(--fog-dark-accent, #94a3b8); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
@media (max-width: 640px) { |
|
|
|
|
.tabs { |
|
|
|
|
gap: 0.25rem; |
|
|
|
|
padding-bottom: 0.5rem; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
.tab-button { |
|
|
|
|
padding: 0.5rem 0.75rem; |
|
|
|
|
font-size: 0.8125rem; |
|
|
|
|
min-height: 2.5rem; |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.tab-content { |
|
|
|
|
margin-top: 2rem; |
|
|
|
|
@ -1278,30 +1562,6 @@
@@ -1278,30 +1562,6 @@
|
|
|
|
|
color: var(--fog-dark-text-light, #9ca3af); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
.file-tree { |
|
|
|
|
padding: 1rem; |
|
|
|
|
border: 1px solid var(--fog-border, #e5e7eb); |
|
|
|
|
border-radius: 0.5rem; |
|
|
|
|
background: var(--fog-post, #ffffff); |
|
|
|
|
overflow-x: auto; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
:global(.dark) .file-tree { |
|
|
|
|
border-color: var(--fog-dark-border, #374151); |
|
|
|
|
background: var(--fog-dark-post, #1f2937); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
.file-tree-content { |
|
|
|
|
margin: 0; |
|
|
|
|
font-family: monospace; |
|
|
|
|
font-size: 0.875rem; |
|
|
|
|
color: var(--fog-text, #1f2937); |
|
|
|
|
white-space: pre; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
:global(.dark) .file-tree-content { |
|
|
|
|
color: var(--fog-dark-text, #f9fafb); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
.issues-filter { |
|
|
|
|
display: flex; |
|
|
|
|
@ -1740,7 +2000,7 @@
@@ -1740,7 +2000,7 @@
|
|
|
|
|
|
|
|
|
|
.doc-header { |
|
|
|
|
display: flex; |
|
|
|
|
justify-content: space-between; |
|
|
|
|
justify-content: flex-end; |
|
|
|
|
align-items: center; |
|
|
|
|
margin-bottom: 1rem; |
|
|
|
|
padding-bottom: 1rem; |
|
|
|
|
@ -1751,17 +2011,6 @@
@@ -1751,17 +2011,6 @@
|
|
|
|
|
border-bottom-color: var(--fog-dark-border, #374151); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
.doc-title { |
|
|
|
|
font-size: 1.25rem; |
|
|
|
|
font-weight: 600; |
|
|
|
|
color: var(--fog-text, #1f2937); |
|
|
|
|
margin: 0; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
:global(.dark) .doc-title { |
|
|
|
|
color: var(--fog-dark-text, #f9fafb); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
.doc-meta { |
|
|
|
|
display: flex; |
|
|
|
|
gap: 1rem; |
|
|
|
|
@ -1769,6 +2018,18 @@
@@ -1769,6 +2018,18 @@
|
|
|
|
|
font-size: 0.875rem; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
@media (max-width: 640px) { |
|
|
|
|
.doc-header { |
|
|
|
|
justify-content: flex-start; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
.doc-meta { |
|
|
|
|
flex-direction: column; |
|
|
|
|
align-items: flex-start; |
|
|
|
|
gap: 0.5rem; |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
.doc-kind { |
|
|
|
|
color: var(--fog-text-light, #6b7280); |
|
|
|
|
padding: 0.25rem 0.5rem; |
|
|
|
|
@ -1797,4 +2058,59 @@
@@ -1797,4 +2058,59 @@
|
|
|
|
|
.doc-content { |
|
|
|
|
margin-top: 1rem; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
.pagination { |
|
|
|
|
display: flex; |
|
|
|
|
align-items: center; |
|
|
|
|
justify-content: center; |
|
|
|
|
gap: 1rem; |
|
|
|
|
margin-top: 2rem; |
|
|
|
|
padding: 1rem; |
|
|
|
|
border-top: 1px solid var(--fog-border, #e5e7eb); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
:global(.dark) .pagination { |
|
|
|
|
border-top-color: var(--fog-dark-border, #374151); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
.pagination-button { |
|
|
|
|
padding: 0.5rem 1rem; |
|
|
|
|
border: 1px solid var(--fog-border, #e5e7eb); |
|
|
|
|
border-radius: 0.375rem; |
|
|
|
|
background: var(--fog-post, #ffffff); |
|
|
|
|
color: var(--fog-text, #1f2937); |
|
|
|
|
font-size: 0.875rem; |
|
|
|
|
cursor: pointer; |
|
|
|
|
transition: all 0.2s; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
.pagination-button:hover:not(:disabled) { |
|
|
|
|
background: var(--fog-highlight, #f3f4f6); |
|
|
|
|
border-color: var(--fog-accent, #64748b); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
.pagination-button:disabled { |
|
|
|
|
opacity: 0.5; |
|
|
|
|
cursor: not-allowed; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
:global(.dark) .pagination-button { |
|
|
|
|
border-color: var(--fog-dark-border, #374151); |
|
|
|
|
background: var(--fog-dark-post, #1f2937); |
|
|
|
|
color: var(--fog-dark-text, #f9fafb); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
:global(.dark) .pagination-button:hover:not(:disabled) { |
|
|
|
|
background: var(--fog-dark-highlight, #475569); |
|
|
|
|
border-color: var(--fog-dark-accent, #94a3b8); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
.pagination-info { |
|
|
|
|
font-size: 0.875rem; |
|
|
|
|
color: var(--fog-text-light, #6b7280); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
:global(.dark) .pagination-info { |
|
|
|
|
color: var(--fog-dark-text-light, #9ca3af); |
|
|
|
|
} |
|
|
|
|
</style> |
|
|
|
|
|