<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Gemini Data Dashboard</title>
<!-- Tailwind CSS CDN -->
<script src="https://cdn.tailwindcss.com"></script>
<!-- Inter Font -->
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<style>
body {
font-family: 'Inter', sans-serif;
}
/* Custom styles for print, to ensure charts are visible */
@media print {
body {
-webkit-print-color-adjust: exact !important;
color-adjust: exact !important;
}
.no-print {
display: none !important;
}
}
</style>
</head>
<body class="bg-gray-100">
<div id="root"></div>
<!-- React CDN -->
<script crossorigin src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
<!-- Babel for JSX transformation -->
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<!-- PapaParse CDN for CSV parsing -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/PapaParse/5.3.0/papaparse.min.js"></script>
<!-- Plotly.js CDN for charting -->
<script src="https://cdn.plot.ly/plotly-2.30.0.min.js"></script>
<!-- html2canvas for capturing HTML as image -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js"></script>
<!-- jsPDF for creating PDF from image -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js"></script>
<script type="text/babel">
// --- API Key Configuration ---
// WARNING: For production, secure your API key using a backend or environment variables.
// Embedding it directly in client-side code is for demonstration purposes only.
const GEMINI_API_KEY = "AIzaSyC7SjhHIrA2VrmuYcTYFhCXR2Ffg09L2_Y";
// Main App Component
const App = () => {
const [data, setData] = React.useState([]);
const [filteredData, setFilteredData] = React.useState([]);
const [isLoading, setIsLoading] = React.useState(false);
const [error, setError] = React.useState(null);
const [filters, setFilters] = React.useState({
platform: 'All',
sentiment: 'All',
mediaType: 'All',
location: 'All',
startDate: '',
endDate: '',
});
const [uniqueOptions, setUniqueOptions] = React.useState({
platforms: [],
sentiments: [],
mediaTypes: [],
locations: [],
});
// State for dynamic insights
const [campaignSummaryText, setCampaignSummaryText] = React.useState("Unggah data untuk menghasilkan ringkasan strategi kampanye.");
const [sentimentInsightsText, setSentimentInsightsText] = React.useState("Menunggu data dan AI untuk menghasilkan insight.");
const [engagementInsightsText, setEngagementInsightsText] = React.useState("Menunggu data dan AI untuk menghasilkan insight.");
const [platformInsightsText, setPlatformInsightsText] = React.useState("Menunggu data dan AI untuk menghasilkan insight.");
const [mediaTypeInsightsText, setMediaTypeInsightsText] = React.useState("Menunggu data dan AI untuk menghasilkan insight.");
const [locationInsightsText, setLocationInsightsText] = React.useState("Menunggu data dan AI untuk menghasilkan insight.");
const groupName = "Kelompok Gemini Insight"; // Group Name
// Refs for Plotly charts
const sentimentPieRef = React.useRef(null);
const engagementLineRef = React.useRef(null);
const platformBarRef = React.useRef(null);
const mediaPieRef = React.useRef(null);
const locationBarRef = React.useRef(null);
const dashboardRef = React.useRef(null); // Ref for the entire dashboard for PDF export
// --- Gemini API Call Function ---
const getGeminiResponse = async (prompt_text) => {
if (!GEMINI_API_KEY) {
return "API Key tidak tersedia untuk menghasilkan insight.";
}
const apiUrl = `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key=${GEMINI_API_KEY}`;
const headers = {
'Content-Type': 'application/json'
};
const payload = {
contents: [{ role: "user", parts: [{ text: prompt_text }] }]
};
try {
const response = await fetch(apiUrl, {
method: 'POST',
headers: headers,
body: JSON.stringify(payload)
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(`HTTP error! status: ${response.status}, message: ${JSON.stringify(errorData)}`);
}
const result = await response.json();
if (result.candidates && result.candidates.length > 0 &&
result.candidates[0].content && result.candidates[0].content.parts &&
result.candidates[0].content.parts.length > 0) {
return result.candidates[0].content.parts[0].text;
} else {
console.error("Unexpected API response structure:", result);
return "Tidak dapat menghasilkan insight. Struktur respons tidak terduga.";
}
} catch (e) {
console.error("Error calling Gemini API:", e);
return `Terjadi kesalahan saat memanggil Gemini API: ${e.message}.`;
}
};
// --- Data Cleaning Function ---
const cleanData = (parsedData) => {
const cleaned = parsedData.map(row => {
// Convert 'Date' to YYYY-MM-DD format for consistency and easier comparison
let cleanedDate = null;
if (row.Date) {
const dateObj = new Date(row.Date);
if (!isNaN(dateObj)) {
cleanedDate = dateObj.toISOString().split('T')[0]; // YYYY-MM-DD
}
}
// Fill empty 'Engagements' with 0 and convert to number
let engagements = parseFloat(row.Engagements);
if (isNaN(engagements)) {
engagements = 0;
}
return {
...row,
Date: cleanedDate,
Engagements: engagements,
};
});
// Filter out rows where Date couldn't be parsed
return cleaned.filter(row => row.Date !== null);
};
// --- CSV File Upload Handler ---
const handleFileUpload = async (event) => {
const file = event.target.files[0];
if (file) {
setIsLoading(true);
setError(null);
setCampaignSummaryText("Menghasilkan ringkasan strategi kampanye..."); // Reset and show loading
window.Papa.parse(file, {
header: true,
skipEmptyLines: true,
complete: async (results) => {
if (results.errors.length) {
setError('Error parsing CSV: ' + results.errors[0].message);
setIsLoading(false);
return;
}
const cleaned = cleanData(results.data);
setData(cleaned);
setFilteredData(cleaned); // Initially, filtered data is all data
// Extract unique options for filters
const platforms = [...new Set(cleaned.map(d => d.Platform).filter(Boolean))].sort();
const sentiments = [...new Set(cleaned.map(d => d.Sentiment).filter(Boolean))].sort();
const mediaTypes = [...new Set(cleaned.map(d => d['Media Type']).filter(Boolean))].sort();
const locations = [...new Set(cleaned.map(d => d.Location).filter(Boolean))].sort();
setUniqueOptions({ platforms, sentiments, mediaTypes, locations });
// Generate initial campaign summary
if (cleaned.length > 0) {
const summaryPrompt = `
Berdasarkan data sosial media berikut (hanya 5 baris pertama untuk contoh):
${JSON.stringify(cleaned.slice(0, 5), null, 2)}
dan kolom yang tersedia: ${Object.keys(cleaned[0] || {}).join(', ')}.
Tuliskan ringkasan strategi kampanye yang relevan dan singkat (sekitar 3-5 poin kunci) dalam bahasa Indonesia,
fokus pada engagement, sentimen, platform, media type, dan lokasi.
`;
const summary = await getGeminiResponse(summaryPrompt);
setCampaignSummaryText(summary);
} else {
setCampaignSummaryText("Tidak ada data yang diunggah untuk menghasilkan ringkasan strategi kampanye.");
}
setIsLoading(false);
},
error: (err) => {
setError('Failed to parse CSV: ' + err.message);
setIsLoading(false);
}
});
}
};
// --- Apply Filters Logic ---
const applyFilters = React.useCallback(() => {
let currentFilteredData = [...data];
if (filters.platform !== 'All') {
currentFilteredData = currentFilteredData.filter(d => d.Platform === filters.platform);
}
if (filters.sentiment !== 'All') {
currentFilteredData = currentFilteredData.filter(d => d.Sentiment === filters.sentiment);
}
if (filters.mediaType !== 'All') {
currentFilteredData = currentFilteredData.filter(d => d['Media Type'] === filters.mediaType);
}
if (filters.location !== 'All') {
currentFilteredData = currentFilteredData.filter(d => d.Location === filters.location);
}
// Date Range Filter
if (filters.startDate && filters.endDate) {
const start = new Date(filters.startDate);
const end = new Date(filters.endDate);
currentFilteredData = currentFilteredData.filter(d => {
const date = new Date(d.Date);
return date >= start && date <= end;
});
} else if (filters.startDate) {
const start = new Date(filters.startDate);
currentFilteredData = currentFilteredData.filter(d => {
const date = new Date(d.Date);
return date >= start;
});
} else if (filters.endDate) {
const end = new Date(filters.endDate);
currentFilteredData = currentFilteredData.filter(d => {
const date = new Date(d.Date);
return date <= end;
});
}
setFilteredData(currentFilteredData);
}, [data, filters]);
React.useEffect(() => {
applyFilters();
}, [filters, data, applyFilters]); // Re-run when filters or original data changes
// --- Chart Rendering Functions (using Plotly) & Insight Generation ---
const renderSentimentPieChart = React.useCallback(async () => {
if (!sentimentPieRef.current || !filteredData.length) {
setSentimentInsightsText("Menunggu data dan AI untuk menghasilkan insight.");
return;
}
const sentimentCounts = {};
filteredData.forEach(d => {
const sentiment = d.Sentiment || 'Unknown';
sentimentCounts[sentiment] = (sentimentCounts[sentiment] || 0) + 1;
});
const plotData = [{
labels: Object.keys(sentimentCounts),
values: Object.values(sentimentCounts),
type: 'pie',
hole: .4,
textinfo: "label+percent",
marker: {
colors: ['#636EFA', '#EF553B', '#00CC96', '#AB63FA', '#FFA15A', '#19D3F3'],
},
}];
const layout = {
title: 'Sentiment Breakdown',
height: 400,
width: 500,
showlegend: true,
margin: { t: 50, b: 50, l: 50, r: 50 },
font: { family: 'Inter, sans-serif' },
};
window.Plotly.newPlot(sentimentPieRef.current, plotData, layout, {responsive: true});
// Generate dynamic insight
setSentimentInsightsText("Menghasilkan insight sentimen...");
const insightPrompt = `
Berdasarkan data sentimen berikut (sentimen: jumlah): ${JSON.stringify(sentimentCounts)}.
Berikan 3 insight singkat dan relevan tentang sentimen ini dalam bahasa Indonesia.
`;
const insights = await getGeminiResponse(insightPrompt);
setSentimentInsightsText(insights);
}, [filteredData]);
const renderEngagementLineChart = React.useCallback(async () => {
if (!engagementLineRef.current || !filteredData.length) {
setEngagementInsightsText("Menunggu data dan AI untuk menghasilkan insight.");
return;
}
const engagementByDate = {};
filteredData.forEach(d => {
if (d.Date) {
engagementByDate[d.Date] = (engagementByDate[d.Date] || 0) + d.Engagements;
}
});
const sortedDates = Object.keys(engagementByDate).sort();
const engagements = sortedDates.map(date => engagementByDate[date]);
const plotData = [{
x: sortedDates,
y: engagements,
mode: 'lines+markers',
name: 'Engagements',
line: { color: '#EF553B' },
}];
const layout = {
title: 'Engagement Trend over Time',
xaxis: { title: 'Date', type: 'date', tickformat: '%Y-%m-%d' },
yaxis: { title: 'Total Engagements' },
height: 400,
width: 700,
margin: { t: 50, b: 70, l: 70, r: 50 },
font: { family: 'Inter, sans-serif' },
};
window.Plotly.newPlot(engagementLineRef.current, plotData, layout, {responsive: true});
// Generate dynamic insight
setEngagementInsightsText("Menghasilkan insight tren engagement...");
const insightPrompt = `
Berdasarkan tren engagement berikut (tanggal: engagement): ${JSON.stringify(engagementByDate)}.
Berikan 3 insight singkat dan relevan tentang tren engagement ini dalam bahasa Indonesia.
`;
const insights = await getGeminiResponse(insightPrompt);
setEngagementInsightsText(insights);
}, [filteredData]);
const renderPlatformBarChart = React.useCallback(async () => {
if (!platformBarRef.current || !filteredData.length) {
setPlatformInsightsText("Menunggu data dan AI untuk menghasilkan insight.");
return;
}
const platformEngagements = {};
filteredData.forEach(d => {
const platform = d.Platform || 'Unknown';
platformEngagements[platform] = (platformEngagements[platform] || 0) + d.Engagements;
});
const platforms = Object.keys(platformEngagements);
const engagements = Object.values(platformEngagements);
const plotData = [{
x: platforms,
y: engagements,
type: 'bar',
marker: { color: '#00CC96' },
}];
const layout = {
title: 'Platform Engagements',
xaxis: { title: 'Platform' },
yaxis: { title: 'Total Engagements' },
height: 400,
width: 700,
margin: { t: 50, b: 70, l: 70, r: 50 },
font: { family: 'Inter, sans-serif' },
};
window.Plotly.newPlot(platformBarRef.current, plotData, layout, {responsive: true});
// Generate dynamic insight
setPlatformInsightsText("Menghasilkan insight platform engagement...");
const insightPrompt = `
Berdasarkan engagement platform berikut (platform: engagement): ${JSON.stringify(platformEngagements)}.
Berikan 3 insight singkat dan relevan tentang engagement platform ini dalam bahasa Indonesia.
`;
const insights = await getGeminiResponse(insightPrompt);
setPlatformInsightsText(insights);
}, [filteredData]);
const renderMediaTypePieChart = React.useCallback(async () => {
if (!mediaPieRef.current || !filteredData.length) {
setMediaTypeInsightsText("Menunggu data dan AI untuk menghasilkan insight.");
return;
}
const mediaTypeCounts = {};
filteredData.forEach(d => {
const mediaType = d['Media Type'] || 'Unknown';
mediaTypeCounts[mediaType] = (mediaTypeCounts[mediaType] || 0) + 1;
});
const plotData = [{
labels: Object.keys(mediaTypeCounts),
values: Object.values(mediaTypeCounts),
type: 'pie',
hole: .4,
textinfo: "label+percent",
marker: {
colors: ['#AB63FA', '#FFA15A', '#19D3F3', '#636EFA', '#EF553B', '#00CC96'],
},
}];
const layout = {
title: 'Media Type Mix',
height: 400,
width: 500,
showlegend: true,
margin: { t: 50, b: 50, l: 50, r: 50 },
font: { family: 'Inter, sans-serif' },
};
window.Plotly.newPlot(mediaPieRef.current, plotData, layout, {responsive: true});
// Generate dynamic insight
setMediaTypeInsightsText("Menghasilkan insight jenis media...");
const insightPrompt = `
Berdasarkan jenis media berikut (jenis media: jumlah): ${JSON.stringify(mediaTypeCounts)}.
Berikan 3 insight singkat dan relevan tentang jenis media ini dalam bahasa Indonesia.
`;
const insights = await getGeminiResponse(insightPrompt);
setMediaTypeInsightsText(insights);
}, [filteredData]);
const renderLocationBarChart = React.useCallback(async () => {
if (!locationBarRef.current || !filteredData.length) {
setLocationInsightsText("Menunggu data dan AI untuk menghasilkan insight.");
return;
}
const locationCounts = {};
filteredData.forEach(d => {
const location = d.Location || 'Unknown';
locationCounts[location] = (locationCounts[location] || 0) + 1;
});
// Sort locations by count and take top 5
const sortedLocations = Object.entries(locationCounts)
.sort(([, countA], [, countB]) => countB - countA)
.slice(0, 5);
const locations = sortedLocations.map(([loc]) => loc);
const counts = sortedLocations.map(([, count]) => count);
const plotData = [{
x: locations,
y: counts,
type: 'bar',
marker: { color: '#AB63FA' },
}];
const layout = {
title: 'Top 5 Locations',
xaxis: { title: 'Location' },
yaxis: { title: 'Number of Posts' },
height: 400,
width: 700,
margin: { t: 50, b: 70, l: 70, r: 50 },
font: { family: 'Inter, sans-serif' },
};
window.Plotly.newPlot(locationBarRef.current, plotData, layout, {responsive: true});
// Generate dynamic insight
setLocationInsightsText("Menghasilkan insight lokasi...");
const insightPrompt = `
Berdasarkan 5 lokasi teratas berikut (lokasi: jumlah): ${JSON.stringify(Object.fromEntries(sortedLocations))}.
Berikan 3 insight singkat dan relevan tentang lokasi ini dalam bahasa Indonesia.
`;
const insights = await getGeminiResponse(insightPrompt);
setLocationInsightsText(insights);
}, [filteredData]);
// --- Render charts whenever filteredData changes ---
React.useEffect(() => {
if (filteredData.length) {
renderSentimentPieChart();
renderEngagementLineChart();
renderPlatformBarChart();
renderMediaTypePieChart();
renderLocationBarChart();
} else {
// Clear charts if no data
if (window.Plotly) {
if (sentimentPieRef.current) window.Plotly.purge(sentimentPieRef.current);
if (engagementLineRef.current) window.Plotly.purge(engagementLineRef.current);
if (platformBarRef.current) window.Plotly.purge(platformBarRef.current);
if (mediaPieRef.current) window.Plotly.purge(mediaPieRef.current);
if (locationBarRef.current) window.Plotly.purge(locationBarRef.current);
}
// Reset insights when data is cleared
setSentimentInsightsText("Menunggu data dan AI untuk menghasilkan insight.");
setEngagementInsightsText("Menunggu data dan AI untuk menghasilkan insight.");
setPlatformInsightsText("Menunggu data dan AI untuk menghasilkan insight.");
setMediaTypeInsightsText("Menunggu data dan AI untuk menghasilkan insight.");
setLocationInsightsText("Menunggu data dan AI untuk menghasilkan insight.");
}
}, [filteredData, renderSentimentPieChart, renderEngagementLineChart, renderPlatformBarChart, renderMediaTypePieChart, renderLocationBarChart]);
// --- Export to PDF Function ---
const exportDashboardToPdf = () => {
setIsLoading(true);
setError(null);
// Temporarily hide elements that shouldn't be in the PDF
const elementsToHide = document.querySelectorAll('.no-print');
elementsToHide.forEach(el => el.style.display = 'none');
html2canvas(dashboardRef.current, {
scale: 2, // Increase scale for better resolution
useCORS: true, // Needed if your assets are from different origin (e.g., fonts)
}).then(canvas => {
const imgData = canvas.toDataURL('image/png');
const pdf = new window.jspdf.jsPDF('p', 'mm', 'a4'); // 'p' for portrait, 'mm' for units, 'a4' for size
const imgWidth = 210; // A4 width in mm
const pageHeight = 297; // A4 height in mm
const imgHeight = canvas.height * imgWidth / canvas.width;
let heightLeft = imgHeight;
let position = 0;
pdf.addImage(imgData, 'PNG', 0, position, imgWidth, imgHeight);
heightLeft -= pageHeight;
while (heightLeft >= 0) {
position = heightLeft - imgHeight;
pdf.addPage();
pdf.addImage(imgData, 'PNG', 0, position, imgWidth, imgHeight);
heightLeft -= pageHeight;
}
pdf.save('gemini-dashboard.pdf');
setIsLoading(false);
// Show hidden elements again
elementsToHide.forEach(el => el.style.display = '');
}).catch(err => {
setError('Failed to export PDF: ' + err.message);
setIsLoading(false);
// Ensure elements are shown even on error
elementsToHide.forEach(el => el.style.display = '');
});
};
return (
<div ref={dashboardRef} className="min-h-screen bg-gray-100 p-4 font-sans text-gray-800">
<div className="max-w-7xl mx-auto bg-white shadow-lg rounded-xl p-8 mb-8">
<h1 className="text-4xl font-bold text-center text-indigo-700 mb-6">Gemini Data Dashboard</h1>
<p className="text-center text-lg text-gray-600 mb-8">
Analisis data media sosial Anda dengan visualisasi interaktif.
</p>
{/* Group Name */}
<div className="text-center text-indigo-600 text-xl font-semibold mb-8 border-t border-b border-indigo-200 py-3">
{groupName}
</div>
{/* File Upload Section */}
<div className="mb-8 p-6 bg-indigo-50 rounded-lg shadow-sm no-print">
<label htmlFor="csv-upload" className="block text-xl font-medium text-indigo-800 mb-3">
Unggah File CSV:
</label>
<input
id="csv-upload"
type="file"
accept=".csv"
onChange={handleFileUpload}
className="block w-full text-sm text-gray-700
file:mr-4 file:py-2 file:px-4
file:rounded-full file:border-0
file:text-sm file:font-semibold
file:bg-indigo-500 file:text-white
hover:file:bg-indigo-600 cursor-pointer"
/>
{isLoading && <p className="mt-3 text-indigo-600">Memuat dan membersihkan data...</p>}
{error && <p className="mt-3 text-red-600">Error: {error}</p>}
{!data.length && !isLoading && (
<p className="mt-3 text-gray-500 text-sm">
Harap unggah file CSV dengan kolom seperti: 'Date', 'Engagements', 'Platform', 'Sentiment', 'Media Type', 'Location'.
</p>
)}
</div>
{data.length > 0 && (
<>
{/* Export PDF Button */}
<div className="text-center mb-8 no-print">
<button
onClick={exportDashboardToPdf}
className="px-8 py-3 bg-green-600 text-white font-semibold rounded-lg shadow-md hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-green-500 focus:ring-opacity-75 transition duration-150 ease-in-out"
disabled={isLoading}
>
{isLoading ? 'Mengekspor...' : 'Ekspor Dashboard ke PDF'}
</button>
</div>
{/* Key Action Summary Section (Dynamic) */}
<div className="mb-10 p-6 bg-blue-50 rounded-lg shadow-sm">
<h2 className="text-2xl font-semibold text-blue-800 mb-4">Ringkasan Strategi Kampanye (Key Action Summary)</h2>
<div className="text-gray-700 leading-relaxed">
<p>{campaignSummaryText}</p>
</div>
</div>
{/* Filters Section */}
<div className="mb-10 p-6 bg-purple-50 rounded-lg shadow-sm no-print">
<h2 className="text-2xl font-semibold text-purple-800 mb-4">Saring Data</h2>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{/* Platform Filter */}
<div>
<label htmlFor="platform-filter" className="block text-sm font-medium text-gray-700 mb-1">Platform:</label>
<select
id="platform-filter"
value={filters.platform}
onChange={(e) => setFilters({ ...filters, platform: e.target.value })}
className="mt-1 block w-full pl-3 pr-10 py-2 text-base border-gray-300 focus:outline-none focus:ring-purple-500 focus:border-purple-500 sm:text-sm rounded-md shadow-sm"
>
<option value="All">Semua Platform</option>
{uniqueOptions.platforms.map(option => (
<option key={option} value={option}>{option}</option>
))}
</select>
</div>
{/* Sentiment Filter */}
<div>
<label htmlFor="sentiment-filter" className="block text-sm font-medium text-gray-700 mb-1">Sentimen:</label>
<select
id="sentiment-filter"
value={filters.sentiment}
onChange={(e) => setFilters({ ...filters, sentiment: e.target.value })}
className="mt-1 block w-full pl-3 pr-10 py-2 text-base border-gray-300 focus:outline-none focus:ring-purple-500 focus:border-purple-500 sm:text-sm rounded-md shadow-sm"
>
<option value="All">Semua Sentimen</option>
{uniqueOptions.sentiments.map(option => (
<option key={option} value={option}>{option}</option>
))}
</select>
</div>
{/* Media Type Filter */}
<div>
<label htmlFor="media-type-filter" className="block text-sm font-medium text-gray-700 mb-1">Jenis Media:</label>
<select
id="media-type-filter"
value={filters.mediaType}
onChange={(e) => setFilters({ ...filters, mediaType: e.target.value })}
className="mt-1 block w-full pl-3 pr-10 py-2 text-base border-gray-300 focus:outline-none focus:ring-purple-500 focus:border-purple-500 sm:text-sm rounded-md shadow-sm"
>
<option value="All">Semua Jenis Media</option>
{uniqueOptions.mediaTypes.map(option => (
<option key={option} value={option}>{option}</option>
))}
</select>
</div>
{/* Location Filter */}
<div>
<label htmlFor="location-filter" className="block text-sm font-medium text-gray-700 mb-1">Lokasi:</label>
<select
id="location-filter"
value={filters.location}
onChange={(e) => setFilters({ ...filters, location: e.target.value })}
className="mt-1 block w-full pl-3 pr-10 py-2 text-base border-gray-300 focus:outline-none focus:ring-purple-500 focus:border-purple-500 sm:text-sm rounded-md shadow-sm"
>
<option value="All">Semua Lokasi</option>
{uniqueOptions.locations.map(option => (
<option key={option} value={option}>{option}</option>
))}
</select>
</div>
{/* Start Date Filter */}
<div>
<label htmlFor="start-date-filter" className="block text-sm font-medium text-gray-700 mb-1">Tanggal Mulai:</label>
<input
id="start-date-filter"
type="date"
value={filters.startDate}
onChange={(e) => setFilters({ ...filters, startDate: e.target.value })}
className="mt-1 block w-full pl-3 pr-3 py-2 text-base border-gray-300 focus:outline-none focus:ring-purple-500 focus:border-purple-500 sm:text-sm rounded-md shadow-sm"
/>
</div>
{/* End Date Filter */}
<div>
<label htmlFor="end-date-filter" className="block text-sm font-medium text-gray-700 mb-1">Tanggal Berakhir:</label>
<input
id="end-date-filter"
type="date"
value={filters.endDate}
onChange={(e) => setFilters({ ...filters, endDate: e.target.value })}
className="mt-1 block w-full pl-3 pr-3 py-2 text-base border-gray-300 focus:outline-none focus:ring-purple-500 focus:border-purple-500 sm:text-sm rounded-md shadow-sm"
/>
</div>
</div>
<div className="mt-6 text-center">
<button
onClick={() => setFilters({
platform: 'All',
sentiment: 'All',
mediaType: 'All',
location: 'All',
startDate: '',
endDate: '',
})}
className="px-6 py-2 bg-gray-200 text-gray-800 rounded-lg shadow-md hover:bg-gray-300 focus:outline-none focus:ring-2 focus:ring-gray-400 focus:ring-opacity-75 transition duration-150 ease-in-out"
>
Atur Ulang Filter
</button>
</div>
</div>
{/* Visualizations Section */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
{/* Sentiment Breakdown */}
<div className="bg-white p-6 rounded-xl shadow-md flex flex-col items-center">
<div ref={sentimentPieRef} className="w-full h-full min-h-[400px]"></div>
<h3 className="text-xl font-semibold text-gray-700 mt-4 mb-2">Insight:</h3>
<div className="text-gray-600 text-sm insights-list">
<p>{sentimentInsightsText}</p>
</div>
</div>
{/* Engagement Trend */}
<div className="bg-white p-6 rounded-xl shadow-md flex flex-col items-center">
<div ref={engagementLineRef} className="w-full h-full min-h-[400px]"></div>
<h3 className="text-xl font-semibold text-gray-700 mt-4 mb-2">Insight:</h3>
<div className="text-gray-600 text-sm insights-list">
<p>{engagementInsightsText}</p>
</div>
</div>
{/* Platform Engagements */}
<div className="bg-white p-6 rounded-xl shadow-md flex flex-col items-center">
<div ref={platformBarRef} className="w-full h-full min-h-[400px]"></div>
<h3 className="text-xl font-semibold text-gray-700 mt-4 mb-2">Insight:</h3>
<div className="text-gray-600 text-sm insights-list">
<p>{platformInsightsText}</p>
</div>
</div>
{/* Media Type Mix */}
<div className="bg-white p-6 rounded-xl shadow-md flex flex-col items-center">
<div ref={mediaPieRef} className="w-full h-full min-h-[400px]"></div>
<h3 className="text-xl font-semibold text-gray-700 mt-4 mb-2">Insight:</h3>
<div className="text-gray-600 text-sm insights-list">
<p>{mediaTypeInsightsText}</p>
</div>
</div>
{/* Top 5 Locations */}
<div className="bg-white p-6 rounded-xl shadow-md col-span-1 lg:col-span-2 flex flex-col items-center">
<div ref={locationBarRef} className="w-full h-full min-h-[400px]"></div>
<h3 className="text-xl font-semibold text-gray-700 mt-4 mb-2">Insight:</h3>
<div className="text-gray-600 text-sm insights-list">
<p>{locationInsightsText}</p>
</div>
</div>
</div>
</>
)}
</div>
</div>
);
};
// Render the React component into the 'root' div
ReactDOM.createRoot(document.getElementById('root')).render(<App />);
</script>
</body>
</html>