Browse Source

bug-fixes

main
Silberengel 4 weeks ago
parent
commit
3f8399e203
  1. 2
      docs/tutorial.md
  2. 14
      src/app.css
  3. 3
      src/routes/docs/nip34/+page.svelte
  4. 20
      src/routes/docs/nip34/spec/+page.server.ts
  5. 169
      src/routes/docs/nip34/spec/+page.svelte
  6. 241
      src/routes/signup/+page.svelte

2
docs/tutorial.md

@ -592,7 +592,7 @@ GitRepublic implements NIP-34 for repository announcements. Key event types:
- **Kind 1621**: Issue - **Kind 1621**: Issue
- **Kind 1641**: Ownership transfer - **Kind 1641**: Ownership transfer
See the [NIP-34 specification](https://github.com/nostr-protocol/nips/blob/master/34.md) for full details. See the [NIP-34 specification](/docs/nip34/spec) for full details.
### NIP-98 HTTP Authentication ### NIP-98 HTTP Authentication

14
src/app.css

@ -1170,8 +1170,10 @@ pre code {
background: var(--bg-secondary); background: var(--bg-secondary);
} }
label.filter-checkbox,
.filter-checkbox { .filter-checkbox {
display: flex; display: flex !important;
flex-direction: row !important;
align-items: center; align-items: center;
gap: 0.5rem; gap: 0.5rem;
cursor: pointer; cursor: pointer;
@ -1180,11 +1182,21 @@ pre code {
font-size: 0.9rem; font-size: 0.9rem;
} }
label.filter-checkbox input[type="checkbox"],
.filter-checkbox input[type="checkbox"] { .filter-checkbox input[type="checkbox"] {
cursor: pointer; cursor: pointer;
width: 1.1rem; width: 1.1rem;
height: 1.1rem; height: 1.1rem;
accent-color: var(--accent); accent-color: var(--accent);
flex-shrink: 0;
margin: 0;
display: block;
}
label.filter-checkbox > span,
.filter-checkbox > span {
display: inline;
line-height: 1.5;
} }
.repos-list { .repos-list {

3
src/routes/docs/nip34/+page.svelte

@ -42,7 +42,8 @@
<div class="container"> <div class="container">
<header> <header>
<h1>GitRepublic Documentation</h1> <h1>GitRepublic Tutorial</h1>
<p class="subtitle">Complete guide to using GitRepublic</p>
</header> </header>
<main class="docs-content"> <main class="docs-content">

20
src/routes/docs/nip34/spec/+page.server.ts

@ -0,0 +1,20 @@
/**
* Server-side loader for NIP-34 specification reference
*/
import { readFile } from 'fs/promises';
import { join } from 'path';
import type { PageServerLoad } from './$types';
import logger from '$lib/services/logger.js';
export const load: PageServerLoad = async () => {
try {
// Read NIP-34 specification from docs/34.md
const filePath = join(process.cwd(), 'docs', '34.md');
const content = await readFile(filePath, 'utf-8');
return { content };
} catch (error) {
logger.error({ error }, 'Error loading NIP-34 specification');
return { content: null, error: 'Failed to load NIP-34 specification' };
}
};

169
src/routes/docs/nip34/spec/+page.svelte

@ -0,0 +1,169 @@
<script lang="ts">
import { page } from '$app/stores';
import { onMount } from 'svelte';
let content = $state('');
let loading = $state(true);
let error = $state<string | null>(null);
onMount(async () => {
try {
const docContent = $page.data.content;
if (docContent) {
const MarkdownIt = (await import('markdown-it')).default;
const hljsModule = await import('highlight.js');
const hljs = hljsModule.default || hljsModule;
const md: any = new MarkdownIt({
highlight: function (str: string, lang: string): string {
if (lang && hljs.getLanguage(lang)) {
try {
return '<pre class="hljs"><code>' +
hljs.highlight(str, { language: lang }).value +
'</code></pre>';
} catch (__) {}
}
return '<pre class="hljs"><code>' + md.utils.escapeHtml(str) + '</code></pre>';
}
});
content = md.render(docContent);
} else {
error = $page.data.error || 'Failed to load NIP-34 specification';
}
} catch (err) {
error = err instanceof Error ? err.message : 'Failed to load specification';
console.error('Error parsing NIP-34 specification:', err);
} finally {
loading = false;
}
});
</script>
<div class="container">
<header>
<h1>NIP-34 Specification</h1>
<p class="subtitle">Nostr Improvement Proposal for Git Collaboration</p>
</header>
<main class="docs-content">
{#if loading}
<div class="loading">Loading specification...</div>
{:else if error}
<div class="error">{error}</div>
{:else}
<div class="markdown-content">
{@html content}
</div>
{/if}
</main>
</div>
<style>
.subtitle {
color: var(--text-muted);
margin: 0;
}
.docs-content {
background: var(--card-bg);
padding: 2rem;
border-radius: 0.5rem;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
border: 1px solid var(--border-color);
}
:global(.markdown-content) {
line-height: 1.6;
}
:global(.markdown-content h1) {
font-size: 2rem;
margin-top: 2rem;
margin-bottom: 1rem;
border-bottom: 2px solid var(--border-color);
padding-bottom: 0.5rem;
color: var(--text-primary);
}
:global(.markdown-content h2) {
font-size: 1.5rem;
margin-top: 1.5rem;
margin-bottom: 0.75rem;
color: var(--text-primary);
}
:global(.markdown-content h3) {
font-size: 1.25rem;
margin-top: 1.25rem;
margin-bottom: 0.5rem;
color: var(--text-primary);
}
:global(.markdown-content code) {
background: var(--bg-secondary);
padding: 0.125rem 0.25rem;
border-radius: 0.25rem;
font-family: 'IBM Plex Mono', monospace;
font-size: 0.875em;
color: var(--text-primary);
}
:global(.markdown-content pre) {
background: var(--bg-tertiary);
color: var(--text-primary);
padding: 1rem;
border-radius: 0.5rem;
overflow-x: auto;
margin: 1rem 0;
border: 1px solid var(--border-color);
}
:global(.markdown-content pre code) {
background: transparent;
padding: 0;
color: inherit;
}
:global(.markdown-content p) {
margin: 1rem 0;
}
:global(.markdown-content ul, .markdown-content ol) {
margin: 1rem 0;
padding-left: 2rem;
}
:global(.markdown-content li) {
margin: 0.5rem 0;
}
:global(.markdown-content blockquote) {
border-left: 4px solid var(--accent);
padding-left: 1rem;
margin: 1rem 0;
color: var(--text-secondary);
}
:global(.markdown-content table) {
width: 100%;
border-collapse: collapse;
margin: 1rem 0;
}
:global(.markdown-content th, .markdown-content td) {
border: 1px solid var(--border-color);
padding: 0.5rem;
text-align: left;
}
:global(.markdown-content th) {
background: var(--bg-secondary);
font-weight: 600;
color: var(--text-primary);
}
:global(.markdown-content td) {
color: var(--text-primary);
}
</style>

241
src/routes/signup/+page.svelte

@ -20,6 +20,8 @@
let cloneUrls = $state<string[]>(['']); let cloneUrls = $state<string[]>(['']);
let webUrls = $state<string[]>(['']); let webUrls = $state<string[]>(['']);
let maintainers = $state<string[]>(['']); let maintainers = $state<string[]>(['']);
let relays = $state<string[]>(['']);
let blossoms = $state<string[]>(['']);
let tags = $state<string[]>(['']); let tags = $state<string[]>(['']);
let documentation = $state<string[]>(['']); let documentation = $state<string[]>(['']);
let alt = $state(''); let alt = $state('');
@ -96,6 +98,34 @@
maintainers = newMaintainers; maintainers = newMaintainers;
} }
function addRelay() {
relays = [...relays, ''];
}
function removeRelay(index: number) {
relays = relays.filter((_, i) => i !== index);
}
function updateRelay(index: number, value: string) {
const newRelays = [...relays];
newRelays[index] = value;
relays = newRelays;
}
function addBlossom() {
blossoms = [...blossoms, ''];
}
function removeBlossom(index: number) {
blossoms = blossoms.filter((_, i) => i !== index);
}
function updateBlossom(index: number, value: string) {
const newBlossoms = [...blossoms];
newBlossoms[index] = value;
blossoms = newBlossoms;
}
function addTag() { function addTag() {
tags = [...tags, '']; tags = [...tags, ''];
} }
@ -603,6 +633,34 @@
} }
maintainers = maintainersList.length > 0 ? maintainersList : ['']; maintainers = maintainersList.length > 0 ? maintainersList : [''];
// Extract relays
const relaysList: string[] = [];
for (const tag of event.tags) {
if (tag[0] === 'relays') {
for (let i = 1; i < tag.length; i++) {
const relay = tag[i];
if (relay && typeof relay === 'string' && relay.trim()) {
relaysList.push(relay.trim());
}
}
}
}
relays = relaysList.length > 0 ? relaysList : [''];
// Extract blossoms
const blossomsList: string[] = [];
for (const tag of event.tags) {
if (tag[0] === 'blossoms') {
for (let i = 1; i < tag.length; i++) {
const blossom = tag[i];
if (blossom && typeof blossom === 'string' && blossom.trim()) {
blossomsList.push(blossom.trim());
}
}
}
}
blossoms = blossomsList.length > 0 ? blossomsList : [''];
// Extract tags/labels // Extract tags/labels
const tagsList: string[] = []; const tagsList: string[] = [];
for (const tag of event.tags) { for (const tag of event.tags) {
@ -612,15 +670,90 @@
} }
tags = tagsList.length > 0 ? tagsList : ['']; tags = tagsList.length > 0 ? tagsList : [''];
// Extract documentation - handle both formats // Extract documentation - handle relay hints correctly
// Only treat values as multiple entries if they are in the same format
// If a value looks like a relay URL (wss:// or ws://), it's a relay hint for the previous value
const docsList: string[] = []; const docsList: string[] = [];
const isRelayUrl = (value: string): boolean => {
return typeof value === 'string' && (value.startsWith('wss://') || value.startsWith('ws://'));
};
const getDocFormat = (value: string): string | null => {
// Check if it's naddr format (starts with naddr1)
if (value.startsWith('naddr1')) return 'naddr';
// Check if it's kind:pubkey:identifier format
if (/^\d+:[0-9a-f]{64}:[a-zA-Z0-9_-]+$/.test(value)) return 'kind:pubkey:identifier';
return null;
};
for (const tag of event.tags) { for (const tag of event.tags) {
if (tag[0] === 'documentation') { if (tag[0] === 'documentation') {
for (let i = 1; i < tag.length; i++) { let i = 1;
const doc = tag[i];
if (doc && typeof doc === 'string' && doc.trim()) { while (i < tag.length) {
docsList.push(doc.trim()); const value = tag[i];
if (!value || typeof value !== 'string' || !value.trim()) {
i++;
continue;
}
const trimmed = value.trim();
// Skip relay URLs (they're hints, not entries)
if (isRelayUrl(trimmed)) {
i++;
continue;
}
// Check if this is a documentation reference
const format = getDocFormat(trimmed);
if (!format) {
i++;
continue; // Skip invalid formats
}
// Check if next value is a relay URL (hint for this entry)
const nextValue = i + 1 < tag.length ? tag[i + 1] : null;
if (nextValue && typeof nextValue === 'string' && isRelayUrl(nextValue.trim())) {
// Current value has a relay hint - store just the doc reference, skip the relay
docsList.push(trimmed);
i += 2; // Skip both the doc and the relay hint
continue;
} }
// Check if we have multiple entries in the same format
// Collect all consecutive entries of the same format
const sameFormatEntries: string[] = [trimmed];
let j = i + 1;
while (j < tag.length) {
const nextVal = tag[j];
if (!nextVal || typeof nextVal !== 'string' || !nextVal.trim()) {
j++;
continue;
}
const nextTrimmed = nextVal.trim();
// Stop if we hit a relay URL (it's a hint for the previous entry)
if (isRelayUrl(nextTrimmed)) {
break;
}
// Check if it's the same format
const nextFormat = getDocFormat(nextTrimmed);
if (nextFormat === format) {
sameFormatEntries.push(nextTrimmed);
j++;
} else {
// Different format - stop collecting
break;
}
}
// If we have multiple entries in the same format, add them all
// Otherwise, just add the single entry
docsList.push(...sameFormatEntries);
i = j; // Move to the next unprocessed value
} }
} }
} }
@ -761,27 +894,37 @@
// Build maintainers list // Build maintainers list
const allMaintainers = maintainers.filter(m => m.trim()); const allMaintainers = maintainers.filter(m => m.trim());
// Build relays list - combine user relays with default relays
const allRelays = [
...relays.filter(r => r.trim()),
...DEFAULT_NOSTR_RELAYS.filter(r => !relays.includes(r))
];
// Build blossoms list
const allBlossoms = blossoms.filter(b => b.trim());
// Build documentation list // Build documentation list
const allDocumentation = documentation.filter(d => d.trim()); const allDocumentation = documentation.filter(d => d.trim());
// Build tags/labels (excluding 'private' and 'fork' which are handled separately) // Build tags/labels (excluding 'private' and 'fork' which are handled separately)
const allTags = tags.filter(t => t.trim() && t !== 'private' && t !== 'fork'); const allTags = tags.filter(t => t.trim() && t !== 'private' && t !== 'fork');
// Build event tags - use separate tag for each value (correct format) // Build event tags - use single tag with multiple values (NIP-34 format)
const eventTags: string[][] = [ const eventTags: string[][] = [
['d', dTag], ['d', dTag],
['name', repoName], ['name', repoName],
...(description ? [['description', description]] : []), ...(description ? [['description', description]] : []),
...allCloneUrls.map(url => ['clone', url]), // Separate tag per URL ...(allCloneUrls.length > 0 ? [['clone', ...allCloneUrls]] : []), // Single tag with all clone URLs
...allWebUrls.map(url => ['web', url]), // Separate tag per URL ...(allWebUrls.length > 0 ? [['web', ...allWebUrls]] : []), // Single tag with all web URLs
...allMaintainers.map(m => ['maintainers', m]), // Separate tag per maintainer ...(allMaintainers.length > 0 ? [['maintainers', ...allMaintainers]] : []), // Single tag with all maintainers
...allDocumentation.map(d => ['documentation', d]), // Separate tag per documentation ...(allRelays.length > 0 ? [['relays', ...allRelays]] : []), // Single tag with all relays
...(allBlossoms.length > 0 ? [['blossoms', ...allBlossoms]] : []), // Single tag with all blossoms
...allDocumentation.map(d => ['documentation', d]), // Documentation can have relay hints, so keep separate
...allTags.map(t => ['t', t]), ...allTags.map(t => ['t', t]),
...(imageUrl.trim() ? [['image', imageUrl.trim()]] : []), ...(imageUrl.trim() ? [['image', imageUrl.trim()]] : []),
...(bannerUrl.trim() ? [['banner', bannerUrl.trim()]] : []), ...(bannerUrl.trim() ? [['banner', bannerUrl.trim()]] : []),
...(alt.trim() ? [['alt', alt.trim()]] : []), ...(alt.trim() ? [['alt', alt.trim()]] : []),
...(earliestCommit.trim() ? [['r', earliestCommit.trim(), 'euc']] : []), ...(earliestCommit.trim() ? [['r', earliestCommit.trim(), 'euc']] : [])
...DEFAULT_NOSTR_RELAYS.map(relay => ['relays', relay]) // Separate tag per relay (correct format)
]; ];
// Add fork tags if this is a fork // Add fork tags if this is a fork
@ -1296,6 +1439,76 @@
</button> </button>
</div> </div>
<div class="form-group">
<div class="label">
Relays (optional)
<small>Nostr relays that this repository will monitor for patches and issues. Default relays will be added automatically.</small>
</div>
{#each relays as relay, index}
<div class="input-group">
<input
type="text"
value={relay}
oninput={(e) => updateRelay(index, e.currentTarget.value)}
placeholder="wss://relay.example.com"
disabled={loading}
/>
{#if relays.length > 1}
<button
type="button"
onclick={() => removeRelay(index)}
disabled={loading}
>
Remove
</button>
{/if}
</div>
{/each}
<button
type="button"
onclick={addRelay}
disabled={loading}
class="add-button"
>
+ Add Relay
</button>
</div>
<div class="form-group">
<div class="label">
Blossoms (optional)
<small>Blossom URLs for this repository. These are preserved but not actively used by GitRepublic.</small>
</div>
{#each blossoms as blossom, index}
<div class="input-group">
<input
type="text"
value={blossom}
oninput={(e) => updateBlossom(index, e.currentTarget.value)}
placeholder="https://example.com"
disabled={loading}
/>
{#if blossoms.length > 1}
<button
type="button"
onclick={() => removeBlossom(index)}
disabled={loading}
>
Remove
</button>
{/if}
</div>
{/each}
<button
type="button"
onclick={addBlossom}
disabled={loading}
class="add-button"
>
+ Add Blossom
</button>
</div>
<div class="form-group"> <div class="form-group">
<label for="image-url"> <label for="image-url">
Repository Image URL (optional) Repository Image URL (optional)
@ -1390,7 +1603,7 @@
<div class="form-group"> <div class="form-group">
<div class="label"> <div class="label">
Documentation (optional) Documentation (optional)
<small>Documentation event addresses (naddr format). Example: 30818:pubkey:nkbip-01</small> <small>Documentation event addresses (naddr format). Example: 30818:fd208ee8c8f283780a9552896e4823cc9dc6bfd442063889577106940fd927c1:nkbip-01</small>
</div> </div>
{#each documentation as doc, index} {#each documentation as doc, index}
<div class="input-group"> <div class="input-group">
@ -1398,7 +1611,7 @@
type="text" type="text"
value={doc} value={doc}
oninput={(e) => updateDocumentation(index, e.currentTarget.value)} oninput={(e) => updateDocumentation(index, e.currentTarget.value)}
placeholder="30818:pubkey:doc-name" placeholder="30818:pubkey:d-tag"
disabled={loading} disabled={loading}
/> />
{#if documentation.length > 1} {#if documentation.length > 1}

Loading…
Cancel
Save