|
import * as React from 'react'; |
|
import { useState, useEffect, useCallback } from 'react'; |
|
import Box from '@mui/material/Box'; |
|
import Card from '@mui/material/Card'; |
|
import CardContent from '@mui/material/CardContent'; |
|
import Typography from '@mui/material/Typography'; |
|
import './Sources.css'; |
|
|
|
|
|
const getDomainName = (url) => { |
|
try { |
|
const hostname = new URL(url).hostname; |
|
|
|
const domain = hostname.startsWith('www.') ? hostname.slice(4) : hostname; |
|
|
|
const parts = domain.split('.'); |
|
return parts[0].charAt(0).toUpperCase() + parts[0].slice(1); |
|
} catch (err) { |
|
return url; |
|
} |
|
}; |
|
|
|
export default function Sources({ sources, handleSourceClick }) { |
|
|
|
const [fetchedSources, setFetchedSources] = useState([]); |
|
const [loading, setLoading] = useState(true); |
|
const [error, setError] = useState(null); |
|
|
|
const fetchSources = useCallback(async () => { |
|
setLoading(true); |
|
setError(null); |
|
const startTime = Date.now(); |
|
try { |
|
|
|
const bodyData = sources && sources.payload ? sources.payload : sources; |
|
const res = await fetch("/action/sources", { |
|
method: "POST", |
|
headers: { "Content-Type": "application/json" }, |
|
body: JSON.stringify(bodyData) |
|
}); |
|
const data = await res.json(); |
|
|
|
setFetchedSources(data.result); |
|
} catch (err) { |
|
console.error("Error fetching sources:", err); |
|
setError("Error fetching sources."); |
|
} |
|
const elapsed = Date.now() - startTime; |
|
|
|
if (elapsed < 500) { |
|
setTimeout(() => { |
|
setLoading(false); |
|
}, 500 - elapsed); |
|
} else { |
|
setLoading(false); |
|
} |
|
}, [sources]); |
|
|
|
useEffect(() => { |
|
if (sources) { |
|
fetchSources(); |
|
} |
|
}, [sources, fetchSources]); |
|
|
|
if (loading) { |
|
return ( |
|
<Box className="sources-container"> |
|
<Typography className="loading-sources" variant="body2">Loading Sources...</Typography> |
|
</Box> |
|
); |
|
} |
|
|
|
if (error) { |
|
return ( |
|
<Box className="sources-container"> |
|
<Typography variant="body2" color="error">{error}</Typography> |
|
</Box> |
|
); |
|
} |
|
|
|
return ( |
|
<Box className="sources-container"> |
|
{fetchedSources.map((source, index) => { |
|
const domain = getDomainName(source.link); |
|
let hostname = ''; |
|
try { |
|
hostname = new URL(source.link).hostname; |
|
} catch (err) { |
|
hostname = source.link; |
|
} |
|
return ( |
|
<Card |
|
key={index} |
|
variant="outlined" |
|
className="source-card" |
|
onClick={() => handleSourceClick(source)} |
|
> |
|
<CardContent> |
|
{/* Header/Title */} |
|
<Typography variant="h6" component="div" className="source-title"> |
|
{source.title} |
|
</Typography> |
|
{/* Link info: icon, domain, bullet, serial number */} |
|
<Typography variant="body2" className="source-link"> |
|
<img |
|
src={`https://www.google.com/s2/favicons?domain=${hostname}`} |
|
alt={domain} |
|
className="source-icon" |
|
/> |
|
<span className="source-domain">{domain}</span> |
|
<span className="separator"> • </span> |
|
<span className="source-serial">{index + 1}</span> |
|
</Typography> |
|
{/* Description */} |
|
<Typography variant="body2" className="source-description"> |
|
{source.description} |
|
</Typography> |
|
</CardContent> |
|
</Card> |
|
); |
|
})} |
|
</Box> |
|
); |
|
} |