Browse Source

finish profile page

Nostr-Signature: 8a5aed2f8ac370f781dca9db96ade991c18b7cc3b0d27149d9e2741e8276f16f 573634b648634cbad10f2451776089ea21090d9407f715e83c577b4611ae6edc 16e9a9242f7c22dab8e37fd9d618419b4d51d7c0156f52c1289e275d2528312f4006696473c6836b5a661425fe0412fe54127291fb9b0d14777f93c8228cffb0
main
Silberengel 3 weeks ago
parent
commit
40ae1e3588
  1. 1
      nostr/commit-signatures.jsonl
  2. 4
      src/lib/services/nostr/persistent-event-cache.ts
  3. 323
      src/routes/repos/+page.svelte
  4. 889
      src/routes/users/[npub]/+page.svelte

1
nostr/commit-signatures.jsonl

@ -36,3 +36,4 @@ @@ -36,3 +36,4 @@
{"kind":1640,"pubkey":"573634b648634cbad10f2451776089ea21090d9407f715e83c577b4611ae6edc","created_at":1771626015,"tags":[["author","Silberengel","silberengel7@protonmail.com"],["message","bug-fixes"]],"content":"Signed commit: bug-fixes","id":"f5bde3d9199d8cbacca481959663f1e14c43e143ef2b5686502559408e1c526b","sig":"3ed47cd283746d290d8609cbfdefbcee31a19d8e43e1a6ebf5a2829904000d79b83d3235296af4b5f7b555051214fbf2fa5c7a6d7986dca853112bb4e122a6d5"}
{"kind":1640,"pubkey":"573634b648634cbad10f2451776089ea21090d9407f715e83c577b4611ae6edc","created_at":1771627873,"tags":[["author","Silberengel","silberengel7@protonmail.com"],["message","bug-fixes"]],"content":"Signed commit: bug-fixes","id":"5726811907af73d3b478f3938cdc6421200040542cb1a586b3497c56a24c33cb","sig":"3833d05ba5a34cad78caacbc8382fcd7a85c60b56dd3b18f9a5c68c890d7a611fa6b885ef02be465f541629b0afaeec0e9d57d3b00db332c5c8ae42fd72fc83d"}
{"kind":1640,"pubkey":"573634b648634cbad10f2451776089ea21090d9407f715e83c577b4611ae6edc","created_at":1771664126,"tags":[["author","Silberengel","silberengel7@protonmail.com"],["message","update profile page, dashboard, and connections"]],"content":"Signed commit: update profile page, dashboard, and connections","id":"862b888e52bf4fc3e53c80afd9f301b22ce674366f48d006bca520479394c0f9","sig":"c2e895f67ff5a68e87dcdc54a0312e169f4729a05a62f1ffbe92afd6e57b7d232b36ef4291c07969e531cdc4f22f5ac32723a2aecc57a0b613b945217ecc651a"}
{"kind":1640,"pubkey":"573634b648634cbad10f2451776089ea21090d9407f715e83c577b4611ae6edc","created_at":1771664339,"tags":[["author","Silberengel","silberengel7@protonmail.com"],["message","added lightning address copy button"]],"content":"Signed commit: added lightning address copy button","id":"f0973d13a903f64895d265643390fe54bd86fe492a53c3ffea303dad8cf8a2f6","sig":"8c98969c5755bf8742733e05ca4be53f4f3ba276a2445ee7b903e443947fc53808b046c188dd91f26b6dcaecbe93585e1f2539855c8eba57e17a915e81bfa2d4"}

4
src/lib/services/nostr/persistent-event-cache.ts

