Enhance deployment script and update package dependencies

- Added detailed labels to the deployment script for IndeedHub, including title, version, description, license, icon, and repository URL.
- Updated package dependencies in package.json and package-lock.json, including upgrading 'nostr-tools' to version 2.23.0 and adding 'axios' and '@tanstack/vue-query'.
- Improved README with a modern description of the platform and updated project structure details.

This commit enhances the clarity of the deployment process and ensures the project is using the latest dependencies for better performance and features.
This commit is contained in:
Dorian
2026-02-12 10:30:47 +00:00
parent dacfa7a822
commit c970f5b29f
43 changed files with 6906 additions and 603 deletions

View File

@@ -0,0 +1,90 @@
import { computed } from 'vue'
import { libraryService } from '../services/library.service'
import { subscriptionService } from '../services/subscription.service'
import { useAuthStore } from '../stores/auth'
/**
* Access Control Composable
* Check user access to content (subscription or rental)
*/
export function useAccess() {
const authStore = useAuthStore()
/**
* Check if user has access to specific content
*/
async function checkContentAccess(contentId: string): Promise<{
hasAccess: boolean
method?: 'subscription' | 'rental'
expiresAt?: string
}> {
if (!authStore.isAuthenticated) {
return { hasAccess: false }
}
// Check subscription first (instant check)
if (authStore.hasActiveSubscription()) {
return { hasAccess: true, method: 'subscription' }
}
// Check if we're in development mode
const useMockData = import.meta.env.VITE_USE_MOCK_DATA === 'true' || import.meta.env.DEV
if (useMockData) {
// In dev mode without subscription, no access (prompt rental)
return { hasAccess: false }
}
// Real API call to check rental
try {
return await libraryService.checkContentAccess(contentId)
} catch (error) {
console.error('Failed to check access:', error)
return { hasAccess: false }
}
}
/**
* Check if user has active subscription
*/
const hasActiveSubscription = computed(() => {
return authStore.hasActiveSubscription()
})
/**
* Get user's subscription tier
*/
async function getSubscriptionTier() {
if (!authStore.isAuthenticated) return null
try {
const subscription = await subscriptionService.getActiveSubscription()
return subscription?.tier || null
} catch {
return null
}
}
/**
* Check if content requires subscription
*/
function requiresSubscription(_content: any): boolean {
// All content requires subscription or rental unless explicitly free
return true
}
/**
* Check if content can be rented
*/
function canRent(_content: any): boolean {
return !!_content.rentalPrice && _content.rentalPrice > 0
}
return {
checkContentAccess,
hasActiveSubscription,
getSubscriptionTier,
requiresSubscription,
canRent,
}
}

View File

@@ -0,0 +1,68 @@
import { computed } from 'vue'
import { useAuthStore } from '../stores/auth'
import type { ApiUser } from '../types/api'
/**
* Auth Composable
* Provides reactive authentication state and methods
*/
export function useAuth() {
const authStore = useAuthStore()
// Reactive state
const user = computed<ApiUser | null>(() => authStore.user)
const isAuthenticated = computed(() => authStore.isAuthenticated)
const isLoading = computed(() => authStore.isLoading)
const authType = computed(() => authStore.authType)
const nostrPubkey = computed(() => authStore.nostrPubkey)
// Methods
const login = async (email: string, password: string) => {
return authStore.loginWithCognito(email, password)
}
const loginWithNostr = async (pubkey: string, signature: string, event: any) => {
return authStore.loginWithNostr(pubkey, signature, event)
}
const register = async (email: string, password: string, legalName: string) => {
return authStore.register(email, password, legalName)
}
const logout = async () => {
return authStore.logout()
}
const linkNostr = async (pubkey: string, signature: string) => {
return authStore.linkNostr(pubkey, signature)
}
const unlinkNostr = async () => {
return authStore.unlinkNostr()
}
// Computed getters
const isFilmmaker = computed(() => authStore.isFilmmaker())
const hasActiveSubscription = computed(() => authStore.hasActiveSubscription())
return {
// State
user,
isAuthenticated,
isLoading,
authType,
nostrPubkey,
// Methods
login,
loginWithNostr,
register,
logout,
linkNostr,
unlinkNostr,
// Getters
isFilmmaker,
hasActiveSubscription,
}
}

350
src/composables/useNostr.ts Normal file
View File

