Browse Source

rearrange repo pages

Nostr-Signature: 9f8b68f36189073807510a2dac268b466629ecbc6b8dca66ba809cbf3a36dab5 573634b648634cbad10f2451776089ea21090d9407f715e83c577b4611ae6edc 911debb546c23038bbf77a57bee089130c7cce3a51f2cfb385c3904ec39bc76b90dc9bef2e8e501824ecff13925523d802b6c916d07fef2718554f4f65e6f4d2
main
Silberengel 3 weeks ago
parent
commit
ff67570739
  1. 1
      nostr/commit-signatures.jsonl
  2. 125
      src/lib/styles/repo.css
  3. 252
      src/routes/repos/[npub]/[repo]/+page.svelte

1
nostr/commit-signatures.jsonl

@ -63,3 +63,4 @@
{"kind":1640,"pubkey":"573634b648634cbad10f2451776089ea21090d9407f715e83c577b4611ae6edc","created_at":1771840660,"tags":[["author","Silberengel","silberengel7@protonmail.com"],["message","bug-fixes"]],"content":"Signed commit: bug-fixes","id":"e96c955f550a94c9c6d1228d2a7e479ced331334aaa4eea84525b362b8484d6e","sig":"1218bd9e449404ccc56c5727e8bdff5db31e37c2053a2d91ba02d214c0988173ba480010e53401661cb439884308a575230a7a12124f8e6d8f058c8a804a42f6"} {"kind":1640,"pubkey":"573634b648634cbad10f2451776089ea21090d9407f715e83c577b4611ae6edc","created_at":1771840660,"tags":[["author","Silberengel","silberengel7@protonmail.com"],["message","bug-fixes"]],"content":"Signed commit: bug-fixes","id":"e96c955f550a94c9c6d1228d2a7e479ced331334aaa4eea84525b362b8484d6e","sig":"1218bd9e449404ccc56c5727e8bdff5db31e37c2053a2d91ba02d214c0988173ba480010e53401661cb439884308a575230a7a12124f8e6d8f058c8a804a42f6"}
{"kind":1640,"pubkey":"573634b648634cbad10f2451776089ea21090d9407f715e83c577b4611ae6edc","created_at":1771845583,"tags":[["author","Silberengel","silberengel7@protonmail.com"],["message","fix search and relay connections"]],"content":"Signed commit: fix search and relay connections","id":"24db15027960b244eb4c8664a3642c64684ebfef8c200250093dd047cd119e7d","sig":"561d15ae39b3bf7a5b8a67539a5cfa19d53cbaca9f904589ab7cb69e568ddf056d0d83ced4830cdfdc0b386f13c4bab930264a0f6144cbb833b187b5d452c4ae"} {"kind":1640,"pubkey":"573634b648634cbad10f2451776089ea21090d9407f715e83c577b4611ae6edc","created_at":1771845583,"tags":[["author","Silberengel","silberengel7@protonmail.com"],["message","fix search and relay connections"]],"content":"Signed commit: fix search and relay connections","id":"24db15027960b244eb4c8664a3642c64684ebfef8c200250093dd047cd119e7d","sig":"561d15ae39b3bf7a5b8a67539a5cfa19d53cbaca9f904589ab7cb69e568ddf056d0d83ced4830cdfdc0b386f13c4bab930264a0f6144cbb833b187b5d452c4ae"}
{"kind":1640,"pubkey":"573634b648634cbad10f2451776089ea21090d9407f715e83c577b4611ae6edc","created_at":1771847704,"tags":[["author","Silberengel","silberengel7@protonmail.com"],["message","bug-fixes"]],"content":"Signed commit: bug-fixes","id":"9566e4e2964d0a7b80cce1889092c4db333f89843b5d68906b3c3c568e4ba57d","sig":"8cf9166c630a8dc21bbc3dfaea4330c80c93bf7bc9e8d5d3be182fb11a3b96ea2e5969f452d3e2b309103b3e7fea8fc1aa6e5908d499d0696e9bfcd3859a8e32"} {"kind":1640,"pubkey":"573634b648634cbad10f2451776089ea21090d9407f715e83c577b4611ae6edc","created_at":1771847704,"tags":[["author","Silberengel","silberengel7@protonmail.com"],["message","bug-fixes"]],"content":"Signed commit: bug-fixes","id":"9566e4e2964d0a7b80cce1889092c4db333f89843b5d68906b3c3c568e4ba57d","sig":"8cf9166c630a8dc21bbc3dfaea4330c80c93bf7bc9e8d5d3be182fb11a3b96ea2e5969f452d3e2b309103b3e7fea8fc1aa6e5908d499d0696e9bfcd3859a8e32"}
{"kind":1640,"pubkey":"573634b648634cbad10f2451776089ea21090d9407f715e83c577b4611ae6edc","created_at":1771849427,"tags":[["author","Silberengel","silberengel7@protonmail.com"],["message","bug-fixes"]],"content":"Signed commit: bug-fixes","id":"1d4e6ff4059b064d7cdd465d623a606cfcc5d0565681a34f6384463d40cc8c71","sig":"f5fe3547289e994ff1a3b191607e76d778d318ca4538e70253406867ecef214c1be437dca373f9a461c9cf2ca2978a581b54a9d323baeb2c91851e9cc6ffbfd6"}