@ -41,8 +41,8 @@ const STORE_FILTERS = 'filters'; @@ -41,8 +41,8 @@ const STORE_FILTERS = 'filters';
const STORE_PROFILES = 'profiles';
const REPLACEABLE_KINDS = [0, 3, 10002]; // Profile, Contacts, Relay List
const DEFAULT_TTL = 5 * 60 * 1000; // 5 minutes
const PROFILE_TTL = 30 * 60 * 1000; // 30 minutes
const DEFAULT_TTL = 30 * 60 * 1000; // 30 minutes (increased for better performance)
const PROFILE_TTL = 60 * 60 * 1000; // 60 minutes (increased for profiles)
interface CachedEvent {
event: NostrEvent;

323
src/routes/repos/+page.svelte

@ -35,6 +35,13 @@ @@ -35,6 +35,13 @@
let myRepos = $state<Array<{ event: NostrEvent; npub: string; repoName: string; transferred?: boolean; currentOwner?: string }>>([]);
let loadingMyRepos = $state(false);
// Most Favorited Repositories
let mostFavoritedRepos = $state<Array<{ event: NostrEvent; npub: string; repoName: string; favoriteCount: number }>>([]);
let loadingMostFavorited = $state(false);
let mostFavoritedPage = $state(0);
let mostFavoritedCache: { data: typeof mostFavoritedRepos; timestamp: number } | null = null;
const CACHE_TTL = 5 * 60 * 1000; // 5 minutes
import { DEFAULT_NOSTR_RELAYS } from '$lib/config.js';
const forkCountService = new ForkCountService(DEFAULT_NOSTR_RELAYS);
@ -44,6 +51,7 @@ @@ -44,6 +51,7 @@
await loadRepos();
await loadUserAndContacts();
await loadMyRepos();
await loadMostFavoritedRepos();
});
// Reload repos when page becomes visible (e.g., after returning from another page)
@ -411,6 +419,133 @@ @@ -411,6 +419,133 @@
goto(`/signup?npub=${encodeURIComponent(npub)}&repo=${encodeURIComponent(repo)}`);
}
async function loadMostFavoritedRepos() {
// Check cache first
if (mostFavoritedCache && Date.now() - mostFavoritedCache.timestamp < CACHE_TTL) {
updateMostFavoritedPage();
return;
}
loadingMostFavorited = true;
try {
// Fetch up to 1000 bookmark events (kind 10003)
const bookmarkEvents = await nostrClient.fetchEvents([
{
kinds: [KIND.BOOKMARKS],
limit: 1000
}
]);
// Count how many times each repo a-tag appears
const repoCounts = new Map<string, { count: number; aTag: string }>();
for (const bookmark of bookmarkEvents) {
for (const tag of bookmark.tags) {
if (tag[0] === 'a' && tag[1]?.startsWith(`${KIND.REPO_ANNOUNCEMENT}:`)) {
const aTag = tag[1];
const current = repoCounts.get(aTag) || { count: 0, aTag };
current.count++;
repoCounts.set(aTag, current);
}
}
}
// Convert to array and sort by count
const repoCountsArray = Array.from(repoCounts.entries())
.map(([aTag, data]) => ({ aTag, count: data.count }))
.sort((a, b) => b.count - a.count);
// Fetch repo announcements for the top repos
const topRepos: Array<{ event: NostrEvent; npub: string; repoName: string; favoriteCount: number }> = [];
for (const { aTag, count } of repoCountsArray.slice(0, 100)) {
// Parse a-tag: 30617:pubkey:d-tag
const parts = aTag.split(':');
if (parts.length >= 3) {
const pubkey = parts[1];
const dTag = parts[2];
// Fetch the repo announcement
const announcements = await nostrClient.fetchEvents([
{
kinds: [KIND.REPO_ANNOUNCEMENT],
authors: [pubkey],
'#d': [dTag],
limit: 1
}
]);
if (announcements.length > 0) {
try {
const npub = nip19.npubEncode(pubkey);
topRepos.push({
event: announcements[0],
npub,
repoName: dTag,
favoriteCount: count
});
} catch (err) {
console.warn('Failed to encode npub for favorited repo:', err);
}
}
}
}
// Cache the results
mostFavoritedCache = {
data: topRepos,
timestamp: Date.now()
};
updateMostFavoritedPage();
} catch (err) {
console.error('Failed to load most favorited repos:', err);
} finally {
loadingMostFavorited = false;
}
}
function updateMostFavoritedPage() {
if (!mostFavoritedCache) return;
const start = mostFavoritedPage * 10;
const end = start + 10;
mostFavoritedRepos = mostFavoritedCache.data.slice(start, end);
}
function nextMostFavoritedPage() {
if (!mostFavoritedCache) return;
const maxPage = Math.ceil(mostFavoritedCache.data.length / 10) - 1;
if (mostFavoritedPage < maxPage) {
mostFavoritedPage++;
updateMostFavoritedPage();
}
}
function prevMostFavoritedPage() {
if (mostFavoritedPage > 0) {
mostFavoritedPage--;
updateMostFavoritedPage();
}
}
// Background refresh of cache
async function refreshMostFavoritedCache() {
if (!mostFavoritedCache || Date.now() - mostFavoritedCache.timestamp >= CACHE_TTL) {
await loadMostFavoritedRepos();
}
}
// Start background refresh
$effect(() => {
if (typeof window !== 'undefined') {
const interval = setInterval(() => {
refreshMostFavoritedCache().catch(err => console.warn('Background refresh failed:', err));
}, CACHE_TTL);
return () => clearInterval(interval);
}
});
async function loadForkCounts(repoEvents: NostrEvent[]) {
const counts = new Map<string, number>();
@ -620,6 +755,58 @@ @@ -620,6 +755,58 @@
</div>
{/if}
<!-- Most Favorited Repositories -->
<div class="most-favorited-section">
<div class="section-header">
<h3>Most Favorited Repositories</h3>
{#if loadingMostFavorited}
<span class="loading-indicator">Loading...</span>
{/if}
</div>
{#if loadingMostFavorited && mostFavoritedRepos.length === 0}
<div class="loading">Loading most favorited repositories...</div>
{:else if mostFavoritedRepos.length === 0}
<div class="empty">No favorited repositories found.</div>
{:else}
<div class="most-favorited-list">
{#each mostFavoritedRepos as item}
{@const repo = item.event}
{@const repoImage = getRepoImage(repo)}
<a href="/repos/{item.npub}/{item.repoName}" class="most-favorited-item">
{#if repoImage}
<img src={repoImage} alt={getRepoName(repo)} class="most-favorited-image" />
{:else}
<img src="/icons/package.svg" alt="Repository" class="most-favorited-icon" />
{/if}
<div class="most-favorited-info">
<h4>{getRepoName(repo)}</h4>
{#if getRepoDescription(repo)}
<p class="most-favorited-description">{getRepoDescription(repo)}</p>
{/if}
<span class="most-favorited-count">{item.favoriteCount} {item.favoriteCount === 1 ? 'favorite' : 'favorites'}</span>
</div>
</a>
{/each}
</div>
{#if mostFavoritedCache && mostFavoritedCache.data.length > 10}
<div class="pagination">
<button onclick={prevMostFavoritedPage} disabled={mostFavoritedPage === 0}>
Previous
</button>
<span class="page-info">
Page {mostFavoritedPage + 1} of {Math.ceil(mostFavoritedCache.data.length / 10)}
</span>
<button
onclick={nextMostFavoritedPage}
disabled={mostFavoritedPage >= Math.ceil(mostFavoritedCache.data.length / 10) - 1}
>
Next
</button>
</div>
{/if}
{/if}
</div>
<div class="repos-header">
<h2>Repositories on {$page.data.gitDomain || 'localhost:6543'}</h2>
<button onclick={loadRepos} disabled={loading}>
@ -800,3 +987,139 @@ @@ -800,3 +987,139 @@
{/if}
</main>
</div>
<style>
.most-favorited-section {
margin: 2rem 0;
padding: 1.5rem;
background: var(--card-bg, #ffffff);
border: 1px solid var(--border-color, #e0e0e0);
border-radius: 0.5rem;
}
.most-favorited-section .section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
}
.most-favorited-section .section-header h3 {
margin: 0;
font-size: 1.25rem;
color: var(--text-primary, #1a1a1a);
}
.loading-indicator {
color: var(--text-secondary, #666);
font-size: 0.875rem;
}
.most-favorited-list {
display: flex;
flex-direction: column;
gap: 1rem;
}
.most-favorited-item {
display: flex;
align-items: center;
gap: 1rem;
padding: 1rem;
background: var(--bg-secondary, #f5f5f5);
border: 1px solid var(--border-color, #e0e0e0);
border-radius: 0.5rem;
text-decoration: none;
color: inherit;
transition: all 0.2s ease;
}
.most-favorited-item:hover {
background: var(--bg-tertiary, #eeeeee);
border-color: var(--accent, #007bff);
transform: translateY(-2px);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.most-favorited-image,
.most-favorited-icon {
width: 48px;
height: 48px;
border-radius: 0.25rem;
object-fit: cover;
flex-shrink: 0;
}
.most-favorited-icon {
padding: 8px;
background: var(--bg-tertiary, #eeeeee);
}
.most-favorited-info {
flex: 1;
min-width: 0;
}
.most-favorited-info h4 {
margin: 0 0 0.5rem 0;
font-size: 1.1rem;
color: var(--text-primary, #1a1a1a);
}
.most-favorited-description {
margin: 0 0 0.5rem 0;
color: var(--text-secondary, #666);
font-size: 0.9rem;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
.most-favorited-count {
display: inline-block;
padding: 0.25rem 0.5rem;
background: var(--accent, #007bff);
color: var(--accent-text, #ffffff);
border-radius: 0.25rem;
font-size: 0.875rem;
font-weight: 500;
}
.pagination {
display: flex;
justify-content: center;
align-items: center;
gap: 1rem;
margin-top: 1.5rem;
padding-top: 1.5rem;
border-top: 1px solid var(--border-color, #e0e0e0);
}
.pagination button {
padding: 0.5rem 1rem;
background: var(--accent, #007bff);
color: var(--accent-text, #ffffff);
border: none;
border-radius: 0.25rem;
cursor: pointer;
font-weight: 500;
transition: all 0.2s ease;
}
.pagination button:hover:not(:disabled) {
background: var(--accent-hover, #0056b3);
transform: translateY(-1px);
}
.pagination button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.page-info {
color: var(--text-secondary, #666);
font-size: 0.9rem;
}
</style>

889
src/routes/users/[npub]/+page.svelte

File diff suppressed because it is too large Load Diff
Loading…
Cancel
Save