Spaces:
Paused
Paused
| 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'; | |
| // Helper function to extract a friendly domain name from a URL. | |
| const getDomainName = (url) => { | |
| try { | |
| const hostname = new URL(url).hostname; | |
| // Remove "www." if present. | |
| const domain = hostname.startsWith('www.') ? hostname.slice(4) : hostname; | |
| // Return the first part in title case. | |
| const parts = domain.split('.'); | |
| return parts[0].charAt(0).toUpperCase() + parts[0].slice(1); | |
| } catch (err) { | |
| return url; | |
| } | |
| }; | |
| export default function Sources({ sources, handleSourceClick }) { | |
| // "sources" prop is the payload passed from the parent. | |
| 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(); // record start time | |
| try { | |
| // Use sources.payload if it exists. | |
| 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(); | |
| // Backend returns {"result": [...]} | |
| setFetchedSources(data.result); | |
| } catch (err) { | |
| console.error("Error fetching sources:", err); | |
| setError("Error fetching sources."); | |
| } | |
| const elapsed = Date.now() - startTime; | |
| // Ensure that the loading state lasts at least 1 second. | |
| 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> | |
| ); | |
| } |