Update package dependencies and enhance application structure

- Added several new dependencies related to the Applesauce library, including 'applesauce-accounts', 'applesauce-common', 'applesauce-core', 'applesauce-loaders', 'applesauce-relay', and 'applesauce-signers', all at version 5.1.0.
- Updated the development script in package.json to specify a port for Vite and added new seed scripts for profiles and activity.
- Removed outdated image files from the public directory to clean up unused assets.
- Enhanced the App.vue structure by integrating shared components like AppHeader and AuthModal for improved user experience.
- Refactored ContentDetailModal and MobileNav components to support new features and improve usability.

These changes improve the overall functionality and maintainability of the application while ensuring it utilizes the latest libraries for better performance.
This commit is contained in:
Dorian
2026-02-12 12:24:58 +00:00
parent c970f5b29f
commit 725896673c
42 changed files with 3767 additions and 1329 deletions

340
scripts/seed-activity.ts Normal file
View File

@@ -0,0 +1,340 @@
/**
* Seeds the local relay with reactions (kind 17) and comments (kind 1111)
* for all IndeeHub content, so the UI has real data to display.
*
* Run after seed-profiles.ts and with the relay already running.
*/
import { Relay } from 'applesauce-relay'
import { PrivateKeySigner } from 'applesauce-signers/signers/private-key-signer'
import {
TEST_PERSONAS,
TASTEMAKER_PERSONAS,
} from '../src/data/testPersonas.js'
const RELAY_URL = 'ws://localhost:7777'
const ORIGIN = 'http://localhost:5174'
// ── Content catalog (matching src/data/indeeHubFilms.ts) ──────────
const CONTENT = [
{ id: 'god-bless-bitcoin', title: 'God Bless Bitcoin' },
{ id: 'thethingswecarry', title: 'The Things We Carry' },
{ id: 'duel', title: 'Duel' },
{ id: '2b0d7349-c010-47a0-b584-49e1bf86ab2f', title: 'Hard Money' },
{ id: '665a4095-73b9-480d-a0a4-b2aafaf2bce4', title: 'Bitcoiners' },
{ id: '3c113b66-3bb5-4cac-90eb-965ecedc4aa2', title: 'Lekker Feeling' },
{ id: 'stranded', title: 'STRANDED' },
{ id: 'bbdb0178-0b96-4ab5-addf-ba1f029c1cb3', title: 'The Housing Bubble' },
{ id: '584f310b-2269-4b05-a09d-261a0a3c1f78', title: 'Menger' },
{ id: 'ef92cd99-7188-4c48-b4bf-0b31fdd8934e', title: 'Everybody Does It' },
{ id: 'e1bd64d6-63c9-4c91-8d91-c69f5376286e', title: 'Gods of Their Own Religion' },
{ id: 'forgingacountry', title: 'Forging a Country' },
{ id: 'home', title: 'HOME' },
{ id: 'e1f58162-9288-418e-803d-196dcde00782', title: 'Kismet' },
{ id: 'identity-theft', title: 'Identity Theft' },
{ id: 'comingto', title: 'Coming To' },
{ id: 'down-the-pch', title: 'Down the P.C.H.' },
{ id: '0cb9de15-566d-4130-b80c-d42e952bb803', title: 'Breaking Up Is Hard to Do' },
{ id: '24b6f7c6-8f56-40f2-831a-54f40b03c427', title: 'The Florist' },
{ id: '311f772f-6559-4982-8918-d0f4be9e1b76', title: 'Plastic Money' },
{ id: '5bd753b7-9ff1-4966-a1c4-b3b93c62ed5d', title: 'Time Traveling Thieves' },
{ id: '34f042bd-23d6-40f4-9707-4b3bb62fdd58', title: 'Little Billy' },
]
// ── helpers ──────────────────────────────────────────────────────
type Persona = { name: string; nsec: string; pubkey: string }
function contentUrl(contentId: string): string {
return `${ORIGIN}/content/${contentId}`
}
function pick<T>(arr: T[], n: number): T[] {
const shuffled = [...arr].sort(() => Math.random() - 0.5)
return shuffled.slice(0, n)
}
function randomInt(min: number, max: number): number {
return Math.floor(Math.random() * (max - min + 1)) + min
}
// ── personas ────────────────────────────────────────────────────
const allPersonas: Persona[] = [
...(TEST_PERSONAS as unknown as Persona[]),
...(TASTEMAKER_PERSONAS as unknown as Persona[]),
]
const tastemakers: Persona[] = TASTEMAKER_PERSONAS as unknown as Persona[]
const now = Math.floor(Date.now() / 1000)
const ONE_DAY = 86400
const ONE_WEEK = 7 * ONE_DAY
// Content subsets for different activity patterns
const topContent = CONTENT.slice(0, 8)
const midContent = CONTENT.slice(8, 16)
const trendingContent = pick(CONTENT.slice(0, 12), 5)
const tastemakerFaves = pick(CONTENT.slice(0, 10), 6)
// ── sample comments ─────────────────────────────────────────────
const POSITIVE_COMMENTS = [
'Absolutely incredible film. A masterpiece in every sense.',
'This movie changed my perspective on cinema. Must watch!',
'The cinematography alone is worth the price of admission.',
'One of the greatest performances I\'ve ever seen on screen.',
'Every frame is a painting. Stunning work.',
'I\'ve seen this at least 5 times and it gets better every watch.',
'The screenplay is tight, the pacing is perfect.',
'A landmark achievement in filmmaking.',
'This deserves every award it got and more.',
'Rewatched it last night — still holds up beautifully.',
'Such an important documentary. Everyone should see this.',
'The storytelling here is on another level.',
]
const MIXED_COMMENTS = [
'Good but I think it\'s a bit overrated honestly.',
'Solid film, though the third act drags a little.',
'Worth watching once for sure, but I wouldn\'t rewatch.',
'Technically impressive but emotionally I felt nothing.',
'The hype is a bit much, but it\'s still a decent movie.',
'Some great moments, but also some really slow stretches.',
'I can see why people love it, just not my cup of tea.',
'Better than I expected, worse than the reviews suggest.',
]
const NEGATIVE_COMMENTS = [
'I really don\'t understand the hype around this one.',
'Couldn\'t finish it. Way too slow for my taste.',
'Overrated. There are much better films in this genre.',
]
// ── publishing helper ───────────────────────────────────────────
async function publishEvent(
relay: Relay,
signer: PrivateKeySigner,
event: { kind: number; content: string; tags: string[][]; created_at: number },
label: string,
): Promise<boolean> {
const signed = await signer.signEvent(event)
try {
const res = await relay.publish(signed, { timeout: 5000 })
if (!res.ok) console.warn(`${label}: ${res.message}`)
return true
} catch (err) {
console.error(`${label}:`, err instanceof Error ? err.message : err)
return false
}
}
// ── seed reactions (kind 17) ────────────────────────────────────
async function seedReactions(relay: Relay) {
console.log('\n📊 Seeding reactions (kind 17)...')
let count = 0
// Top content: lots of positive reactions
for (const item of topContent) {
const voters = pick(allPersonas, randomInt(5, 10))
for (const persona of voters) {
const signer = PrivateKeySigner.fromKey(persona.nsec)
const emoji = Math.random() < 0.9 ? '+' : '-'
const age = randomInt(1 * ONE_DAY, 30 * ONE_DAY)
const ok = await publishEvent(relay, signer, {
kind: 17,
content: emoji,
tags: [
['i', contentUrl(item.id)],
['k', 'web'],
],
created_at: now - age,
}, `reaction ${persona.name}->${item.title}`)
if (ok) count++
}
}
// Mid content: moderate reactions, more mixed
for (const item of pick(midContent, 5)) {
const voters = pick(allPersonas, randomInt(2, 5))
for (const persona of voters) {
const signer = PrivateKeySigner.fromKey(persona.nsec)
const emoji = Math.random() < 0.6 ? '+' : '-'
const age = randomInt(2 * ONE_DAY, 60 * ONE_DAY)
const ok = await publishEvent(relay, signer, {
kind: 17,
content: emoji,
tags: [
['i', contentUrl(item.id)],
['k', 'web'],
],
created_at: now - age,
}, `reaction ${persona.name}->${item.title}`)
if (ok) count++
}
}
// Trending content: recent reactions
for (const item of trendingContent) {
const voters = pick(allPersonas, randomInt(4, 8))
for (const persona of voters) {
const signer = PrivateKeySigner.fromKey(persona.nsec)
const age = randomInt(0, ONE_WEEK)
const ok = await publishEvent(relay, signer, {
kind: 17,
content: '+',
tags: [
['i', contentUrl(item.id)],
['k', 'web'],
],
created_at: now - age,
}, `trending-reaction ${persona.name}->${item.title}`)
if (ok) count++
}
}
// Tastemaker-specific reactions
for (const item of tastemakerFaves) {
const voters = pick(tastemakers, randomInt(2, 5))
for (const persona of voters) {
const signer = PrivateKeySigner.fromKey(persona.nsec)
const age = randomInt(0, 14 * ONE_DAY)
const ok = await publishEvent(relay, signer, {
kind: 17,
content: '+',
tags: [
['i', contentUrl(item.id)],
['k', 'web'],
],
created_at: now - age,
}, `tastemaker-reaction ${persona.name}->${item.title}`)
if (ok) count++
}
}
console.log(`${count} reactions seeded`)
}
// ── seed comments (kind 1111) ───────────────────────────────────
async function seedComments(relay: Relay) {
console.log('\n💬 Seeding comments (kind 1111)...')
let count = 0
// Top content: several comments
for (const item of topContent) {
const url = contentUrl(item.id)
const commenters = pick(allPersonas, randomInt(2, 5))
for (const persona of commenters) {
const signer = PrivateKeySigner.fromKey(persona.nsec)
const comments =
Math.random() < 0.7 ? POSITIVE_COMMENTS : MIXED_COMMENTS
const content = comments[randomInt(0, comments.length - 1)]
const age = randomInt(1 * ONE_DAY, 30 * ONE_DAY)
const ok = await publishEvent(relay, signer, {
kind: 1111,
content,
tags: [
['I', url],
['K', 'web'],
['i', url],
['k', 'web'],
],
created_at: now - age,
}, `comment ${persona.name}->${item.title}`)
if (ok) count++
}
}
// Mid content: occasional comments
for (const item of pick(midContent, 4)) {
const url = contentUrl(item.id)
const commenters = pick(allPersonas, randomInt(1, 2))
for (const persona of commenters) {
const signer = PrivateKeySigner.fromKey(persona.nsec)
const pool = [...MIXED_COMMENTS, ...NEGATIVE_COMMENTS]
const content = pool[randomInt(0, pool.length - 1)]
const age = randomInt(3 * ONE_DAY, 45 * ONE_DAY)
const ok = await publishEvent(relay, signer, {
kind: 1111,
content,
tags: [
['I', url],
['K', 'web'],
['i', url],
['k', 'web'],
],
created_at: now - age,
}, `comment ${persona.name}->${item.title}`)
if (ok) count++
}
}
// Tastemaker reviews on their faves
for (const item of tastemakerFaves.slice(0, 4)) {
const url = contentUrl(item.id)
const reviewers = pick(tastemakers, randomInt(1, 3))
for (const persona of reviewers) {
const signer = PrivateKeySigner.fromKey(persona.nsec)
const content = POSITIVE_COMMENTS[randomInt(0, POSITIVE_COMMENTS.length - 1)]
const age = randomInt(0, 10 * ONE_DAY)
const ok = await publishEvent(relay, signer, {
kind: 1111,
content,
tags: [
['I', url],
['K', 'web'],
['i', url],
['k', 'web'],
],
created_at: now - age,
}, `tastemaker-comment ${persona.name}->${item.title}`)
if (ok) count++
}
}
// Trending content: recent comments
for (const item of trendingContent.slice(0, 3)) {
const url = contentUrl(item.id)
const commenters = pick(allPersonas, randomInt(2, 4))
for (const persona of commenters) {
const signer = PrivateKeySigner.fromKey(persona.nsec)
const content = POSITIVE_COMMENTS[randomInt(0, POSITIVE_COMMENTS.length - 1)]
const age = randomInt(0, 3 * ONE_DAY)
const ok = await publishEvent(relay, signer, {
kind: 1111,
content,
tags: [
['I', url],
['K', 'web'],
['i', url],
['k', 'web'],
],
created_at: now - age,
}, `trending-comment ${persona.name}->${item.title}`)
if (ok) count++
}
}
console.log(`${count} comments seeded`)
}
// ── main ────────────────────────────────────────────────────────
async function main() {
console.log('🎬 Seeding activity data into relay at', RELAY_URL)
const relay = new Relay(RELAY_URL)
await seedReactions(relay)
await seedComments(relay)
console.log('\n✅ Done! Activity seeded successfully.')
setTimeout(() => process.exit(0), 1000)
}
main().catch((err) => {
console.error('Fatal:', err)
process.exit(1)
})