125
src/lib/styles/repo.css

@ -1439,17 +1439,32 @@ span.clone-more {
flex-shrink: 0; flex-shrink: 0;
} }
.issues-content,
.prs-content,
.patches-content, .patches-content,
.discussions-content { .discussions-content,
.tags-content,
.docs-content,
.commits-content {
padding: 1rem; padding: 1rem;
flex: 1; flex: 1;
overflow-y: auto; overflow-y: auto;
} }
.patch-header,
.issue-header {
display: flex;
align-items: center;
width: 100%;
}
.patch-subject, .patch-subject,
.issue-subject,
.discussion-title { .discussion-title {
font-weight: 600; font-weight: 600;
color: var(--text-primary); color: var(--text-primary);
width: 100%;
word-wrap: break-word;
} }
.patch-meta, .patch-meta,
@ -1481,7 +1496,8 @@ span.clone-more {
} }
.patch-meta-detail, .patch-meta-detail,
.discussion-meta-detail { .discussion-meta-detail,
.commit-meta-detail {
display: flex; display: flex;
gap: 1rem; gap: 1rem;
align-items: center; align-items: center;
@ -1489,6 +1505,98 @@ span.clone-more {
color: var(--text-secondary); color: var(--text-secondary);
} }
.commit-detail {
margin-bottom: 1rem;
}
.commit-detail-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 1rem;
}
.commit-detail-header h3 {
margin: 0;
flex: 1;
font-size: 1.25rem;
font-weight: 600;
color: var(--text-primary);
}
.diff-view {
margin-top: 1.5rem;
}
.diff-file {
margin-bottom: 1.5rem;
border: 1px solid var(--border-color);
border-radius: 0.375rem;
overflow: hidden;
}
.diff-file-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.75rem 1rem;
background: var(--bg-secondary);
border-bottom: 1px solid var(--border-color);
}
.diff-file-name {
font-weight: 500;
color: var(--text-primary);
}
.diff-stats {
display: flex;
gap: 0.5rem;
font-size: 0.875rem;
}
.diff-stats .additions {
color: #22c55e;
}
.diff-stats .deletions {
color: #ef4444;
}
.diff-content {
margin: 0;
padding: 0.75rem 1rem;
background: var(--bg-primary);
overflow-x: auto;
font-size: 0.875rem;
line-height: 1.5;
}
.close-button {
padding: 0.25rem 0.5rem;
background: var(--bg-tertiary);
border: 1px solid var(--border-color);
border-radius: 0.25rem;
cursor: pointer;
color: var(--text-primary);
font-size: 1.25rem;
line-height: 1;
transition: background 0.2s ease;
}
.close-button:hover {
background: var(--bg-hover);
}
.patch-description {
font-style: italic;
color: var(--text-primary);
font-size: 0.875rem;
margin-top: 0.5rem;
padding-top: 0.5rem;
border-top: 1px solid var(--border-color);
}
.patch-body { .patch-body {
margin-top: 1rem; margin-top: 1rem;
} }
@ -1531,7 +1639,8 @@ span.clone-more {
} }
.patch-item-button, .patch-item-button,
.discussion-item-button { .discussion-item-button,
.issue-item-button {
width: 100%; width: 100%;
padding: 0.75rem 1rem; padding: 0.75rem 1rem;
text-align: left; text-align: left;
@ -1540,15 +1649,21 @@ span.clone-more {
cursor: pointer; cursor: pointer;
color: inherit; color: inherit;
transition: background 0.2s ease; transition: background 0.2s ease;
display: flex;
flex-direction: column;
gap: 0.5rem;
align-items: flex-start;
} }
.patch-item:not(.selected) .patch-item-button:hover, .patch-item:not(.selected) .patch-item-button:hover,
.discussion-item:not(.selected) .discussion-item-button:hover { .discussion-item:not(.selected) .discussion-item-button:hover,
.issue-item:not(.selected) .issue-item-button:hover {
background: var(--bg-tertiary); background: var(--bg-tertiary);
} }
.patch-item.selected .patch-item-button, .patch-item.selected .patch-item-button,
.discussion-item.selected .discussion-item-button { .discussion-item.selected .discussion-item-button,
.issue-item.selected .issue-item-button {
background: var(--accent); background: var(--accent);
color: var(--text-on-accent); color: var(--text-on-accent);
} }

252
src/routes/repos/[npub]/[repo]/+page.svelte

@ -314,13 +314,16 @@
let announcementEventId = $state<string | null>(null); let announcementEventId = $state<string | null>(null);
// Issues // Issues
let issues = $state<Array<{ id: string; subject: string; content: string; status: string; author: string; created_at: number; kind: number }>>([]); let issues = $state<Array<{ id: string; subject: string; content: string; status: string; author: string; created_at: number; kind: number; tags?: string[][] }>>([]);
let loadingIssues = $state(false); let loadingIssues = $state(false);
let showCreateIssueDialog = $state(false); let showCreateIssueDialog = $state(false);
let newIssueSubject = $state(''); let newIssueSubject = $state('');
let newIssueContent = $state(''); let newIssueContent = $state('');
let newIssueLabels = $state<string[]>(['']); let newIssueLabels = $state<string[]>(['']);
let updatingIssueStatus = $state<Record<string, boolean>>({}); let updatingIssueStatus = $state<Record<string, boolean>>({});
let selectedIssue = $state<string | null>(null);
let issueReplies = $state<Array<{ id: string; content: string; author: string; created_at: number; tags: string[][] }>>([]);
let loadingIssueReplies = $state(false);
// Pull Requests // Pull Requests
let prs = $state<Array<{ id: string; subject: string; content: string; status: string; author: string; created_at: number; commitId?: string; kind: number }>>([]); let prs = $state<Array<{ id: string; subject: string; content: string; status: string; author: string; created_at: number; commitId?: string; kind: number }>>([]);
@ -370,7 +373,7 @@
}); });
// Patches // Patches
let patches = $state<Array<{ id: string; subject: string; content: string; author: string; created_at: number; kind: number }>>([]); let patches = $state<Array<{ id: string; subject: string; content: string; author: string; created_at: number; kind: number; description?: string; tags?: string[][] }>>([]);
let loadingPatches = $state(false); let loadingPatches = $state(false);
let selectedPatch = $state<string | null>(null); let selectedPatch = $state<string | null>(null);
let showCreatePatchDialog = $state(false); let showCreatePatchDialog = $state(false);
@ -3907,6 +3910,9 @@
} }
async function viewDiff(commitHash: string) { async function viewDiff(commitHash: string) {
// Set selected commit immediately so it shows in the right panel
selectedCommit = commitHash;
showDiff = false; // Start with false, will be set to true when diff loads
loadingCommits = true; loadingCommits = true;
error = null; error = null;
try { try {
@ -3922,7 +3928,6 @@
}); });
if (response.ok) { if (response.ok) {
diffData = await response.json(); diffData = await response.json();
selectedCommit = commitHash;
showDiff = true; showDiff = true;
} }
} catch (err) { } catch (err) {
@ -4008,8 +4013,14 @@
status: issue.status || 'open', status: issue.status || 'open',
author: issue.pubkey, author: issue.pubkey,
created_at: issue.created_at, created_at: issue.created_at,
kind: issue.kind || KIND.ISSUE kind: issue.kind || KIND.ISSUE,
tags: issue.tags || []
})); }));
// Auto-select first issue if none selected
if (issues.length > 0 && !selectedIssue) {
selectedIssue = issues[0].id;
loadIssueReplies(issues[0].id);
}
} else { } else {
// Handle non-OK responses // Handle non-OK responses
const errorText = await response.text().catch(() => response.statusText); const errorText = await response.text().catch(() => response.statusText);
@ -4039,6 +4050,32 @@
} }
} }
async function loadIssueReplies(issueId: string) {
loadingIssueReplies = true;
try {
const replies = await nostrClient.fetchEvents([
{
kinds: [KIND.COMMENT],
'#e': [issueId],
limit: 100
}
]) as NostrEvent[];
issueReplies = replies.map(reply => ({
id: reply.id,
content: reply.content,
author: reply.pubkey,
created_at: reply.created_at,
tags: reply.tags || []
})).sort((a, b) => a.created_at - b.created_at);
} catch (err) {
console.error('[Issues] Error loading replies:', err);
issueReplies = [];
} finally {
loadingIssueReplies = false;
}
}
async function createIssue() { async function createIssue() {
if (!newIssueSubject.trim() || !newIssueContent.trim()) { if (!newIssueSubject.trim() || !newIssueContent.trim()) {
alert('Please enter a subject and content'); alert('Please enter a subject and content');
@ -4296,14 +4333,45 @@
}); });
if (response.ok) { if (response.ok) {
const data = await response.json(); const data = await response.json();
patches = data.map((patch: { id: string; tags: string[][]; content: string; pubkey: string; created_at: number; kind?: number }) => ({ patches = data.map((patch: { id: string; tags: string[][]; content: string; pubkey: string; created_at: number; kind?: number }) => {
// Extract subject/title from various sources
let subject = patch.tags.find((t: string[]) => t[0] === 'subject')?.[1];
const description = patch.tags.find((t: string[]) => t[0] === 'description')?.[1];
const alt = patch.tags.find((t: string[]) => t[0] === 'alt')?.[1];
// If no subject tag, try description or alt
if (!subject) {
if (description) {
subject = description.trim();
} else if (alt) {
// Remove "git patch: " prefix if present
subject = alt.replace(/^git patch:\s*/i, '').trim();
} else {
// Try to extract from patch content (git patch format)
const subjectMatch = patch.content.match(/^Subject:\s*\[PATCH[^\]]*\]\s*(.+)$/m);
if (subjectMatch) {
subject = subjectMatch[1].trim();
} else {
// Try simpler Subject: line
const simpleSubjectMatch = patch.content.match(/^Subject:\s*(.+)$/m);
if (simpleSubjectMatch) {
subject = simpleSubjectMatch[1].trim();
}
}
}
}
return {
id: patch.id, id: patch.id,
subject: patch.tags.find((t: string[]) => t[0] === 'subject')?.[1] || 'Untitled', subject: subject || 'Untitled',
content: patch.content, content: patch.content,
author: patch.pubkey, author: patch.pubkey,
created_at: patch.created_at, created_at: patch.created_at,
kind: patch.kind || KIND.PATCH kind: patch.kind || KIND.PATCH,
})); description: description?.trim(),
tags: patch.tags || []
};
});
} }
} catch (err) { } catch (err) {
error = err instanceof Error ? err.message : 'Failed to load patches'; error = err instanceof Error ? err.message : 'Failed to load patches';
@ -4835,22 +4903,6 @@
<img src="/icons/arrow-right.svg" alt="Show content" class="icon-inline" /> <img src="/icons/arrow-right.svg" alt="Show content" class="icon-inline" />
</button> </button>
</div> </div>
{#if tags.length > 0}
<ul class="tag-list">
{#each tags as tag}
{@const tagHash = tag.hash || ''}
{#if tagHash}
<li class="tag-item">
<div class="tag-name">{tag.name}</div>
<div class="tag-hash">{tagHash.slice(0, 7)}</div>
{#if tag.message}
<div class="tag-message">{tag.message}</div>
{/if}
</li>
{/if}
{/each}
</ul>
{/if}
</aside> </aside>
{/if} {/if}
@ -4885,34 +4937,21 @@
{:else if issues.length > 0} {:else if issues.length > 0}
<ul class="issue-list"> <ul class="issue-list">
{#each issues as issue} {#each issues as issue}
<li class="issue-item"> <li class="issue-item" class:selected={selectedIssue === issue.id}>
<button
onclick={() => {
selectedIssue = issue.id;
loadIssueReplies(issue.id);
}}
class="issue-item-button"
>
<div class="issue-header"> <div class="issue-header">
<span class="issue-status" class:open={issue.status === 'open'} class:closed={issue.status === 'closed'} class:resolved={issue.status === 'resolved'}> <span class="issue-status" class:open={issue.status === 'open'} class:closed={issue.status === 'closed'} class:resolved={issue.status === 'resolved'}>
{issue.status} {issue.status}
</span> </span>
<span class="issue-subject">{issue.subject}</span>
</div>
<div class="issue-meta">
<span>#{issue.id.slice(0, 7)}</span>
<span>{new Date(issue.created_at * 1000).toLocaleDateString()}</span>
<EventCopyButton eventId={issue.id} kind={issue.kind} pubkey={issue.author} />
</div> </div>
{#if userPubkeyHex && (isMaintainer || userPubkeyHex === issue.author)} <div class="issue-subject">{issue.subject}</div>
<div class="issue-actions">
{#if issue.status === 'open'}
<button onclick={() => updateIssueStatus(issue.id, issue.author, 'closed')} disabled={updatingIssueStatus[issue.id]} class="issue-action-btn close-btn">
{updatingIssueStatus[issue.id] ? 'Closing...' : 'Close'}
</button>
<button onclick={() => updateIssueStatus(issue.id, issue.author, 'resolved')} disabled={updatingIssueStatus[issue.id]} class="issue-action-btn resolve-btn">
{updatingIssueStatus[issue.id] ? 'Resolving...' : 'Resolve'}
</button> </button>
{:else if issue.status === 'closed' || issue.status === 'resolved'}
<button onclick={() => updateIssueStatus(issue.id, issue.author, 'open')} disabled={updatingIssueStatus[issue.id]} class="issue-action-btn reopen-btn">
{updatingIssueStatus[issue.id] ? 'Reopening...' : 'Reopen'}
</button>
{/if}
</div>
{/if}
</li> </li>
{/each} {/each}
</ul> </ul>
@ -5286,7 +5325,8 @@
</div> </div>
{/if} {/if}
{#if activeTab === 'history' && showDiff} {#if activeTab === 'history'}
<div class="commits-content" class:hide-on-mobile={showLeftPanelOnMobile && activeTab === 'history'}>
<div class="content-header-mobile"> <div class="content-header-mobile">
<button <button
onclick={() => showLeftPanelOnMobile = !showLeftPanelOnMobile} onclick={() => showLeftPanelOnMobile = !showLeftPanelOnMobile}
@ -5296,11 +5336,23 @@
<img src="/icons/arrow-right.svg" alt="Show list" class="icon-inline mobile-toggle-left" /> <img src="/icons/arrow-right.svg" alt="Show list" class="icon-inline mobile-toggle-left" />
</button> </button>
</div> </div>
<div class="diff-view"> {#if selectedCommit}
<div class="diff-header"> {@const commit = commits.find(c => (c.hash || (c as any).sha) === selectedCommit)}
<h3>Diff for commit {selectedCommit?.slice(0, 7)}</h3> {#if commit}
<div class="commit-detail">
<div class="commit-detail-header">
<h3>{commit.message || 'No message'}</h3>
<button onclick={() => { showDiff = false; selectedCommit = null; }} class="close-button">×</button> <button onclick={() => { showDiff = false; selectedCommit = null; }} class="close-button">×</button>
</div> </div>
<div class="commit-meta-detail">
<span>#{selectedCommit.slice(0, 7)}</span>
<span>{commit.author || 'Unknown'}</span>
<span>{commit.date ? new Date(commit.date).toLocaleString() : 'Unknown date'}</span>
</div>
{#if loadingCommits}
<div class="loading">Loading diff...</div>
{:else if showDiff && diffData.length > 0}
<div class="diff-view">
{#each diffData as diff} {#each diffData as diff}
<div class="diff-file"> <div class="diff-file">
<div class="diff-file-header"> <div class="diff-file-header">
@ -5314,22 +5366,27 @@
</div> </div>
{/each} {/each}
</div> </div>
{:else if activeTab === 'history'} {:else if showDiff}
<div class="content-header-mobile"> <div class="empty-state">
<button <p>No diff data available</p>
onclick={() => showLeftPanelOnMobile = !showLeftPanelOnMobile}
class="mobile-toggle-button"
title="Show list"
>
<img src="/icons/arrow-right.svg" alt="Show list" class="icon-inline mobile-toggle-left" />
</button>
</div> </div>
{:else}
<div class="empty-state"> <div class="empty-state">
<p>Select a commit to view its diff</p> <p>Loading diff...</p>
</div>
{/if}
</div>
{/if}
{:else}
<div class="empty-state">
<p>Select a commit from the sidebar to view details</p>
</div>
{/if}
</div> </div>
{/if} {/if}
{#if activeTab === 'tags'} {#if activeTab === 'tags'}
<div class="tags-content" class:hide-on-mobile={showLeftPanelOnMobile && activeTab === 'tags'}>
<div class="content-header-mobile"> <div class="content-header-mobile">
<button <button
onclick={() => showLeftPanelOnMobile = !showLeftPanelOnMobile} onclick={() => showLeftPanelOnMobile = !showLeftPanelOnMobile}
@ -5339,8 +5396,26 @@
<img src="/icons/arrow-right.svg" alt="Show list" class="icon-inline mobile-toggle-left" /> <img src="/icons/arrow-right.svg" alt="Show list" class="icon-inline mobile-toggle-left" />
</button> </button>
</div> </div>
{#if tags.length > 0}
<ul class="tag-list">
{#each tags as tag}
{@const tagHash = tag.hash || ''}
{#if tagHash}
<li class="tag-item">
<div class="tag-name">{tag.name}</div>
<div class="tag-hash">{tagHash.slice(0, 7)}</div>
{#if tag.message}
<div class="tag-message">{tag.message}</div>
{/if}
</li>
{/if}
{/each}
</ul>
{:else}
<div class="empty-state"> <div class="empty-state">
<p>Tags are displayed in the sidebar</p> <p>No tags found</p>
</div>
{/if}
</div> </div>
{/if} {/if}
@ -5368,23 +5443,71 @@
<div class="empty-state"> <div class="empty-state">
<p>No issues found. Create one to get started!</p> <p>No issues found. Create one to get started!</p>
</div> </div>
{:else} {:else if selectedIssue}
{#each issues as issue} {@const issue = issues.find(i => i.id === selectedIssue)}
{#if issue}
<div class="issue-detail"> <div class="issue-detail">
<h3>{issue.subject}</h3> <h3>{issue.subject}</h3>
<div class="issue-meta-detail"> <div class="issue-meta-detail">
<span class="issue-status" class:open={issue.status === 'open'} class:closed={issue.status === 'closed'} class:resolved={issue.status === 'resolved'}> <span class="issue-status" class:open={issue.status === 'open'} class:closed={issue.status === 'closed'} class:resolved={issue.status === 'resolved'}>
{issue.status} {issue.status}
</span> </span>
<span>#{issue.id.slice(0, 7)}</span>
<span>Created {new Date(issue.created_at * 1000).toLocaleString()}</span> <span>Created {new Date(issue.created_at * 1000).toLocaleString()}</span>
<EventCopyButton eventId={issue.id} kind={issue.kind} pubkey={issue.author} />
</div> </div>
<div class="issue-body"> <div class="issue-body">
{@html issue.content.replace(/\n/g, '<br>')} {@html issue.content.replace(/\n/g, '<br>')}
</div> </div>
{#if userPubkeyHex && (isMaintainer || userPubkeyHex === issue.author)}
<div class="issue-actions">
{#if issue.status === 'open'}
<button onclick={() => updateIssueStatus(issue.id, issue.author, 'closed')} disabled={updatingIssueStatus[issue.id]} class="issue-action-btn close-btn">
{updatingIssueStatus[issue.id] ? 'Closing...' : 'Close'}
</button>
<button onclick={() => updateIssueStatus(issue.id, issue.author, 'resolved')} disabled={updatingIssueStatus[issue.id]} class="issue-action-btn resolve-btn">
{updatingIssueStatus[issue.id] ? 'Resolving...' : 'Resolve'}
</button>
{:else if issue.status === 'closed' || issue.status === 'resolved'}
<button onclick={() => updateIssueStatus(issue.id, issue.author, 'open')} disabled={updatingIssueStatus[issue.id]} class="issue-action-btn reopen-btn">
{updatingIssueStatus[issue.id] ? 'Reopening...' : 'Reopen'}
</button>
{/if}
</div>
{/if}
<div class="issue-replies">
<h4>Replies ({issueReplies.length})</h4>
{#if loadingIssueReplies}
<div class="loading">Loading replies...</div>
{:else if issueReplies.length === 0}
<div class="empty-state">
<p>No replies yet.</p>
</div>
{:else}
{#each issueReplies as reply}
<div class="issue-reply">
<div class="reply-header">
<UserBadge pubkey={reply.author} />
<span class="reply-date">{new Date(reply.created_at * 1000).toLocaleString()}</span>
<EventCopyButton eventId={reply.id} kind={KIND.COMMENT} pubkey={reply.author} />
</div>
<div class="reply-body">
{@html reply.content.replace(/\n/g, '<br>')}
</div>
</div> </div>
{/each} {/each}
{/if} {/if}
</div> </div>
</div>
{/if}
{:else}
<div class="empty-state">
<p>Select an issue to view details</p>
</div>
{/if}
</div>
{/if} {/if}
{#if activeTab === 'prs'} {#if activeTab === 'prs'}
@ -5476,6 +5599,9 @@
<span>Created {new Date(patch.created_at * 1000).toLocaleString()}</span> <span>Created {new Date(patch.created_at * 1000).toLocaleString()}</span>
<EventCopyButton eventId={patch.id} kind={patch.kind} pubkey={patch.author} /> <EventCopyButton eventId={patch.id} kind={patch.kind} pubkey={patch.author} />
</div> </div>
{#if patch.description && patch.description !== patch.subject}
<div class="patch-description">{patch.description}</div>
{/if}
<div class="patch-body"> <div class="patch-body">
<pre class="patch-content">{patch.content}</pre> <pre class="patch-content">{patch.content}</pre>
</div> </div>

Loading…
Cancel
Save