Browse Source

add lookups to the create repo page

main
Silberengel 4 weeks ago
parent
commit
33f5dd2324
  1. 204
      src/app.css
  2. 6
      src/lib/services/nostr/user-relays.ts
  3. 583
      src/routes/signup/+page.svelte

204
src/app.css

@ -467,6 +467,210 @@ input:disabled, textarea:disabled, select:disabled { @@ -467,6 +467,210 @@ input:disabled, textarea:disabled, select:disabled {
}
}
/* Lookup Button Styles */
.lookup-button {
padding: 0.5rem 0.75rem;
font-size: 0.9rem;
background: var(--button-secondary);
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
transition: background 0.2s;
white-space: nowrap;
}
.lookup-button:hover:not(:disabled) {
background: var(--button-secondary-hover);
}
.lookup-button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.lookup-error {
margin-top: 0.5rem;
padding: 0.5rem;
background: var(--error-bg);
color: var(--error-text);
border-radius: 4px;
font-size: 0.9rem;
}
.lookup-results {
margin-top: 0.5rem;
border: 1px solid var(--border-color);
border-radius: 4px;
background: var(--bg-secondary);
max-height: 300px;
overflow-y: auto;
}
.lookup-results-header {
padding: 0.5rem;
background: var(--bg-tertiary);
font-weight: 600;
font-size: 0.9rem;
border-bottom: 1px solid var(--border-color);
display: flex;
justify-content: space-between;
align-items: center;
}
.clear-lookup-button {
background: transparent;
border: none;
color: var(--text-muted);
cursor: pointer;
font-size: 1.2rem;
padding: 0.25rem 0.5rem;
border-radius: 4px;
transition: background 0.2s, color 0.2s;
}
.clear-lookup-button:hover {
background: var(--bg-secondary);
color: var(--text-primary);
}
.lookup-result-item {
padding: 0.75rem;
cursor: pointer;
border-bottom: 1px solid var(--border-light);
transition: background 0.2s;
}
.lookup-result-item:hover {
background: var(--bg-tertiary);
}
.lookup-result-item:last-child {
border-bottom: none;
}
.lookup-result-item strong {
display: block;
margin-bottom: 0.25rem;
color: var(--text-primary);
}
.lookup-result-item small {
display: block;
color: var(--text-muted);
font-family: 'IBM Plex Mono', monospace;
font-size: 0.85rem;
}
.lookup-result-item.repo-result,
.lookup-result-item.profile-result {
padding: 1rem;
}
.result-header {
display: flex;
gap: 1rem;
align-items: flex-start;
}
.result-image {
width: 60px;
height: 60px;
object-fit: cover;
border-radius: 4px;
flex-shrink: 0;
}
.result-avatar {
width: 50px;
height: 50px;
border-radius: 50%;
object-fit: cover;
flex-shrink: 0;
}
.result-avatar-placeholder {
width: 50px;
height: 50px;
border-radius: 50%;
background: var(--accent);
color: white;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.2rem;
font-weight: bold;
flex-shrink: 0;
}
.result-info {
flex: 1;
min-width: 0;
}
.result-info strong {
display: block;
margin-bottom: 0.25rem;
color: var(--text-primary);
font-size: 1rem;
}
.result-description {
margin: 0.5rem 0;
color: var(--text-secondary);
font-size: 0.9rem;
line-height: 1.4;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.result-meta {
display: flex;
gap: 1rem;
margin-top: 0.5rem;
flex-wrap: wrap;
}
.result-meta small {
color: var(--text-muted);
font-size: 0.8rem;
}
.d-tag {
display: inline-block;
margin-top: 0.25rem;
padding: 0.125rem 0.5rem;
background: var(--bg-tertiary);
border-radius: 3px;
font-family: 'IBM Plex Mono', monospace;
font-size: 0.8rem;
}
.npub-display {
display: block;
margin-top: 0.5rem;
word-break: break-all;
}
.result-tags {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
margin-top: 0.5rem;
}
.tag-badge {
display: inline-block;
padding: 0.125rem 0.5rem;
background: var(--accent-light);
color: var(--accent);
border-radius: 12px;
font-size: 0.75rem;
font-weight: 500;
}
.search-input {
width: 100%;
padding: 0.75rem;

6
src/lib/services/nostr/user-relays.ts

@ -16,16 +16,18 @@ export async function getUserRelays( @@ -16,16 +16,18 @@ export async function getUserRelays(
const outbox: string[] = [];
try {
// Fetch kind 10002 (relay list)
// Fetch kind 10002 (relay list) - get multiple to find the newest
const relayListEvents = await nostrClient.fetchEvents([
{
kinds: [KIND.RELAY_LIST],
authors: [pubkey],
limit: 1
limit: 10 // Get multiple to ensure we find the newest
}
]);
if (relayListEvents.length > 0) {
// Sort by created_at descending to get the newest event first
relayListEvents.sort((a, b) => b.created_at - a.created_at);
const event = relayListEvents[0];
for (const tag of event.tags) {
if (tag[0] === 'relay' && tag[1]) {

583
src/routes/signup/+page.svelte

@ -40,9 +40,15 @@ @@ -40,9 +40,15 @@
let previewLoading = $state(false);
let previewTimeout: ReturnType<typeof setTimeout> | null = null;
import { DEFAULT_NOSTR_RELAYS, combineRelays } from '../../lib/config.js';
// Lookup state
let lookupLoading = $state<{ [key: string]: boolean }>({});
let lookupError = $state<{ [key: string]: string | null }>({});
let lookupResults = $state<{ [key: string]: any }>({});
import { DEFAULT_NOSTR_RELAYS, DEFAULT_NOSTR_SEARCH_RELAYS, combineRelays } from '../../lib/config.js';
const nostrClient = new NostrClient(DEFAULT_NOSTR_RELAYS);
const searchClient = new NostrClient(DEFAULT_NOSTR_SEARCH_RELAYS);
onMount(() => {
nip07Available = isNIP07Available();
@ -185,6 +191,313 @@ @@ -185,6 +191,313 @@
}
}
// Validation functions
function validateCloneUrl(url: string): string | null {
if (!url.trim()) return null; // Empty is OK
if (!isValidUrl(url.trim())) {
return 'Invalid URL format. Must start with http:// or https://';
}
if (!url.trim().endsWith('.git') && !url.trim().includes('/')) {
return 'Clone URL should end with .git or be a valid repository URL';
}
return null;
}
function validateWebUrl(url: string): string | null {
if (!url.trim()) return null; // Empty is OK
if (!isValidUrl(url.trim())) {
return 'Invalid URL format. Must start with http:// or https://';
}
return null;
}
function validateMaintainer(maintainer: string): string | null {
if (!maintainer.trim()) return null; // Empty is OK
// Check if it's a valid npub or hex pubkey
try {
if (maintainer.startsWith('npub')) {
nip19.decode(maintainer);
return null;
} else if (maintainer.length === 64 && /^[0-9a-f]+$/i.test(maintainer)) {
return null; // Valid hex pubkey
} else {
return 'Invalid maintainer format. Use npub1... or 64-character hex pubkey';
}
} catch {
return 'Invalid maintainer format. Use npub1... or 64-character hex pubkey';
}
}
function validateDocumentation(doc: string): string | null {
if (!doc.trim()) return null; // Empty is OK
// Check if it's in naddr format or 30618:pubkey:identifier format
if (doc.startsWith('naddr')) {
try {
const decoded = nip19.decode(doc);
if (decoded.type === 'naddr') return null;
} catch {
return 'Invalid naddr format';
}
} else if (/^\d+:[0-9a-f]{64}:[a-zA-Z0-9_-]+$/.test(doc)) {
return null; // Valid kind:pubkey:identifier format
}
return 'Invalid documentation format. Use naddr1... or kind:pubkey:identifier';
}
function validateImageUrl(url: string): string | null {
if (!url.trim()) return null; // Empty is OK
if (!isValidUrl(url.trim())) {
return 'Invalid URL format. Must start with http:// or https://';
}
// Check if it's likely an image URL
const imageExtensions = ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.svg'];
const lowerUrl = url.toLowerCase();
if (!imageExtensions.some(ext => lowerUrl.includes(ext)) && !lowerUrl.includes('image') && !lowerUrl.includes('img')) {
return 'Warning: URL does not appear to be an image';
}
return null;
}
// Lookup functions
// Use only default search relays for lookups to avoid connecting to random/unreachable user relays
function getSearchRelays(): string[] {
return DEFAULT_NOSTR_SEARCH_RELAYS;
}
async function lookupRepoAnnouncement(query: string, fieldName: string) {
const lookupKey = `repo-${fieldName}`;
lookupLoading[lookupKey] = true;
lookupError[lookupKey] = null;
lookupResults[lookupKey] = null;
try {
const relays = await getSearchRelays();
const client = new NostrClient(relays);
// Try to decode as naddr, nevent, or hex
const decoded = decodeNostrAddress(query.trim());
let events: NostrEvent[] = [];
if (decoded) {
if (decoded.type === 'note' && decoded.id) {
events = await client.fetchEvents([{ ids: [decoded.id], limit: 10 }]);
} else if (decoded.type === 'nevent' && decoded.id) {
events = await client.fetchEvents([{ ids: [decoded.id], limit: 10 }]);
} else if (decoded.type === 'naddr' && decoded.pubkey && decoded.kind && decoded.identifier) {
events = await client.fetchEvents([
{
kinds: [decoded.kind],
authors: [decoded.pubkey],
'#d': [decoded.identifier],
limit: 10
}
]);
}
}
// Also search by name or d-tag if query doesn't look like an address
if (events.length === 0 && !query.startsWith('naddr') && !query.startsWith('nevent') && !/^[0-9a-f]{64}$/i.test(query)) {
const repoEvents = await client.fetchEvents([
{
kinds: [KIND.REPO_ANNOUNCEMENT],
limit: 20
}
]);
const searchLower = query.toLowerCase();
events = repoEvents.filter(event => {
const name = event.tags.find(t => t[0] === 'name')?.[1] || '';
const dTag = event.tags.find(t => t[0] === 'd')?.[1] || '';
return name.toLowerCase().includes(searchLower) || dTag.toLowerCase().includes(searchLower);
});
}
if (events.length === 0) {
lookupError[lookupKey] = 'No repository announcements found';
} else {
lookupResults[lookupKey] = events;
}
} catch (err) {
lookupError[lookupKey] = `Lookup failed: ${String(err)}`;
} finally {
lookupLoading[lookupKey] = false;
}
}
async function lookupNpub(query: string, fieldName: string, index?: number) {
const lookupKey = index !== undefined ? `npub-${fieldName}-${index}` : `npub-${fieldName}`;
lookupLoading[lookupKey] = true;
lookupError[lookupKey] = null;
lookupResults[lookupKey] = null;
try {
// Try to decode as npub
let pubkey: string | null = null;
try {
if (query.startsWith('npub')) {
const decoded = nip19.decode(query);
if (decoded.type === 'npub') {
pubkey = decoded.data as string;
}
} else if (query.length === 64 && /^[0-9a-f]+$/i.test(query)) {
pubkey = query;
}
} catch {
// Invalid format
}
if (!pubkey) {
// Search for npubs by name (requires kind 0 metadata)
const relays = getSearchRelays();
const client = new NostrClient(relays);
// Search for profiles
const profileEvents = await client.fetchEvents([
{
kinds: [0], // Metadata events
limit: 20
}
]);
const searchLower = query.toLowerCase();
const matches = profileEvents.filter(event => {
try {
const content = JSON.parse(event.content);
const name = content.name || content.display_name || '';
return name.toLowerCase().includes(searchLower);
} catch {
return false;
}
});
if (matches.length > 0) {
lookupResults[lookupKey] = matches.map(e => {
try {
const content = JSON.parse(e.content);
return {
pubkey: e.pubkey,
npub: nip19.npubEncode(e.pubkey),
name: content.name || content.display_name || 'Unknown',
about: content.about || '',
picture: content.picture || ''
};
} catch {
return {
pubkey: e.pubkey,
npub: nip19.npubEncode(e.pubkey),
name: 'Unknown'
};
}
});
} else {
lookupError[lookupKey] = 'No profiles found matching the query';
}
} else {
// Valid pubkey, try to fetch profile
const relays = getSearchRelays();
const client = new NostrClient(relays);
const profileEvents = await client.fetchEvents([
{
kinds: [0],
authors: [pubkey],
limit: 1
}
]);
let profileData: any = {
pubkey,
npub: query.startsWith('npub') ? query : nip19.npubEncode(pubkey)
};
if (profileEvents.length > 0) {
try {
const content = JSON.parse(profileEvents[0].content);
profileData.name = content.name || content.display_name || '';
profileData.about = content.about || '';
profileData.picture = content.picture || '';
} catch {
// Invalid JSON, use defaults
}
}
lookupResults[lookupKey] = [profileData];
}
} catch (err) {
lookupError[lookupKey] = `Lookup failed: ${String(err)}`;
} finally {
lookupLoading[lookupKey] = false;
}
}
async function lookupNevent(query: string, fieldName: string) {
const lookupKey = `nevent-${fieldName}`;
lookupLoading[lookupKey] = true;
lookupError[lookupKey] = null;
lookupResults[lookupKey] = null;
try {
const relays = await getSearchRelays();
const client = new NostrClient(relays);
let events: NostrEvent[] = [];
const decoded = decodeNostrAddress(query.trim());
if (decoded && decoded.id) {
events = await client.fetchEvents([{ ids: [decoded.id], limit: 10 }]);
} else if (/^[0-9a-f]{64}$/i.test(query.trim())) {
// Hex event ID
events = await client.fetchEvents([{ ids: [query.trim()], limit: 10 }]);
}
if (events.length === 0) {
lookupError[lookupKey] = 'No events found';
} else {
lookupResults[lookupKey] = events;
}
} catch (err) {
lookupError[lookupKey] = `Lookup failed: ${String(err)}`;
} finally {
lookupLoading[lookupKey] = false;
}
}
function selectRepoResult(result: NostrEvent, fieldName: string) {
if (fieldName === 'existingRepoRef') {
existingRepoRef = result.id;
loadExistingRepo();
} else if (fieldName === 'forkOriginalRepo') {
// Convert to naddr format if possible
const dTag = result.tags.find(t => t[0] === 'd')?.[1];
if (dTag) {
try {
const naddr = nip19.naddrEncode({
pubkey: result.pubkey,
kind: result.kind,
identifier: dTag,
relays: []
});
forkOriginalRepo = naddr;
} catch {
forkOriginalRepo = `${result.kind}:${result.pubkey}:${dTag}`;
}
}
}
lookupResults[`repo-${fieldName}`] = null;
}
function selectNpubResult(result: { pubkey: string; npub: string; name?: string; about?: string; picture?: string }, fieldName: string, index?: number) {
if (fieldName === 'maintainers' && index !== undefined) {
updateMaintainer(index, result.npub);
}
const lookupKey = index !== undefined ? `npub-${fieldName}-${index}` : `npub-${fieldName}`;
lookupResults[lookupKey] = null;
}
function clearLookupResults(key: string) {
lookupResults[key] = null;
lookupError[key] = null;
}
async function loadExistingRepo() {
if (!existingRepoRef.trim()) return;
@ -360,6 +673,61 @@ @@ -360,6 +673,61 @@
return;
}
// Validate all fields
const validationErrors: string[] = [];
// Validate clone URLs
for (let i = 0; i < cloneUrls.length; i++) {
const urlError = validateCloneUrl(cloneUrls[i]);
if (urlError) {
validationErrors.push(`Clone URL ${i + 1}: ${urlError}`);
}
}
// Validate web URLs
for (let i = 0; i < webUrls.length; i++) {
const urlError = validateWebUrl(webUrls[i]);
if (urlError) {
validationErrors.push(`Web URL ${i + 1}: ${urlError}`);
}
}
// Validate maintainers
for (let i = 0; i < maintainers.length; i++) {
const maintainerError = validateMaintainer(maintainers[i]);
if (maintainerError) {
validationErrors.push(`Maintainer ${i + 1}: ${maintainerError}`);
}
}
// Validate documentation
for (let i = 0; i < documentation.length; i++) {
const docError = validateDocumentation(documentation[i]);
if (docError) {
validationErrors.push(`Documentation ${i + 1}: ${docError}`);
}
}
// Validate image URLs
if (imageUrl.trim()) {
const imageError = validateImageUrl(imageUrl);
if (imageError) {
validationErrors.push(`Image URL: ${imageError}`);
}
}
if (bannerUrl.trim()) {
const bannerError = validateImageUrl(bannerUrl);
if (bannerError) {
validationErrors.push(`Banner URL: ${bannerError}`);
}
}
if (validationErrors.length > 0) {
error = 'Validation errors:\n' + validationErrors.join('\n');
return;
}
loading = true;
error = null;
@ -537,9 +905,12 @@ @@ -537,9 +905,12 @@
// Sign with NIP-07
const signedEvent = await signEventWithNIP07(eventTemplate);
// Get user's inbox/outbox relays (from kind 3 or 10002)
// Get user's inbox/outbox relays (from kind 10002) using full relay set to find newest
const { getUserRelays } = await import('../../lib/services/nostr/user-relays.js');
const { inbox, outbox } = await getUserRelays(pubkey, nostrClient);
// Use comprehensive relay set to ensure we get the newest kind 10002 event
const fullRelaySet = combineRelays([], [...DEFAULT_NOSTR_SEARCH_RELAYS, ...DEFAULT_NOSTR_RELAYS]);
const fullRelayClient = new NostrClient(fullRelaySet);
const { inbox, outbox } = await getUserRelays(pubkey, fullRelayClient);
// Combine user's outbox with default relays
const userRelays = combineRelays(outbox);
@ -614,6 +985,15 @@ @@ -614,6 +985,15 @@
placeholder="hex event ID, nevent1..., or naddr1..."
disabled={loading || loadingExisting}
/>
<button
type="button"
onclick={() => lookupRepoAnnouncement(existingRepoRef || '', 'existingRepoRef')}
disabled={loading || loadingExisting || !existingRepoRef.trim()}
class="lookup-button"
title="Search for repository announcements (supports hex ID, nevent, naddr, or search by name)"
>
{lookupLoading['repo-existingRepoRef'] ? '🔍...' : '🔍'}
</button>
<button
type="button"
onclick={loadExistingRepo}
@ -622,6 +1002,70 @@ @@ -622,6 +1002,70 @@
{loadingExisting ? 'Loading...' : 'Load'}
</button>
</div>
{#if lookupError['repo-existingRepoRef']}
<div class="lookup-error">{lookupError['repo-existingRepoRef']}</div>
{/if}
{#if lookupResults['repo-existingRepoRef']}
<div class="lookup-results">
<div class="lookup-results-header">
<span>Found {lookupResults['repo-existingRepoRef'].length} repository announcement(s):</span>
<button
type="button"
onclick={() => clearLookupResults('repo-existingRepoRef')}
class="clear-lookup-button"
title="Clear results"
>
</button>
</div>
{#each lookupResults['repo-existingRepoRef'] as result}
{@const nameTag = result.tags.find((t: string[]) => t[0] === 'name')?.[1]}
{@const dTag = result.tags.find((t: string[]) => t[0] === 'd')?.[1]}
{@const descTag = result.tags.find((t: string[]) => t[0] === 'description')?.[1]}
{@const imageTag = result.tags.find((t: string[]) => t[0] === 'image')?.[1]}
{@const ownerNpub = nip19.npubEncode(result.pubkey)}
{@const tags = result.tags.filter((t: string[]) => t[0] === 't' && t[1] && t[1] !== 'private' && t[1] !== 'fork').map((t: string[]) => t[1])}
<div
class="lookup-result-item repo-result"
role="button"
tabindex="0"
onclick={() => selectRepoResult(result, 'existingRepoRef')}
onkeydown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
selectRepoResult(result, 'existingRepoRef');
}
}}
>
<div class="result-header">
{#if imageTag}
<img src={imageTag} alt="" class="result-image" />
{/if}
<div class="result-info">
<strong>{nameTag || dTag || 'Unnamed'}</strong>
{#if dTag}
<small class="d-tag">d-tag: {dTag}</small>
{/if}
{#if descTag}
<p class="result-description">{descTag}</p>
{/if}
<div class="result-meta">
<small>Owner: {ownerNpub.slice(0, 16)}...</small>
<small>Event: {result.id.slice(0, 16)}...</small>
</div>
{#if tags.length > 0}
<div class="result-tags">
{#each tags as tag}
<span class="tag-badge">#{tag}</span>
{/each}
</div>
{/if}
</div>
</div>
</div>
{/each}
</div>
{/if}
</div>
<div class="form-group">
@ -756,6 +1200,15 @@ @@ -756,6 +1200,15 @@
placeholder="npub1abc... or hex pubkey"
disabled={loading}
/>
<button
type="button"
onclick={() => lookupNpub(maintainer || '', 'maintainers', index)}
disabled={loading || !maintainer.trim()}
class="lookup-button"
title="Lookup npub or search by name"
>
{lookupLoading[`npub-maintainers-${index}`] ? '🔍...' : '🔍'}
</button>
{#if maintainers.length > 1}
<button
type="button"
@ -766,6 +1219,55 @@ @@ -766,6 +1219,55 @@
</button>
{/if}
</div>
{#if lookupError[`npub-maintainers-${index}`]}
<div class="lookup-error">{lookupError[`npub-maintainers-${index}`]}</div>
{/if}
{#if lookupResults[`npub-maintainers-${index}`]}
<div class="lookup-results">
<div class="lookup-results-header">
<span>Found {lookupResults[`npub-maintainers-${index}`].length} profile(s):</span>
<button
type="button"
onclick={() => clearLookupResults(`npub-maintainers-${index}`)}
class="clear-lookup-button"
title="Clear results"
>
</button>
</div>
{#each lookupResults[`npub-maintainers-${index}`] as result}
<div
class="lookup-result-item profile-result"
role="button"
tabindex="0"
onclick={() => selectNpubResult(result, 'maintainers', index)}
onkeydown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
selectNpubResult(result, 'maintainers', index);
}
}}
>
<div class="result-header">
{#if result.picture}
<img src={result.picture} alt="" class="result-avatar" />
{:else}
<div class="result-avatar-placeholder">
{(result.name || result.npub).slice(0, 2).toUpperCase()}
</div>
{/if}
<div class="result-info">
<strong>{result.name || 'Unknown'}</strong>
{#if result.about}
<p class="result-description">{result.about}</p>
{/if}
<small class="npub-display">{result.npub}</small>
</div>
</div>
</div>
{/each}
</div>
{/if}
{/each}
<button
type="button"
@ -926,6 +1428,7 @@ @@ -926,6 +1428,7 @@
• npub/repo format: npub1abc.../repo-name<br/>
• Repository address: 30617:owner-pubkey:repo-name</small>
</label>
<div class="input-group">
<input
id="fork-original-repo"
type="text"
@ -934,6 +1437,80 @@ @@ -934,6 +1437,80 @@
required={isFork}
disabled={loading}
/>
<button
type="button"
onclick={() => lookupRepoAnnouncement(forkOriginalRepo || '', 'forkOriginalRepo')}
disabled={loading || !forkOriginalRepo.trim()}
class="lookup-button"
title="Search for repository announcements"
>
{lookupLoading['repo-forkOriginalRepo'] ? '🔍...' : '🔍'}
</button>
</div>
{#if lookupError['repo-forkOriginalRepo']}
<div class="lookup-error">{lookupError['repo-forkOriginalRepo']}</div>
{/if}
{#if lookupResults['repo-forkOriginalRepo']}
<div class="lookup-results">
<div class="lookup-results-header">
<span>Found {lookupResults['repo-forkOriginalRepo'].length} repository announcement(s):</span>
<button
type="button"
onclick={() => clearLookupResults('repo-forkOriginalRepo')}
class="clear-lookup-button"
title="Clear results"
>
</button>
</div>
{#each lookupResults['repo-forkOriginalRepo'] as result}
{@const nameTag = result.tags.find((t: string[]) => t[0] === 'name')?.[1]}
{@const dTag = result.tags.find((t: string[]) => t[0] === 'd')?.[1]}
{@const descTag = result.tags.find((t: string[]) => t[0] === 'description')?.[1]}
{@const imageTag = result.tags.find((t: string[]) => t[0] === 'image')?.[1]}
{@const ownerNpub = nip19.npubEncode(result.pubkey)}
{@const tags = result.tags.filter((t: string[]) => t[0] === 't' && t[1] && t[1] !== 'private' && t[1] !== 'fork').map((t: string[]) => t[1])}
<div
class="lookup-result-item repo-result"
role="button"
tabindex="0"
onclick={() => selectRepoResult(result, 'forkOriginalRepo')}
onkeydown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
selectRepoResult(result, 'forkOriginalRepo');
}
}}
>
<div class="result-header">
{#if imageTag}
<img src={imageTag} alt="" class="result-image" />
{/if}
<div class="result-info">
<strong>{nameTag || dTag || 'Unnamed'}</strong>
{#if dTag}
<small class="d-tag">d-tag: {dTag}</small>
{/if}
{#if descTag}
<p class="result-description">{descTag}</p>
{/if}
<div class="result-meta">
<small>Owner: {ownerNpub.slice(0, 16)}...</small>
<small>Event: {result.id.slice(0, 16)}...</small>
</div>
{#if tags.length > 0}
<div class="result-tags">
{#each tags as tag}
<span class="tag-badge">#{tag}</span>
{/each}
</div>
{/if}
</div>
</div>
</div>
{/each}
</div>
{/if}
</div>
{/if}

Loading…
Cancel
Save