Add share discoveries feature with public share links
- Add single and batch share endpoints with signed URL tokens - Add public view endpoints (no auth required) for shared recommendations - Add share button with clipboard copy to RecommendationCard - Create SharedView page with Vynl branding and registration CTA - Add /shared/:recId/:token public route in App.tsx - Add shareRecommendation and getSharedRecommendation API functions
This commit is contained in:
@@ -1,5 +1,8 @@
|
||||
import { Heart, ExternalLink, Music, ThumbsDown } from 'lucide-react'
|
||||
import { useState } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { Heart, ExternalLink, Music, ThumbsDown, Repeat, Share2, Check } from 'lucide-react'
|
||||
import type { RecommendationItem } from '../lib/api'
|
||||
import { shareRecommendation } from '../lib/api'
|
||||
|
||||
interface Props {
|
||||
recommendation: RecommendationItem
|
||||
@@ -10,6 +13,30 @@ interface Props {
|
||||
}
|
||||
|
||||
export default function RecommendationCard({ recommendation, onToggleSave, onDislike, saving, disliking }: Props) {
|
||||
const navigate = useNavigate()
|
||||
const [sharing, setSharing] = useState(false)
|
||||
const [shared, setShared] = useState(false)
|
||||
|
||||
const handleMoreLikeThis = () => {
|
||||
const q = `find songs similar to ${recommendation.artist} - ${recommendation.title}`
|
||||
navigate(`/discover?q=${encodeURIComponent(q)}`)
|
||||
}
|
||||
|
||||
const handleShare = async () => {
|
||||
if (sharing) return
|
||||
setSharing(true)
|
||||
try {
|
||||
const { share_url } = await shareRecommendation(recommendation.id)
|
||||
await navigator.clipboard.writeText(share_url)
|
||||
setShared(true)
|
||||
setTimeout(() => setShared(false), 2000)
|
||||
} catch {
|
||||
// Fallback: if clipboard fails, silently ignore
|
||||
} finally {
|
||||
setSharing(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="bg-white rounded-2xl border border-purple-100 shadow-sm hover:shadow-md transition-shadow overflow-hidden">
|
||||
<div className="flex gap-4 p-5">
|
||||
@@ -80,6 +107,27 @@ export default function RecommendationCard({ recommendation, onToggleSave, onDis
|
||||
</button>
|
||||
)}
|
||||
|
||||
<button
|
||||
onClick={handleShare}
|
||||
disabled={sharing}
|
||||
className={`p-2 rounded-full transition-colors cursor-pointer border-none ${
|
||||
shared
|
||||
? 'bg-green-50 text-green-600'
|
||||
: 'bg-purple-50 text-purple-600 hover:bg-purple-100 hover:text-purple-700'
|
||||
} ${sharing ? 'opacity-50 cursor-not-allowed' : ''}`}
|
||||
title={shared ? 'Link copied!' : 'Share'}
|
||||
>
|
||||
{shared ? <Check className="w-4 h-4" /> : <Share2 className="w-4 h-4" />}
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={handleMoreLikeThis}
|
||||
className="p-2 rounded-full bg-purple-50 text-purple-600 hover:bg-purple-100 hover:text-purple-700 transition-colors cursor-pointer border-none"
|
||||
title="More like this"
|
||||
>
|
||||
<Repeat className="w-4 h-4" />
|
||||
</button>
|
||||
|
||||
{recommendation.youtube_url && (
|
||||
<a
|
||||
href={recommendation.youtube_url}
|
||||
|
||||
Reference in New Issue
Block a user