@@ -0,0 +1,350 @@
import { ref, computed, onUnmounted } from 'vue'
import { nostrClient } from '../lib/nostr'
import { getNostrContentIdentifier } from '../utils/mappers'
import { getMockComments, getMockReactions, getMockProfile, mockProfiles } from '../data/mockSocialData'
import type { Event as NostrEvent } from 'nostr-tools'
const useMockData = import.meta.env.VITE_USE_MOCK_DATA === 'true' || import.meta.env.DEV
/**
* Nostr Composable
* Reactive interface for Nostr features
* Uses mock data in development mode
*/
export function useNostr(contentId?: string) {
const comments = ref<NostrEvent[]>([])
const reactions = ref<NostrEvent[]>([])
const profiles = ref<Map<string, any>>(new Map())
const isLoading = ref(false)
const error = ref<string | null>(null)
let commentSub: any = null
let reactionSub: any = null
/**
* Fetch comments for content
*/
async function fetchComments(id: string = contentId!) {
if (!id) return
isLoading.value = true
error.value = null
try {
if (useMockData) {
// Simulate network delay
await new Promise((resolve) => setTimeout(resolve, 200))
const mockComments = getMockComments(id)
comments.value = mockComments as unknown as NostrEvent[]
// Populate profiles from mock data
mockComments.forEach((comment) => {
const profile = getMockProfile(comment.pubkey)
if (profile) {
profiles.value.set(comment.pubkey, {
name: profile.name,
picture: profile.picture,
about: profile.about,
})
}
})
return
}
const identifier = getNostrContentIdentifier(id)
const events = await nostrClient.getComments(identifier)
// Sort by timestamp (newest first)
comments.value = events.sort((a, b) => b.created_at - a.created_at)
// Fetch profiles for comment authors
await fetchProfiles(events.map((e) => e.pubkey))
} catch (err: any) {
error.value = err.message || 'Failed to fetch comments'
console.error('Nostr comments error:', err)
} finally {
isLoading.value = false
}
}
/**
* Fetch reactions for content
*/
async function fetchReactions(id: string = contentId!) {
if (!id) return
try {
if (useMockData) {
await new Promise((resolve) => setTimeout(resolve, 100))
reactions.value = getMockReactions(id) as unknown as NostrEvent[]
return
}
const identifier = getNostrContentIdentifier(id)
const events = await nostrClient.getReactions(identifier)
reactions.value = events
} catch (err: any) {
console.error('Nostr reactions error:', err)
}
}
/**
* Fetch user profiles
*/
async function fetchProfiles(pubkeys: string[]) {
const uniquePubkeys = [...new Set(pubkeys)]
await Promise.all(
uniquePubkeys.map(async (pubkey) => {
if (profiles.value.has(pubkey)) return
if (useMockData) {
const profile = getMockProfile(pubkey)
if (profile) {
profiles.value.set(pubkey, {
name: profile.name,
picture: profile.picture,
about: profile.about,
})
}
return
}
try {
const profileEvent = await nostrClient.getProfile(pubkey)
if (profileEvent) {
const metadata = JSON.parse(profileEvent.content)
profiles.value.set(pubkey, metadata)
}
} catch (err) {
console.error(`Failed to fetch profile for ${pubkey}:`, err)
}
})
)
}
/**
* Subscribe to real-time comments
*/
function subscribeToComments(id: string = contentId!) {
if (!id || commentSub) return
if (useMockData) {
// In mock mode, no real-time subscription needed
return
}
const identifier = getNostrContentIdentifier(id)
commentSub = nostrClient.subscribeToComments(
identifier,
(event) => {
comments.value = [event, ...comments.value]
fetchProfiles([event.pubkey])
}
)
}
/**
* Subscribe to real-time reactions
*/
function subscribeToReactions(id: string = contentId!) {
if (!id || reactionSub) return
if (useMockData) {
return
}
const identifier = getNostrContentIdentifier(id)
reactionSub = nostrClient.subscribeToReactions(
identifier,
(event) => {
reactions.value = [...reactions.value, event]
}
)
}
/**
* Post a comment
*/
async function postComment(content: string, id: string = contentId!) {
if (!id) {
throw new Error('Content ID required')
}
if (useMockData) {
// In mock mode, add the comment locally
const mockProfile = mockProfiles[0]
const newComment = {
id: Math.random().toString(36).slice(2).padEnd(64, '0'),
pubkey: mockProfile.pubkey,
content,
created_at: Math.floor(Date.now() / 1000),
kind: 1 as const,
tags: [['i', `https://indeedhub.com/content/${id}`, 'text']],
sig: '0'.repeat(128),
}
comments.value = [newComment as unknown as NostrEvent, ...comments.value]
if (!profiles.value.has(mockProfile.pubkey)) {
profiles.value.set(mockProfile.pubkey, {
name: mockProfile.name,
picture: mockProfile.picture,
about: mockProfile.about,
})
}
return newComment
}
if (!window.nostr) {
throw new Error('Nostr extension not available')
}
try {
const pubkey = await window.nostr.getPublicKey()
const identifier = getNostrContentIdentifier(id)
const event = {
kind: 1,
created_at: Math.floor(Date.now() / 1000),
tags: [
['i', identifier, 'text'],
],
content,
pubkey,
}
const signedEvent = await window.nostr.signEvent(event)
await nostrClient.publishEvent(signedEvent)
return signedEvent
} catch (err: any) {
throw new Error(err.message || 'Failed to post comment')
}
}
/**
* Post a reaction (+1 or -1)
*/
async function postReaction(positive: boolean, id: string = contentId!) {
if (!id) {
throw new Error('Content ID required')
}
if (useMockData) {
const mockProfile = mockProfiles[0]
const newReaction = {
id: Math.random().toString(36).slice(2).padEnd(64, '0'),
pubkey: mockProfile.pubkey,
content: positive ? '+' : '-',
created_at: Math.floor(Date.now() / 1000),
kind: 17 as const,
tags: [['i', `https://indeedhub.com/content/${id}`, 'text']],
sig: '0'.repeat(128),
}
reactions.value = [...reactions.value, newReaction as unknown as NostrEvent]
return newReaction
}
if (!window.nostr) {
throw new Error('Nostr extension not available')
}
try {
const pubkey = await window.nostr.getPublicKey()
const identifier = getNostrContentIdentifier(id)
const event = {
kind: 17,
created_at: Math.floor(Date.now() / 1000),
tags: [
['i', identifier, 'text'],
],
content: positive ? '+' : '-',
pubkey,
}
const signedEvent = await window.nostr.signEvent(event)
await nostrClient.publishEvent(signedEvent)
return signedEvent
} catch (err: any) {
throw new Error(err.message || 'Failed to post reaction')
}
}
/**
* Get reaction counts
*/
const reactionCounts = computed(() => {
const positive = reactions.value.filter((r) => r.content === '+').length
const negative = reactions.value.filter((r) => r.content === '-').length
return { positive, negative, total: positive - negative }
})
/**
* Get user's reaction
*/
async function getUserReaction(id: string = contentId!) {
if (!id) return null
if (useMockData) {
return null // Mock user has no existing reaction
}
if (!window.nostr) return null
try {
const pubkey = await window.nostr.getPublicKey()
const userReaction = reactions.value.find((r) => r.pubkey === pubkey)
return userReaction?.content || null
} catch {
return null
}
}
/**
* Cleanup subscriptions
*/
function cleanup() {
if (commentSub) commentSub.close()
if (reactionSub) reactionSub.close()
}
// Auto-cleanup on unmount
onUnmounted(() => {
cleanup()
})
return {
// State
comments,
reactions,
profiles,
isLoading,
error,
reactionCounts,
// Methods
fetchComments,
fetchReactions,
subscribeToComments,
subscribeToReactions,
postComment,
postReaction,
getUserReaction,
cleanup,
}
}
// Declare window.nostr for TypeScript
declare global {
interface Window {
nostr?: {
getPublicKey: () => Promise<string>
signEvent: (event: any) => Promise<any>
}
}
}

View File

@@ -0,0 +1,73 @@
import { ref } from 'vue'
interface Toast {
id: number
message: string
type: 'success' | 'error' | 'info' | 'warning'
duration: number
}
/**
* Toast Notification Composable
* Displays glassmorphic toast notifications
*/
export function useToast() {
const toasts = ref<Toast[]>([])
let nextId = 0
function showToast(
message: string,
type: Toast['type'] = 'info',
duration: number = 3000
) {
const toast: Toast = {
id: nextId++,
message,
type,
duration,
}
toasts.value.push(toast)
if (duration > 0) {
setTimeout(() => {
removeToast(toast.id)
}, duration)
}
return toast.id
}
function removeToast(id: number) {
const index = toasts.value.findIndex((t) => t.id === id)
if (index > -1) {
toasts.value.splice(index, 1)
}
}
function success(message: string, duration?: number) {
return showToast(message, 'success', duration)
}
function error(message: string, duration?: number) {
return showToast(message, 'error', duration)
}
function info(message: string, duration?: number) {
return showToast(message, 'info', duration)
}
function warning(message: string, duration?: number) {
return showToast(message, 'warning', duration)
}
return {
toasts,
showToast,
removeToast,
success,
error,
info,
warning,
}
}