63
scripts/seed-profiles.ts Normal file
View File

@@ -0,0 +1,63 @@
import { Relay } from 'applesauce-relay'
import { PrivateKeySigner } from 'applesauce-signers/signers/private-key-signer'
import {
TEST_PERSONAS,
TASTEMAKER_PERSONAS,
} from '../src/data/testPersonas.js'
const RELAY_URL = 'ws://localhost:7777'
type Persona = { name: string; nsec: string; pubkey: string }
function buildProfile(persona: Persona, role: 'test' | 'tastemaker') {
const about =
role === 'tastemaker'
? `${persona.name} — tastemaker for IndeeHub`
: `${persona.name} — test persona for IndeeHub`
return {
name: persona.name,
display_name: persona.name,
about,
picture: `https://robohash.org/${persona.pubkey}.png`,
bot: true,
}
}
async function seedProfiles() {
const relay = new Relay(RELAY_URL)
const allPersonas: { persona: Persona; role: 'test' | 'tastemaker' }[] = [
...TEST_PERSONAS.map((p) => ({ persona: p as Persona, role: 'test' as const })),
...TASTEMAKER_PERSONAS.map((p) => ({ persona: p as Persona, role: 'tastemaker' as const })),
]
const now = Math.floor(Date.now() / 1000)
for (const { persona, role } of allPersonas) {
const signer = PrivateKeySigner.fromKey(persona.nsec)
const profile = buildProfile(persona, role)
const signed = await signer.signEvent({
kind: 0,
created_at: now,
tags: [],
content: JSON.stringify(profile),
})
try {
const res = await relay.publish(signed, { timeout: 5000 })
console.log(`${persona.name} (${role}): ${res.ok ? 'OK' : res.message}`)
} catch (err) {
console.error(`${persona.name} (${role}):`, err)
}
}
// Give relay time to flush, then exit
setTimeout(() => process.exit(0), 1000)
}
seedProfiles().catch((err) => {
console.error('Fatal:', err)
process.exit(1)
})

