fixed_discripancies_observed

This commit is contained in:
kenilkb 2026-07-17 17:46:48 +05:30
parent 9646309a0a
commit b2853a952b
4 changed files with 193 additions and 19 deletions

View File

@ -134,8 +134,20 @@ export class EcosystemController {
try {
const data = req.body;
// Auto-extract YouTube thumbnail if not provided
let title = data.title;
let description = data.description || null;
let thumbnailUrl = data.thumbnailUrl || null;
if ((!title || !description || !thumbnailUrl) && data.youtubeUrl) {
const meta = await this.scrapeMetaInternal(data.youtubeUrl);
if (meta) {
if (!title) title = meta.title;
if (!description) description = meta.description || null;
if (!thumbnailUrl) thumbnailUrl = meta.thumbnailUrl || null;
}
}
// Auto-extract YouTube thumbnail if not provided and it's YouTube
if (!thumbnailUrl && data.youtubeUrl) {
const videoId = this.extractYouTubeVideoId(data.youtubeUrl);
if (videoId) {
@ -145,8 +157,8 @@ export class EcosystemController {
const created = await prisma.contentShowcase.create({
data: {
title: data.title,
description: data.description || null,
title: title || 'Media Showcase Item',
description,
youtubeUrl: data.youtubeUrl,
thumbnailUrl,
redirectUrl: data.redirectUrl || null,
@ -167,9 +179,20 @@ export class EcosystemController {
const { id } = req.params;
const data = req.body;
// Auto-extract YouTube thumbnail if URL changed and no explicit thumbnail
let title = data.title;
let description = data.description;
let thumbnailUrl = data.thumbnailUrl;
if (thumbnailUrl === undefined && data.youtubeUrl) {
if (data.youtubeUrl) {
const meta = await this.scrapeMetaInternal(data.youtubeUrl);
if (meta) {
if (title === undefined || title === null || title === '') title = meta.title;
if (description === undefined) description = meta.description || null;
if (thumbnailUrl === undefined) thumbnailUrl = meta.thumbnailUrl || null;
}
}
if (!thumbnailUrl && data.youtubeUrl) {
const videoId = this.extractYouTubeVideoId(data.youtubeUrl);
if (videoId) {
thumbnailUrl = `https://img.youtube.com/vi/${videoId}/maxresdefault.jpg`;
@ -179,8 +202,8 @@ export class EcosystemController {
const updated = await prisma.contentShowcase.update({
where: { id },
data: {
title: data.title,
description: data.description,
title,
description,
youtubeUrl: data.youtubeUrl,
thumbnailUrl,
redirectUrl: data.redirectUrl,
@ -210,6 +233,21 @@ export class EcosystemController {
// Helper: Extract video ID from various YouTube URL formats
private extractYouTubeVideoId(url: string): string | null {
try {
const urlObj = new URL(url);
if (urlObj.hostname.includes('youtu.be')) {
return urlObj.pathname.slice(1).split(/[?#]/)[0];
}
if (urlObj.pathname.includes('/shorts/') || urlObj.pathname.includes('/embed/')) {
const parts = urlObj.pathname.split('/');
return parts.pop()?.split(/[?#]/)[0] || null;
}
if (urlObj.searchParams.has('v')) {
return urlObj.searchParams.get('v');
}
} catch (e) {
// Fallback to regex
}
const patterns = [
/(?:youtube\.com\/watch\?v=|youtu\.be\/|youtube\.com\/embed\/|youtube\.com\/shorts\/)([a-zA-Z0-9_-]{11})/,
/(?:youtube-nocookie\.com\/embed\/)([a-zA-Z0-9_-]{11})/,
@ -221,6 +259,113 @@ export class EcosystemController {
return null;
}
private async scrapeMetaInternal(url: string): Promise<{ title?: string; description?: string; thumbnailUrl?: string } | null> {
try {
const normUrl = url.trim();
const lowerUrl = normUrl.toLowerCase();
// Case 1: Twitter / X
if (lowerUrl.includes('twitter.com') || lowerUrl.includes('x.com')) {
const oembedUrl = `https://publish.twitter.com/oembed?url=${encodeURIComponent(normUrl)}`;
const resOembed = await fetch(oembedUrl);
if (resOembed.ok) {
const data = await resOembed.json() as any;
let description = '';
if (data.html) {
const pMatch = data.html.match(/<p[^>]*>([\s\S]*?)<\/p>/i);
if (pMatch) {
description = pMatch[1].replace(/<[^>]*>/g, '').trim();
}
}
return {
title: data.author_name ? `Tweet by ${data.author_name}` : 'Tweet Content',
description,
thumbnailUrl: undefined
};
}
}
// Case 2: Instagram
if (lowerUrl.includes('instagram.com')) {
const oembedUrl = `https://graph.facebook.com/v25.0/instagram_oembed?url=${encodeURIComponent(normUrl)}`;
const resOembed = await fetch(oembedUrl);
if (resOembed.ok) {
const data = await resOembed.json() as any;
return {
title: data.author_name ? `Instagram post by ${data.author_name}` : 'Instagram Post/Reel',
description: data.title || 'Instagram Content',
thumbnailUrl: data.thumbnail_url || undefined
};
}
}
// Case 3: YouTube
const videoId = this.extractYouTubeVideoId(normUrl);
if (videoId) {
let title = '';
let thumbnailUrl = `https://img.youtube.com/vi/${videoId}/maxresdefault.jpg`;
try {
const oembedUrl = `https://www.youtube.com/oembed?url=https://www.youtube.com/watch?v=${videoId}&format=json`;
const oembedRes = await fetch(oembedUrl);
if (oembedRes.ok) {
const data = await oembedRes.json() as any;
title = data.title || '';
thumbnailUrl = data.thumbnail_url || thumbnailUrl;
}
} catch (err) {
console.error('oEmbed fetch error:', err);
}
let description = '';
try {
const watchUrl = `https://www.youtube.com/watch?v=${videoId}`;
const pageRes = await fetch(watchUrl, {
headers: {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.0.0 Safari/537.36',
'Accept-Language': 'en-US,en;q=0.9'
}
});
if (pageRes.ok) {
const html = await pageRes.text();
const playerResponse = this.extractJsonFromHtml(html, 'ytInitialPlayerResponse = ');
if (playerResponse && playerResponse.videoDetails) {
description = playerResponse.videoDetails.shortDescription || '';
if (playerResponse.videoDetails.title && !title) {
title = playerResponse.videoDetails.title;
}
}
if (!description) {
const descMatch = html.match(/<meta\s+property="og:description"\s+content="([^"]*)"/i) ||
html.match(/<meta\s+name="description"\s+content="([^"]*)"/i);
if (descMatch && descMatch[1]) {
description = descMatch[1]
.replace(/&quot;/g, '"')
.replace(/&amp;/g, '&')
.replace(/&lt;/g, '<')
.replace(/&gt;/g, '>')
.replace(/&#39;/g, "'")
.replace(/&apos;/g, "'");
}
}
}
} catch (err) {
console.error('HTML scrape error:', err);
}
return {
title: title || undefined,
description: description || undefined,
thumbnailUrl
};
}
} catch (e) {
console.error('scrapeMetaInternal error:', e);
}
return null;
}
private extractJsonFromHtml(html: string, targetStr: string): any {
const idx = html.indexOf(targetStr);
if (idx === -1) return null;

View File

@ -153,7 +153,7 @@ export const AssetCard: React.FC<AssetCardProps> = ({
}
onViewDetails(asset);
}}
className={`group bg-ink-0 border rounded-xl p-4 transition-all duration-300 flex flex-col justify-between cursor-pointer min-h-[410px] ${
className={`group bg-ink-0 border rounded-xl p-4 transition-[border-color,box-shadow,background-color] duration-300 flex flex-col justify-between cursor-pointer min-h-[410px] ${
isExpanded
? 'absolute z-20 top-0 left-0 right-0 h-auto shadow-2xl border-ink-300 bg-ink-0'
: isRecommended

View File

@ -16,6 +16,21 @@ import type { ContentShowcase } from '../services/ecosystem-api';
// ── Helper: Extract YouTube video ID ──
const extractYouTubeVideoId = (url: string): string | null => {
try {
const urlObj = new URL(url);
if (urlObj.hostname.includes('youtu.be')) {
return urlObj.pathname.slice(1).split(/[?#]/)[0];
}
if (urlObj.pathname.includes('/shorts/') || urlObj.pathname.includes('/embed/')) {
const parts = urlObj.pathname.split('/');
return parts.pop()?.split(/[?#]/)[0] || null;
}
if (urlObj.searchParams.has('v')) {
return urlObj.searchParams.get('v');
}
} catch (e) {
// Fallback to regex
}
const patterns = [
/(?:youtube\.com\/watch\?v=|youtu\.be\/|youtube\.com\/embed\/|youtube\.com\/shorts\/)([a-zA-Z0-9_-]{11})/,
/(?:youtube-nocookie\.com\/embed\/)([a-zA-Z0-9_-]{11})/,
@ -107,13 +122,13 @@ interface VideoDescriptionProps {
}
const VideoDescription: React.FC<VideoDescriptionProps> = ({ text, isExpanded, onToggleExpand }) => {
const shouldCollapse = text.length > 180 || text.includes('\n');
const shouldCollapse = text.length > 120 || text.includes('\n');
return (
<div className="space-y-2">
<div className="space-y-1">
<p
className={`text-[11px] font-medium text-ink-500 leading-relaxed transition-all duration-300 ${
isExpanded ? '' : 'line-clamp-3'
className={`text-[11px] font-medium text-ink-500 leading-relaxed overflow-hidden ${
isExpanded ? '' : 'line-clamp-2'
}`}
style={{ whiteSpace: isExpanded ? 'pre-wrap' : 'normal' }}
>
@ -195,7 +210,7 @@ export const ShowcasePage: React.FC = () => {
<p className="text-xs text-ink-500 mt-1 max-w-sm">There are no featured videos available in the showcase right now. Check back later!</p>
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-6 items-start animate-fadeIn">
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-6 animate-fadeIn">
{items.map((item) => {
const ytId = extractYouTubeVideoId(item.youtubeUrl);
const isIg = item.youtubeUrl.includes('instagram.com');
@ -204,11 +219,11 @@ export const ShowcasePage: React.FC = () => {
const isExpanded = expandedItemId === item.id;
return (
<div key={item.id} className="relative min-h-[400px]">
<div key={item.id} className="relative h-[410px] w-full flex flex-col">
<motion.div
layout
transition={{ type: "spring", stiffness: 320, damping: 28 }}
className={`group flex flex-col bg-ink-0 border rounded-2xl overflow-hidden transition-all duration-300 ${
className={`group flex flex-col bg-ink-0 border rounded-2xl overflow-hidden transition-[border-color,box-shadow,background-color] duration-300 ${
isExpanded
? 'absolute z-20 top-0 left-0 right-0 h-auto shadow-2xl border-ink-350 bg-ink-0'
: 'relative w-full h-full border-ink-200 hover:border-ink-350 hover:shadow-xl'
@ -271,7 +286,7 @@ export const ShowcasePage: React.FC = () => {
{/* Content Info */}
<div className="p-5 flex-grow flex flex-col justify-between space-y-4">
<div className="space-y-2">
<h3 className="text-sm font-black tracking-tight text-ink-900 leading-snug">
<h3 className={`text-sm font-black tracking-tight text-ink-900 leading-snug ${isExpanded ? '' : 'line-clamp-2'}`} title={item.title}>
{item.title}
</h3>
{item.description && (

View File

@ -30,6 +30,7 @@ import {
getYoutubeMeta,
} from '../../services/ecosystem-api';
import type { EcosystemOffering, ContentShowcase } from '../../services/ecosystem-api';
import { useToast } from '../../hooks/use-toast';
// Custom Brand Logo renderer with remote URL preset load + high-fidelity SVG fallback
export const BrandLogo: React.FC<{ name: string; className?: string }> = ({ name, className = "h-7" }) => {
@ -126,6 +127,7 @@ const extractYouTubeVideoId = (url: string): string | null => {
// ── Content Showcase Manager Component ──
const ContentShowcaseManager: React.FC = () => {
const { success, error } = useToast();
const [items, setItems] = useState<ContentShowcase[]>([]);
const [loading, setLoading] = useState(true);
const [isModalOpen, setIsModalOpen] = useState(false);
@ -154,7 +156,10 @@ const ContentShowcaseManager: React.FC = () => {
// Action to fetch metadata explicitly
const handleScrapeMetadata = async () => {
const url = scYoutubeUrl.trim();
if (!url) return;
if (!url) {
error('URL Required', 'Please enter a media URL first.');
return;
}
const lower = url.toLowerCase();
const isYt = lower.includes('youtube.com') || lower.includes('youtu.be') || lower.includes('youtube-nocookie.com');
@ -162,7 +167,7 @@ const ContentShowcaseManager: React.FC = () => {
const isTw = lower.includes('twitter.com') || lower.includes('x.com');
if (!isYt && !isIg && !isTw) {
setFormError('Please enter a valid YouTube, Instagram, or Twitter/X URL.');
error('Invalid URL', 'Please enter a valid YouTube, Instagram, or Twitter/X URL.');
return;
}
@ -175,8 +180,10 @@ const ContentShowcaseManager: React.FC = () => {
if (meta.thumbnailUrl) {
setScThumbnailUrl(meta.thumbnailUrl);
}
success('Metadata Fetched', 'Media title, description, and thumbnail populated successfully.');
} catch (err: any) {
console.error('Failed to fetch video meta:', err);
error('Fetch Failed', 'Failed to retrieve metadata. You can input fields manually.');
setFormError('Failed to fetch video metadata. You can enter the title and description manually.');
} finally {
setIsFetchingMeta(false);
@ -206,7 +213,9 @@ const ContentShowcaseManager: React.FC = () => {
try {
const res = await uploadEcosystemFile(file);
setScThumbnailUrl(res.url);
success('Upload Success', 'Thumbnail image uploaded successfully.');
} catch (err: any) {
error('Upload Failed', 'Failed to upload thumbnail image.');
setFormError('Failed to upload thumbnail image.');
} finally {
setUploadingThumbnail(false);
@ -276,12 +285,15 @@ const ContentShowcaseManager: React.FC = () => {
if (editingItem) {
await updateShowcaseContent(editingItem.id, payload);
success('Showcase Updated', 'The showcase item has been updated successfully.');
} else {
await createShowcaseContent(payload);
success('Showcase Created', 'A new showcase item has been created successfully.');
}
setIsModalOpen(false);
fetchItems();
} catch (err: any) {
error('Save Failed', err.response?.data?.error || 'Failed to save content.');
setFormError(err.response?.data?.error || 'Failed to save content.');
} finally {
setIsSaving(false);
@ -294,14 +306,16 @@ const ContentShowcaseManager: React.FC = () => {
await deleteShowcaseContent(deleteItem.id);
setDeleteItem(null);
fetchItems();
success('Showcase Deleted', 'The showcase item was deleted successfully.');
} catch (err) {
console.error('Failed to delete:', err);
error('Delete Failed', 'Failed to delete the showcase item.');
}
};
// Auto-thumbnail from the current YouTube URL field
const previewVideoId = extractYouTubeVideoId(scYoutubeUrl);
const previewThumbnail = previewVideoId ? `https://img.youtube.com/vi/${previewVideoId}/mqdefault.jpg` : null;
const previewThumbnail = scThumbnailUrl || (previewVideoId ? `https://img.youtube.com/vi/${previewVideoId}/mqdefault.jpg` : null);
return (
<div className="space-y-4 pt-2 border-t border-ink-200 mt-6">