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:
90
src/composables/useAccess.ts
Normal file
90
src/composables/useAccess.ts
Normal 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,
|
||||
}
|
||||
}
|
||||
68
src/composables/useAuth.ts
Normal file
68
src/composables/useAuth.ts
Normal 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
350
src/composables/useNostr.ts
Normal 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>
|
||||
}
|
||||
}
|
||||
}
|
||||
73
src/composables/useToast.ts
Normal file
73
src/composables/useToast.ts
Normal 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,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user