75
scripts/start.sh Executable file
View File

@@ -0,0 +1,75 @@
#!/usr/bin/env bash
set -e
RELAY_PORT=7777
RELAY_URL="ws://localhost:$RELAY_PORT"
VITE_PORT=5174
cleanup() {
echo ""
echo "Shutting down..."
# Kill background jobs (relay)
kill $(jobs -p) 2>/dev/null
exit 0
}
trap cleanup SIGINT SIGTERM
# Check that nak is installed
if ! command -v nak &>/dev/null; then
echo "Error: 'nak' is not installed."
echo "Install with: brew install nak"
exit 1
fi
# Kill any existing process on the relay port
if lsof -i :$RELAY_PORT -P &>/dev/null; then
echo "Port $RELAY_PORT is already in use, killing existing process..."
lsof -ti :$RELAY_PORT | xargs kill -9 2>/dev/null
sleep 1
fi
# Start the local relay in the background
echo "Starting local Nostr relay on port $RELAY_PORT..."
nak serve --port $RELAY_PORT &
RELAY_PID=$!
# Wait for the relay to be ready
echo "Waiting for relay to be ready..."
for i in $(seq 1 30); do
if curl -s -o /dev/null http://localhost:$RELAY_PORT 2>/dev/null; then
echo "Relay is ready at $RELAY_URL (pid $RELAY_PID)"
break
fi
if ! kill -0 $RELAY_PID 2>/dev/null; then
echo "Error: relay process died unexpectedly"
exit 1
fi
sleep 0.5
done
# Verify relay is actually responding
if ! curl -s -o /dev/null http://localhost:$RELAY_PORT 2>/dev/null; then
echo "Error: relay did not start in time"
kill $RELAY_PID 2>/dev/null
exit 1
fi
# Seed test profiles and activity
echo ""
echo "Seeding test profiles..."
npx tsx scripts/seed-profiles.ts
echo ""
echo "Seeding activity (reactions & comments)..."
npx tsx scripts/seed-activity.ts
echo ""
# Start the Vite dev server (in foreground so Ctrl+C works)
echo "Starting dev server..."
echo "============================================"
echo " Relay: $RELAY_URL (pid $RELAY_PID)"
echo " App: http://localhost:$VITE_PORT"
echo " Press Ctrl+C to stop everything"
echo "============================================"
echo ""
npx vite --port $VITE_PORT