@ -2,50 +2,22 @@
import { onMount } from 'svelte';
import { onMount } from 'svelte';
import { goto } from '$app/navigation';
import { goto } from '$app/navigation';
import { page } from '$app/stores';
import { page } from '$app/stores';
import { NostrClient } from '../lib/services/nostr/nostr-client.js';
import { KIND } from '../lib/types/nostr.js';
import type { NostrEvent } from '../lib/types/nostr.js';
import { nip19 } from 'nostr-tools';
import { ForkCountService } from '../lib/services/nostr/fork-count-service.js';
import { getPublicKeyWithNIP07 , isNIP07Available } from '../lib/services/nostr/nip07-signer.js';
import { getPublicKeyWithNIP07 , isNIP07Available } from '../lib/services/nostr/nip07-signer.js';
import { nip19 } from 'nostr-tools';
// Registered repos (with domain in clone URLs)
let registeredRepos = $state< Array < { event : NostrEvent ; npub : string ; repoName : string } > >([]);
let allRegisteredRepos = $state< Array < { event : NostrEvent ; npub : string ; repoName : string } > >([]);
// Local clones (repos without domain in clone URLs)
let localRepos = $state< Array < { npub : string ; repoName : string ; announcement : NostrEvent | null ; lastModified : number } > >([]);
let allLocalRepos = $state< Array < { npub : string ; repoName : string ; announcement : NostrEvent | null ; lastModified : number } > >([]);
let loading = $state(true);
let loadingLocal = $state(false);
let error = $state< string | null > (null);
let forkCounts = $state< Map < string , number > >(new Map());
let searchQuery = $state('');
let showOnlyMyContacts = $state(false);
let userPubkey = $state< string | null > (null);
let userPubkey = $state< string | null > (null);
let userPubkeyHex = $state< string | null > (null);
let userPubkeyHex = $state< string | null > (null);
let contactPubkeys = $state< Set < string > >(new Set());
let checkingAuth = $state(true);
let deletingRepo = $state< { npub : string ; repo : string } | null>(null);
import { DEFAULT_NOSTR_RELAYS } from '../lib/config.js';
const forkCountService = new ForkCountService(DEFAULT_NOSTR_RELAYS);
const nostrClient = new NostrClient(DEFAULT_NOSTR_RELAYS);
onMount(async () => {
onMount(async () => {
await loadRepos();
await checkAuth();
await loadUserAndContacts();
});
});
async function loadUserAndContacts() {
async function checkAuth() {
if (!isNIP07Available()) {
checkingAuth = true;
return;
if (isNIP07Available()) {
}
try {
try {
userPubkey = await getPublicKeyWithNIP07();
userPubkey = await getPublicKeyWithNIP07();
// Convert npub to hex for API calls
// Convert npub to hex for API calls
try {
try {
const decoded = nip19.decode(userPubkey);
const decoded = nip19.decode(userPubkey);
@ -55,586 +27,334 @@
} catch {
} catch {
userPubkeyHex = userPubkey; // Assume it's already hex
userPubkeyHex = userPubkey; // Assume it's already hex
}
}
} catch (err) {
contactPubkeys.add(userPubkeyHex); // Include user's own repos
console.warn('Failed to load user pubkey:', err);
// Fetch user's kind 3 contact list
const contactEvents = await nostrClient.fetchEvents([
{
kinds: [KIND.CONTACT_LIST],
authors: [userPubkeyHex],
limit: 1
}
]);
if (contactEvents.length > 0) {
const contactEvent = contactEvents[0];
// Extract pubkeys from 'p' tags
for (const tag of contactEvent.tags) {
if (tag[0] === 'p' && tag[1]) {
let pubkey = tag[1];
// Try to decode if it's an npub
try {
const decoded = nip19.decode(pubkey);
if (decoded.type === 'npub') {
pubkey = decoded.data as string;
}
} catch {
// Assume it's already a hex pubkey
}
if (pubkey) {
contactPubkeys.add(pubkey);
}
}
}
}
checkingAuth = false;
}
}
async function handleLogin() {
if (isNIP07Available()) {
try {
await checkAuth();
if (userPubkey) {
// User is logged in, go to repos page
goto('/repos');
}
}
} catch (err) {
} catch (err) {
console.warn('Failed to load user or contacts:', err);
console.error('Login failed:', err);
alert('Failed to login. Please make sure you have a Nostr extension installed (like nos2x or Alby).');
}
} else {
alert('Nostr extension not found. Please install a Nostr extension like nos2x or Alby to login.');
}
}
}
}
async function loadRepos() {
function handleViewPublic() {
loading = true;
goto('/repos');
error = null;
}
try {
const gitDomain = $page.data.gitDomain || 'localhost:6543';
const url = `/api/repos/list?domain=${ encodeURIComponent ( gitDomain )} `;
const response = await fetch(url, {
// Get page data for OpenGraph metadata
headers: userPubkeyHex ? {
const pageData = $page.data as {
'X-User-Pubkey': userPubkeyHex
title?: string;
} : {}
description?: string;
});
image?: string;
url?: string;
ogType?: string;
};
< / script >
if (!response.ok) {
< svelte:head >
throw new Error(`Failed to load repositories: ${ response . statusText } `);
< title > { pageData . title || 'GitRepublic - Decentralized Git Hosting on Nostr' } </ title >
}
< meta name = "description" content = { pageData . description || 'A decentralized git hosting platform built on Nostr. Host your repositories, collaborate with others, and maintain full control of your code.' } / >
const data = await response.json();
<!-- OpenGraph / Facebook -->
< meta property = "og:type" content = { pageData . ogType || 'website' } / >
< meta property = "og:title" content = { pageData . title || 'GitRepublic - Decentralized Git Hosting on Nostr' } / >
< meta property = "og:description" content = { pageData . description || 'A decentralized git hosting platform built on Nostr. Host your repositories, collaborate with others, and maintain full control of your code.' } / >
< meta property = "og:url" content = { pageData . url || `https://$ { $page . url . host } $ { $page . url . pathname } ` } / >
{ #if pageData . image }
< meta property = "og:image" content = { pageData . image } / >
< meta property = "og:image:width" content = "1200" / >
< meta property = "og:image:height" content = "630" / >
{ /if }
// Set registered repos
<!-- Twitter Card -->
registeredRepos = data.registered || [];
< meta name = "twitter:card" content = "summary_large_image" / >
allRegisteredRepos = [...registeredRepos];
< meta name = "twitter:title" content = { pageData . title || 'GitRepublic - Decentralized Git Hosting on Nostr' } / >
< meta name = "twitter:description" content = { pageData . description || 'A decentralized git hosting platform built on Nostr. Host your repositories, collaborate with others, and maintain full control of your code.' } / >
{ #if pageData . image }
< meta name = "twitter:image" content = { pageData . image } / >
{ /if }
< / svelte:head >
// Load fork counts for registered repos (in parallel, but don't block)
< div class = "splash-container" >
loadForkCounts(registeredRepos.map(r => r.event)).catch(err => {
< div class = "splash-background" >
console.warn('[RepoList] Failed to load some fork counts:', err);
< img src = "/logo.png" alt = "GitRepublic Logo" class = "splash-logo-bg" / >
});
< / div >
// Load local repos separately (async, don't block)
< div class = "splash-content" >
loadLocalRepos();
< div class = "splash-header" >
} catch (e) {
< img src = "/logo.png" alt = "GitRepublic" class = "splash-logo" / >
error = String(e);
< h1 class = "splash-title" > GitRepublic< / h1 >
console.error('[RepoList] Failed to load repos:', e);
< p class = "splash-subtitle" > Decentralized Git Hosting on Nostr< / p >
} finally {
< / div >
loading = false;
}
}
async function loadLocalRepos() {
< div class = "splash-message" >
loadingLocal = true;
{ #if checkingAuth }
< p class = "splash-text" > Checking authentication...< / p >
{ :else if userPubkey }
< p class = "splash-text" > Welcome back! You're logged in.< / p >
< p class = "splash-text-secondary" > You can now access all repositories you have permission to view.< / p >
{ : else }
< p class = "splash-text" > Login for full functionality< / p >
< p class = "splash-text-secondary" > Access your private repositories, create new ones, and collaborate with others.< / p >
< p class = "splash-text-secondary" > Or browse public repositories without logging in.< / p >
{ /if }
< / div >
try {
< div class = "splash-actions" >
const gitDomain = $page.data.gitDomain || 'localhost:6543';
{ #if checkingAuth }
const url = `/api/repos/local?domain=${ encodeURIComponent ( gitDomain )} `;
< div class = "splash-loading" > Loading...< / div >
{ :else if userPubkey }
< button class = "splash-button splash-button-primary" onclick = {() => goto ( '/repos' )} >
View Repositories
< / button >
{ : else }
< button class = "splash-button splash-button-primary" onclick = { handleLogin } >
Login with Nostr
< / button >
< button class = "splash-button splash-button-secondary" onclick = { handleViewPublic } >
View Public Repositories
< / button >
{ /if }
< / div >
const response = await fetch(url, {
< div class = "splash-features" >
headers: userPubkeyHex ? {
< div class = "feature-item" >
'X-User-Pubkey': userPubkeyHex
< strong > 🔐 Private Repositories< / strong >
} : {}
< p > Control who can access your code< / p >
});
< / div >
< div class = "feature-item" >
< strong > 🌐 Decentralized< / strong >
< p > Built on Nostr protocol< / p >
< / div >
< div class = "feature-item" >
< strong > 🔧 Full Git Support< / strong >
< p > All standard Git operations< / p >
< / div >
< / div >
< / div >
< / div >
if (!response.ok) {
< style >
console.warn('Failed to load local repos:', response.statusText);
.splash-container {
return;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
position: relative;
overflow: hidden;
background: linear-gradient(135deg, var(--bg-primary, #f5f5f5) 0%, var(--bg-secondary, #e8e8e8) 100%);
}
}
const data = await response.json();
.splash-background {
localRepos = data || [];
position: absolute;
allLocalRepos = [...localRepos];
top: 0;
} catch (e) {
left: 0;
console.warn('[RepoList] Failed to load local repos:', e);
right: 0;
} finally {
bottom: 0;
loadingLocal = false;
opacity: 0.05;
}
z-index: 0;
display: flex;
align-items: center;
justify-content: center;
}
}
async function deleteLocalRepo(npub: string, repo: string) {
.splash-logo-bg {
if (!confirm(`Are you sure you want to delete the local clone of ${ repo } ? This will remove the repository from this server but will not delete the announcement on Nostr.`)) {
width: 80vw;
return;
height: 80vh;
object-fit: contain;
filter: blur(20px);
}
}
deletingRepo = { npub , repo } ;
.splash-content {
position: relative;
try {
z-index: 1;
const response = await fetch(`/api/repos/${ npub } /${ repo } /delete`, {
text-align: center;
method: 'DELETE',
padding: 3rem 2rem;
headers: userPubkeyHex ? {
max-width: 800px;
'X-User-Pubkey': userPubkeyHex
width: 100%;
} : {}
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.message || 'Failed to delete repository');
}
}
// Remove from local repos list
.splash-header {
localRepos = localRepos.filter(r => !(r.npub === npub & & r.repoName === repo));
margin-bottom: 3rem;
allLocalRepos = [...localRepos];
alert('Repository deleted successfully');
} catch (e) {
alert(`Failed to delete repository: ${ e instanceof Error ? e.message : String ( e )} `);
} finally {
deletingRepo = null;
}
}
}
function registerRepo(npub: string, repo: string) {
.splash-logo {
// Navigate to signup page with repo pre-filled
width: 120px;
goto(`/signup?npub=${ encodeURIComponent ( npub )} &repo=$ { encodeURIComponent ( repo )} `);
height: 120px;
margin: 0 auto 1.5rem;
display: block;
filter: drop-shadow(0 4px 6px rgba(0, 0, 0, 0.1));
}
}
async function loadForkCounts(repoEvents: NostrEvent[]) {
.splash-title {
const counts = new Map< string , number > ();
font-size: 3.5rem;
font-weight: 700;
// Extract owner pubkey and repo name for each repo
margin: 0 0 0.5rem;
const forkCountPromises = repoEvents.map(async (event) => {
color: var(--text-primary, #1a1a1a);
try {
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
const dTag = event.tags.find(t => t[0] === 'd')?.[1];
if (!dTag) return;
const repoKey = `${ event . pubkey } :${ dTag } `;
const count = await forkCountService.getForkCount(event.pubkey, dTag);
counts.set(repoKey, count);
} catch (err) {
// Ignore individual failures
}
}
});
await Promise.all(forkCountPromises);
.splash-subtitle {
forkCounts = counts;
font-size: 1.5rem;
color: var(--text-secondary, #666);
margin: 0;
font-weight: 300;
}
}
function getForkCount(event: NostrEvent): number {
.splash-message {
const dTag = event.tags.find(t => t[0] === 'd')?.[1];
margin-bottom: 3rem;
if (!dTag) return 0;
const repoKey = `${ event . pubkey } :${ dTag } `;
return forkCounts.get(repoKey) || 0;
}
}
function isOwner(npub: string, repoName: string): boolean {
.splash-text {
if (!userPubkeyHex) return false;
font-size: 1.5rem;
try {
color: var(--text-primary, #1a1a1a);
const decoded = nip19.decode(npub);
margin: 0 0 1rem;
if (decoded.type === 'npub') {
font-weight: 500;
return decoded.data === userPubkeyHex;
}
} catch {
// Invalid npub
}
return false;
}
}
function goToSearch() {
.splash-text-secondary {
goto('/search');
font-size: 1.1rem;
color: var(--text-secondary, #666);
margin: 0.5rem 0;
line-height: 1.6;
}
}
function getRepoName(event: NostrEvent): string {
.splash-actions {
const nameTag = event.tags.find(t => t[0] === 'name' & & t[1]);
display: flex;
if (nameTag?.[1]) return nameTag[1];
gap: 1rem;
justify-content: center;
const dTag = event.tags.find(t => t[0] === 'd')?.[1];
flex-wrap: wrap;
if (dTag) return dTag;
margin-bottom: 4rem;
return `Repository ${ event . id . slice ( 0 , 8 )} `;
}
}
function getRepoDescription(event: NostrEvent): string {
.splash-button {
const descTag = event.tags.find(t => t[0] === 'description' & & t[1]);
padding: 1rem 2.5rem;
return descTag?.[1] || '';
font-size: 1.1rem;
font-weight: 600;
border: none;
border-radius: 8px;
cursor: pointer;
transition: all 0.3s ease;
text-decoration: none;
display: inline-block;
min-width: 200px;
}
}
function getRepoImage(event: NostrEvent): string | null {
.splash-button-primary {
const imageTag = event.tags.find(t => t[0] === 'image' & & t[1]);
background: var(--accent, #007bff);
return imageTag?.[1] || null;
color: white;
box-shadow: 0 4px 6px rgba(0, 123, 255, 0.3);
}
}
function getRepoBanner(event: NostrEvent): string | null {
.splash-button-primary:hover {
const bannerTag = event.tags.find(t => t[0] === 'banner' & & t[1]);
background: var(--accent-hover, #0056b3);
return bannerTag?.[1] || null;
transform: translateY(-2px);
box-shadow: 0 6px 12px rgba(0, 123, 255, 0.4);
}
}
function getCloneUrls(event: NostrEvent): string[] {
.splash-button-secondary {
const urls: string[] = [];
background: white;
color: var(--accent, #007bff);
for (const tag of event.tags) {
border: 2px solid var(--accent, #007bff);
if (tag[0] === 'clone') {
for (let i = 1; i < tag.length ; i ++) {
const url = tag[i];
if (url && typeof url === 'string') {
urls.push(url);
}
}
}
}
}
return urls;
.splash-button-secondary:hover {
background: var(--accent, #007bff);
color: white;
transform: translateY(-2px);
}
}
function getNpubFromEvent(event: NostrEvent): string {
.splash-loading {
// Extract npub from clone URLs that match our domain
font-size: 1.2rem;
const gitDomain = $page.data.gitDomain || 'localhost:6543';
color: var(--text-secondary, #666);
const cloneUrls = getCloneUrls(event);
padding: 2rem;
for (const url of cloneUrls) {
if (url.includes(gitDomain)) {
// Extract npub from URL: https://domain/npub.../repo.git
const match = url.match(/\/(npub[a-z0-9]+)\//);
if (match) {
return match[1];
}
}
}
}
// Fallback: convert pubkey to npub if needed
.splash-features {
try {
display: grid;
if (event.pubkey.startsWith('npub')) {
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
return event.pubkey;
gap: 2rem;
}
margin-top: 4rem;
return nip19.npubEncode(event.pubkey);
padding-top: 3rem;
} catch {
border-top: 1px solid var(--border-color, #ddd);
// If conversion fails, return pubkey as-is
return event.pubkey;
}
}
}
function getRepoNameFromUrl(event: NostrEvent): string {
.feature-item {
const gitDomain = $page.data.gitDomain || 'localhost:6543';
text-align: center;
const cloneUrls = getCloneUrls(event);
for (const url of cloneUrls) {
if (url.includes(gitDomain)) {
// Extract repo name from URL: https://domain/npub.../repo-name.git
const match = url.match(/\/(npub[a-z0-9]+)\/([^\/]+)\.git/);
if (match) {
return match[2];
}
}
}
}
// Fallback to repo name from event
.feature-item strong {
return getRepoName(event);
display: block;
font-size: 1.2rem;
color: var(--text-primary, #1a1a1a);
margin-bottom: 0.5rem;
}
}
// Get page data for OpenGraph metadata
.feature-item p {
const pageData = $page.data as {
font-size: 0.95rem;
title?: string;
color: var(--text-secondary, #666);
description?: string;
margin: 0;
image?: string;
url?: string;
ogType?: string;
};
interface SearchResult {
repo: NostrEvent;
score: number;
matchType: string;
}
}
function performSearch() {
@media (max-width: 768px) {
if (!searchQuery.trim()) {
.splash-title {
registeredRepos = [...allRegisteredRepos];
font-size: 2.5rem;
localRepos = [...allLocalRepos];
return;
}
}
const query = searchQuery.trim().toLowerCase();
.splash-subtitle {
font-size: 1.2rem;
// Search registered repos
let registeredToSearch = allRegisteredRepos;
if (showOnlyMyContacts && contactPubkeys.size > 0) {
registeredToSearch = allRegisteredRepos.filter(item => {
const event = item.event;
// Check if owner is in contacts
if (contactPubkeys.has(event.pubkey)) return true;
// Check if any maintainer is in contacts
const maintainerTags = event.tags.filter(t => t[0] === 'maintainers');
for (const tag of maintainerTags) {
for (let i = 1; i < tag.length ; i ++) {
let maintainerPubkey = tag[i];
try {
const decoded = nip19.decode(maintainerPubkey);
if (decoded.type === 'npub') {
maintainerPubkey = decoded.data as string;
}
} catch {
// Assume it's already a hex pubkey
}
}
if (contactPubkeys.has(maintainerPubkey)) return true;
}
}
return false;
});
}
const registeredResults: Array< { item : typeof allRegisteredRepos [ 0 ]; score : number } > = [];
for (const item of registeredToSearch) {
const repo = item.event;
let score = 0;
const name = getRepoName(repo).toLowerCase();
const dTag = repo.tags.find(t => t[0] === 'd')?.[1]?.toLowerCase() || '';
const description = getRepoDescription(repo).toLowerCase();
if (name.includes(query)) score += 100;
.splash-text {
if (dTag.includes(query)) score += 100;
font-size: 1.2rem;
if (description.includes(query)) score += 30;
if (score > 0) {
registeredResults.push({ item , score } );
}
}
}
registeredResults.sort((a, b) => b.score - a.score || b.item.event.created_at - a.item.event.created_at);
.splash-logo {
registeredRepos = registeredResults.map(r => r.item);
width: 80px;
height: 80px;
// Search local repos
}
const localResults: Array< { item : typeof allLocalRepos [ 0 ]; score : number } > = [];
for (const item of allLocalRepos) {
let score = 0;
const repoName = item.repoName.toLowerCase();
const announcement = item.announcement;
if (repoName.includes(query)) score += 100;
.splash-button {
if (announcement) {
width: 100%;
const name = getRepoName(announcement).toLowerCase();
min-width: unset;
const description = getRepoDescription(announcement).toLowerCase();
if (name.includes(query)) score += 100;
if (description.includes(query)) score += 30;
}
}
if (score > 0) {
.splash-features {
localResults.push({ item , score } );
grid-template-columns: 1fr;
gap: 1.5rem;
}
}
}
}
localResults.sort((a, b) => b.score - a.score || b.item.lastModified - a.item.lastModified);
@media (prefers-color-scheme: dark) {
localRepos = localResults.map(r => r.item);
.splash-container {
background: linear-gradient(135deg, var(--bg-primary, #1a1a1a) 0%, var(--bg-secondary, #2d2d2d) 100%);
}
}
// Reactive search when query or filter changes
.splash-title {
$effect(() => {
color: var(--text-primary, #f5f5f5);
if (!loading) {
performSearch();
}
}
});
< / script >
< svelte:head >
< title > { pageData . title || 'GitRepublic - Decentralized Git Hosting on Nostr' } </ title >
< meta name = "description" content = { pageData . description || 'A decentralized git hosting platform built on Nostr. Host your repositories, collaborate with others, and maintain full control of your code.' } / >
<!-- OpenGraph / Facebook -->
< meta property = "og:type" content = { pageData . ogType || 'website' } / >
< meta property = "og:title" content = { pageData . title || 'GitRepublic - Decentralized Git Hosting on Nostr' } / >
< meta property = "og:description" content = { pageData . description || 'A decentralized git hosting platform built on Nostr. Host your repositories, collaborate with others, and maintain full control of your code.' } / >
< meta property = "og:url" content = { pageData . url || `https://$ { $page . url . host } $ { $page . url . pathname } ` } / >
{ #if pageData . image }
< meta property = "og:image" content = { pageData . image } / >
< meta property = "og:image:width" content = "1200" / >
< meta property = "og:image:height" content = "630" / >
{ /if }
<!-- Twitter Card -->
< meta name = "twitter:card" content = "summary_large_image" / >
< meta name = "twitter:title" content = { pageData . title || 'GitRepublic - Decentralized Git Hosting on Nostr' } / >
< meta name = "twitter:description" content = { pageData . description || 'A decentralized git hosting platform built on Nostr. Host your repositories, collaborate with others, and maintain full control of your code.' } / >
{ #if pageData . image }
< meta name = "twitter:image" content = { pageData . image } / >
{ /if }
< / svelte:head >
< div class = "container" >
.splash-text {
< main >
color: var(--text-primary, #f5f5f5);
< div class = "repos-header" >
}
< h2 > Repositories on { $page . data . gitDomain || 'localhost:6543' } </ h2 >
< button onclick = { loadRepos } disabled= { loading } >
{ loading ? 'Loading...' : 'Refresh' }
< / button >
< / div >
< div class = "search-section" >
< div class = "search-bar-container" >
< input
type="text"
bind:value={ searchQuery }
placeholder="Search by name, d-tag, pubkey, maintainers, clone URL, hex id/naddr/nevent, or fulltext..."
class="search-input"
disabled={ loading }
oninput={ performSearch }
/>
< / div >
{ #if isNIP07Available () && userPubkey }
< label class = "filter-checkbox" >
< input
type="checkbox"
bind:checked={ showOnlyMyContacts }
onchange={ performSearch }
/>
< span > Show only my repos and those of my contacts< / span >
< / label >
{ /if }
< / div >
{ #if error }
< div class = "error" >
Error loading repositories: { error }
< / div >
{ :else if loading }
< div class = "loading" > Loading repositories...< / div >
{ : else }
<!-- Registered Repositories Section -->
< div class = "repo-section" >
< div class = "section-header" >
< h3 > Registered Repositories< / h3 >
< span class = "section-badge" > { registeredRepos . length } </ span >
< / div >
{ #if registeredRepos . length === 0 }
< div class = "empty" > No registered repositories found.< / div >
{ : else }
< div class = "repos-list" >
{ #each registeredRepos as item }
{ @const repo = item . event }
{ @const repoImage = getRepoImage ( repo )}
{ @const repoBanner = getRepoBanner ( repo )}
< div class = "repo-card repo-card-registered" >
{ #if repoBanner }
< div class = "repo-card-banner" >
< img src = { repoBanner } alt="Banner" />
< / div >
{ /if }
< div class = "repo-card-content" >
< div class = "repo-header" >
{ #if repoImage }
< img src = { repoImage } alt="Repository" class = "repo-card-image" />
{ /if }
< div class = "repo-header-text" >
< h3 > { getRepoName ( repo )} </ h3 >
{ #if getRepoDescription ( repo )}
< p class = "description" > { getRepoDescription ( repo )} </ p >
{ /if }
< / div >
< a href = "/repos/ { item . npub } / { item . repoName } " class = "view-button" >
View & Edit →
< / a >
< / div >
< div class = "clone-urls" >
< strong > Clone URLs:< / strong >
{ #each getCloneUrls ( repo ) as url }
< code > { url } </ code >
{ /each }
< / div >
< div class = "repo-meta" >
< span > Created: { new Date ( repo . created_at * 1000 ). toLocaleDateString ()} </ span >
{ #if getForkCount ( repo ) > 0 }
{ @const forkCount = getForkCount ( repo )}
< span class = "fork-count" > 🍴 { forkCount } fork{ forkCount === 1 ? '' : 's' } </ span >
{ /if }
< / div >
< / div >
< / div >
{ /each }
< / div >
{ /if }
< / div >
<!-- Local Clones Section -->
< div class = "repo-section" >
< div class = "section-header" >
< h3 > Local Clones< / h3 >
< span class = "section-badge" > { localRepos . length } </ span >
< span class = "section-description" > Repositories cloned locally but not registered with this domain< / span >
< / div >
{ #if loadingLocal }
< div class = "loading" > Loading local repositories...< / div >
{ :else if localRepos . length === 0 }
< div class = "empty" > No local clones found.< / div >
{ : else }
< div class = "repos-list" >
{ #each localRepos as item }
{ @const repo = item . announcement }
{ @const repoImage = repo ? getRepoImage ( repo ) : null }
{ @const repoBanner = repo ? getRepoBanner ( repo ) : null }
{ @const canDelete = isOwner ( item . npub , item . repoName )}
< div class = "repo-card repo-card-local" >
{ #if repoBanner }
< div class = "repo-card-banner" >
< img src = { repoBanner } alt="Banner" />
< / div >
{ /if }
< div class = "repo-card-content" >
< div class = "repo-header" >
{ #if repoImage }
< img src = { repoImage } alt="Repository" class = "repo-card-image" />
{ /if }
< div class = "repo-header-text" >
< h3 > { repo ? getRepoName ( repo ) : item . repoName } </ h3 >
{ #if repo && getRepoDescription ( repo )}
< p class = "description" > { getRepoDescription ( repo )} </ p >
{ : else }
< p class = "description" > No description available< / p >
{ /if }
< / div >
< div class = "repo-actions" >
< a href = "/repos/ { item . npub } / { item . repoName } " class = "view-button" >
View & Edit →
< / a >
{ #if canDelete }
< button
class="delete-button"
onclick={() => deleteLocalRepo ( item . npub , item . repoName )}
disabled={ deletingRepo ? . npub === item . npub && deletingRepo ? . repo === item . repoName }
>
{ deletingRepo ? . npub === item . npub && deletingRepo ? . repo === item . repoName ? 'Deleting...' : 'Delete' }
< / button >
{ /if }
< button
class="register-button"
onclick={() => registerRepo ( item . npub , item . repoName )}
>
Register
< / button >
< / div >
< / div >
{ #if repo }
< div class = "clone-urls" >
< strong > Clone URLs:< / strong >
{ #each getCloneUrls ( repo ) as url }
< code > { url } </ code >
{ /each }
< / div >
{ /if }
< div class = "repo-meta" >
< span > Last modified: { new Date ( item . lastModified ). toLocaleDateString ()} </ span >
{ #if repo }
< span > Created: { new Date ( repo . created_at * 1000 ). toLocaleDateString ()} </ span >
{ #if getForkCount ( repo ) > 0 }
{ @const forkCount = getForkCount ( repo )}
< span class = "fork-count" > 🍴 { forkCount } fork{ forkCount === 1 ? '' : 's' } </ span >
{ /if }
{ /if }
< / div >
< / div >
< / div >
{ /each }
< / div >
{ /if }
< / div >
{ /if }
< / main >
< / div >
.splash-button-secondary {
background: var(--bg-secondary, #2d2d2d);
border-color: var(--accent, #007bff);
}
}
< / style >