Add trending keywords app with opportunity scoring algorithm
This commit is contained in:
323
app.js
Normal file
323
app.js
Normal file
@@ -0,0 +1,323 @@
|
|||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
// DOM Elements
|
||||||
|
const keywordsContainer = document.getElementById('keywords-container');
|
||||||
|
const refreshBtn = document.getElementById('refresh-btn');
|
||||||
|
const loading = document.getElementById('loading');
|
||||||
|
const selectedKeywordText = document.getElementById('selected-keyword-text');
|
||||||
|
const generatePromptBtn = document.getElementById('generate-prompt-btn');
|
||||||
|
const promptResult = document.getElementById('prompt-result');
|
||||||
|
const promptTitle = document.getElementById('prompt-title');
|
||||||
|
const promptBody = document.getElementById('prompt-body');
|
||||||
|
const copyPromptBtn = document.getElementById('copy-prompt-btn');
|
||||||
|
const copySuccess = document.getElementById('copy-success');
|
||||||
|
|
||||||
|
// State
|
||||||
|
let selectedKeyword = null;
|
||||||
|
let currentPrompt = null;
|
||||||
|
|
||||||
|
// Fetch trending keywords with opportunity scoring
|
||||||
|
const fetchTrendingKeywords = async () => {
|
||||||
|
try {
|
||||||
|
loading.classList.remove('hidden');
|
||||||
|
keywordsContainer.innerHTML = '';
|
||||||
|
|
||||||
|
// Step 1: Get trending topics from multiple sources
|
||||||
|
const trendingTopics = await fetchTrendingTopics();
|
||||||
|
|
||||||
|
// Step 2: Enrich with search volume and result count data
|
||||||
|
const enrichedKeywords = await enrichKeywordsWithOpportunityData(trendingTopics);
|
||||||
|
|
||||||
|
// Step 3: Calculate opportunity score and sort
|
||||||
|
const keywordsWithOpportunity = calculateOpportunityScore(enrichedKeywords);
|
||||||
|
|
||||||
|
if (keywordsWithOpportunity.length > 0) {
|
||||||
|
renderKeywords(keywordsWithOpportunity);
|
||||||
|
} else {
|
||||||
|
useBackupKeywords();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching trending keywords:', error);
|
||||||
|
useBackupKeywords();
|
||||||
|
} finally {
|
||||||
|
loading.classList.add('hidden');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Fetch trending topics from multiple sources for diversity
|
||||||
|
const fetchTrendingTopics = async () => {
|
||||||
|
try {
|
||||||
|
// Source 1: Wikipedia trending articles
|
||||||
|
const wikiResponse = await fetch('https://wikimedia.org/api/rest_v1/metrics/pageviews/top/en.wikipedia/all-access/2023/01/all-days');
|
||||||
|
const wikiData = await wikiResponse.json();
|
||||||
|
|
||||||
|
let combinedTopics = [];
|
||||||
|
|
||||||
|
// Process Wikipedia data
|
||||||
|
if (wikiData && wikiData.items && wikiData.items[0] && wikiData.items[0].articles) {
|
||||||
|
const wikiTopics = wikiData.items[0].articles
|
||||||
|
.filter(article => !article.article.startsWith('Special:') &&
|
||||||
|
!article.article.startsWith('Main_Page') &&
|
||||||
|
!article.article.startsWith('Wikipedia:'))
|
||||||
|
.slice(0, 15)
|
||||||
|
.map(article => ({
|
||||||
|
keyword: article.article.replace(/_/g, ' '),
|
||||||
|
searchVolume: article.views,
|
||||||
|
source: 'wikipedia'
|
||||||
|
}));
|
||||||
|
|
||||||
|
combinedTopics = [...combinedTopics, ...wikiTopics];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Source 2: Use Google Trends API-like data (simulated)
|
||||||
|
// In a real implementation, you would use Google Trends API
|
||||||
|
const techTrendingTopics = [
|
||||||
|
{ keyword: 'quantum computing applications', searchVolume: 85000 },
|
||||||
|
{ keyword: 'edge computing use cases', searchVolume: 62000 },
|
||||||
|
{ keyword: 'zero-knowledge proofs', searchVolume: 45000 },
|
||||||
|
{ keyword: 'synthetic data generation', searchVolume: 73000 },
|
||||||
|
{ keyword: 'federated learning models', searchVolume: 58000 },
|
||||||
|
{ keyword: 'serverless architecture patterns', searchVolume: 67000 },
|
||||||
|
{ keyword: 'homomorphic encryption', searchVolume: 41000 },
|
||||||
|
{ keyword: 'computer vision in healthcare', searchVolume: 89000 },
|
||||||
|
{ keyword: 'explainable ai techniques', searchVolume: 76000 },
|
||||||
|
{ keyword: 'graph neural networks', searchVolume: 52000 }
|
||||||
|
].map(item => ({ ...item, source: 'tech_trends' }));
|
||||||
|
|
||||||
|
combinedTopics = [...combinedTopics, ...techTrendingTopics];
|
||||||
|
|
||||||
|
// Source 3: Emerging research topics (simulated)
|
||||||
|
const emergingTopics = [
|
||||||
|
{ keyword: 'biomimetic materials science', searchVolume: 32000 },
|
||||||
|
{ keyword: 'neuromorphic computing chips', searchVolume: 28000 },
|
||||||
|
{ keyword: 'digital twin technology applications', searchVolume: 47000 },
|
||||||
|
{ keyword: 'post-quantum cryptography standards', searchVolume: 39000 },
|
||||||
|
{ keyword: 'spatial computing interfaces', searchVolume: 43000 }
|
||||||
|
].map(item => ({ ...item, source: 'emerging_research' }));
|
||||||
|
|
||||||
|
combinedTopics = [...combinedTopics, ...emergingTopics];
|
||||||
|
|
||||||
|
// Deduplicate and return
|
||||||
|
return Array.from(new Map(combinedTopics.map(item =>
|
||||||
|
[item.keyword, item])).values());
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching trending topics:', error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Enrich keywords with search result counts
|
||||||
|
const enrichKeywordsWithOpportunityData = async (keywords) => {
|
||||||
|
// In a production environment, you would use a real search API
|
||||||
|
// Here we'll simulate the data with a deterministic algorithm
|
||||||
|
|
||||||
|
return Promise.all(keywords.map(async (keyword) => {
|
||||||
|
// Simulate API call to get search result count
|
||||||
|
// In reality, you would use a search engine API
|
||||||
|
|
||||||
|
// Algorithm to generate realistic but varied result counts
|
||||||
|
// Technical/specific keywords tend to have fewer results
|
||||||
|
const wordCount = keyword.keyword.split(' ').length;
|
||||||
|
const containsTechnicalTerms = /\b(api|algorithm|framework|protocol|quantum|neural|encryption|serverless|computing)\b/i.test(keyword.keyword);
|
||||||
|
const isNiche = wordCount >= 3 || containsTechnicalTerms;
|
||||||
|
|
||||||
|
// Base result count - more specific terms have fewer results
|
||||||
|
let baseResultCount;
|
||||||
|
if (isNiche) {
|
||||||
|
baseResultCount = Math.floor(50000 + Math.random() * 500000);
|
||||||
|
} else {
|
||||||
|
baseResultCount = Math.floor(1000000 + Math.random() * 50000000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add some randomness but keep it deterministic for the same keyword
|
||||||
|
const seed = keyword.keyword.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0);
|
||||||
|
const pseudoRandom = Math.sin(seed) * 10000;
|
||||||
|
const resultCount = Math.max(10000, Math.floor(baseResultCount + pseudoRandom));
|
||||||
|
|
||||||
|
return {
|
||||||
|
...keyword,
|
||||||
|
resultCount
|
||||||
|
};
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
// Calculate opportunity score based on search volume vs. result count
|
||||||
|
const calculateOpportunityScore = (keywords) => {
|
||||||
|
// Calculate the opportunity score
|
||||||
|
// Higher score = high search volume + low result count (better opportunity)
|
||||||
|
const keywordsWithScore = keywords.map(keyword => {
|
||||||
|
// Normalize search volume (0-100)
|
||||||
|
const maxSearchVolume = Math.max(...keywords.map(k => k.searchVolume));
|
||||||
|
const normalizedSearchVolume = (keyword.searchVolume / maxSearchVolume) * 100;
|
||||||
|
|
||||||
|
// Normalize result count inversely (fewer results = higher score)
|
||||||
|
const maxResultCount = Math.max(...keywords.map(k => k.resultCount));
|
||||||
|
const normalizedResultCount = 100 - ((keyword.resultCount / maxResultCount) * 100);
|
||||||
|
|
||||||
|
// Calculate opportunity score (weighted average)
|
||||||
|
// 60% weight to search volume, 40% weight to inverse result count
|
||||||
|
const opportunityScore = (normalizedSearchVolume * 0.6) + (normalizedResultCount * 0.4);
|
||||||
|
|
||||||
|
// Calculate competition level (lower is better)
|
||||||
|
const competitionLevel = keyword.resultCount / keyword.searchVolume;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...keyword,
|
||||||
|
opportunityScore: Math.round(opportunityScore),
|
||||||
|
competitionLevel: Math.round(competitionLevel)
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// Sort by opportunity score (highest first)
|
||||||
|
return keywordsWithScore
|
||||||
|
.sort((a, b) => b.opportunityScore - a.opportunityScore)
|
||||||
|
.slice(0, 10);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Use backup keywords when API fails
|
||||||
|
const useBackupKeywords = () => {
|
||||||
|
const backupKeywords = [
|
||||||
|
{ keyword: 'quantum machine learning algorithms', opportunityScore: 95, searchVolume: 42000, resultCount: 156000, competitionLevel: 4 },
|
||||||
|
{ keyword: 'zero-trust network architecture', opportunityScore: 92, searchVolume: 68000, resultCount: 310000, competitionLevel: 5 },
|
||||||
|
{ keyword: 'synthetic data generation techniques', opportunityScore: 89, searchVolume: 51000, resultCount: 245000, competitionLevel: 5 },
|
||||||
|
{ keyword: 'edge computing security frameworks', opportunityScore: 87, searchVolume: 73000, resultCount: 420000, competitionLevel: 6 },
|
||||||
|
{ keyword: 'federated learning privacy', opportunityScore: 85, searchVolume: 47000, resultCount: 280000, competitionLevel: 6 },
|
||||||
|
{ keyword: 'explainable ai for healthcare', opportunityScore: 82, searchVolume: 59000, resultCount: 390000, competitionLevel: 7 },
|
||||||
|
{ keyword: 'post-quantum cryptography implementation', opportunityScore: 80, searchVolume: 38000, resultCount: 265000, competitionLevel: 7 },
|
||||||
|
{ keyword: 'neuromorphic computing applications', opportunityScore: 78, searchVolume: 31000, resultCount: 225000, competitionLevel: 7 },
|
||||||
|
{ keyword: 'graph neural networks for recommendation', opportunityScore: 76, searchVolume: 44000, resultCount: 340000, competitionLevel: 8 },
|
||||||
|
{ keyword: 'digital twin technology standards', opportunityScore: 74, searchVolume: 53000, resultCount: 420000, competitionLevel: 8 }
|
||||||
|
];
|
||||||
|
|
||||||
|
renderKeywords(backupKeywords);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Render keywords
|
||||||
|
const renderKeywords = (keywords) => {
|
||||||
|
keywordsContainer.innerHTML = '';
|
||||||
|
|
||||||
|
keywords.forEach(keyword => {
|
||||||
|
const keywordCard = document.createElement('div');
|
||||||
|
keywordCard.className = 'keyword-card';
|
||||||
|
keywordCard.dataset.keyword = keyword.keyword;
|
||||||
|
|
||||||
|
// Display opportunity score and metrics
|
||||||
|
const score = keyword.opportunityScore || keyword.score || 0;
|
||||||
|
const searchVolume = keyword.searchVolume ? `${(keyword.searchVolume/1000).toFixed(1)}K` : 'N/A';
|
||||||
|
const resultCount = keyword.resultCount ? `${(keyword.resultCount/1000).toFixed(1)}K` : 'N/A';
|
||||||
|
const competitionLevel = keyword.competitionLevel || 'N/A';
|
||||||
|
|
||||||
|
keywordCard.innerHTML = `
|
||||||
|
<div class="keyword-name">${keyword.keyword}</div>
|
||||||
|
<div class="keyword-metrics">
|
||||||
|
<div class="opportunity-score">Opportunity: <span class="highlight">${score}</span></div>
|
||||||
|
<div class="search-volume">Search: ${searchVolume}</div>
|
||||||
|
<div class="result-count">Results: ${resultCount}</div>
|
||||||
|
<div class="competition">Competition: ${competitionLevel}</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
keywordCard.addEventListener('click', () => selectKeyword(keyword.keyword, keywordCard));
|
||||||
|
|
||||||
|
keywordsContainer.appendChild(keywordCard);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Select a keyword
|
||||||
|
const selectKeyword = (keyword, card) => {
|
||||||
|
// Remove selected class from all cards
|
||||||
|
document.querySelectorAll('.keyword-card').forEach(k => k.classList.remove('selected'));
|
||||||
|
|
||||||
|
// Add selected class to clicked card
|
||||||
|
card.classList.add('selected');
|
||||||
|
|
||||||
|
// Update selected keyword
|
||||||
|
selectedKeyword = keyword;
|
||||||
|
selectedKeywordText.textContent = keyword;
|
||||||
|
|
||||||
|
// Enable generate prompt button
|
||||||
|
generatePromptBtn.disabled = false;
|
||||||
|
|
||||||
|
// Hide prompt result if visible
|
||||||
|
promptResult.classList.add('hidden');
|
||||||
|
};
|
||||||
|
|
||||||
|
// Generate article prompt
|
||||||
|
const generatePrompt = () => {
|
||||||
|
if (!selectedKeyword) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
loading.classList.remove('hidden');
|
||||||
|
generatePromptBtn.disabled = true;
|
||||||
|
|
||||||
|
// Find the selected keyword data
|
||||||
|
const keywordData = Array.from(document.querySelectorAll('.keyword-card')).find(
|
||||||
|
card => card.dataset.keyword === selectedKeyword
|
||||||
|
);
|
||||||
|
|
||||||
|
// Get opportunity metrics if available
|
||||||
|
const opportunityScore = keywordData?.querySelector('.opportunity-score .highlight')?.textContent || 'high';
|
||||||
|
const searchVolume = keywordData?.querySelector('.search-volume')?.textContent.split(': ')[1] || 'significant';
|
||||||
|
const resultCount = keywordData?.querySelector('.result-count')?.textContent.split(': ')[1] || 'limited';
|
||||||
|
|
||||||
|
// Generate a ChatGPT-friendly prompt based on the keyword and opportunity metrics
|
||||||
|
const prompt = {
|
||||||
|
title: `Write a comprehensive article about "${selectedKeyword}" (High-Opportunity Keyword)`,
|
||||||
|
content: `Write a well-researched, engaging, and informative article about "${selectedKeyword}". This is a high-opportunity keyword with ${searchVolume} monthly searches but only ${resultCount} competing results.
|
||||||
|
|
||||||
|
1. Start with an attention-grabbing introduction that explains why "${selectedKeyword}" is important or relevant today
|
||||||
|
2. Include at least 5 main sections with appropriate headings
|
||||||
|
3. Incorporate current statistics, trends, and expert opinions
|
||||||
|
4. Address common questions or misconceptions about "${selectedKeyword}"
|
||||||
|
5. Provide practical tips, applications, or future predictions related to "${selectedKeyword}"
|
||||||
|
6. End with a compelling conclusion that summarizes key points and offers final thoughts
|
||||||
|
7. Optimize the content for SEO while maintaining high-quality, valuable information
|
||||||
|
|
||||||
|
The article should be approximately 1500-2000 words, written in a professional yet accessible tone, and formatted for easy online reading with appropriate headings, subheadings, and bullet points where relevant.`
|
||||||
|
};
|
||||||
|
|
||||||
|
currentPrompt = prompt;
|
||||||
|
displayPrompt(prompt);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error generating prompt:', error);
|
||||||
|
alert('Failed to generate prompt. Please try again.');
|
||||||
|
} finally {
|
||||||
|
loading.classList.add('hidden');
|
||||||
|
generatePromptBtn.disabled = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Display the generated prompt
|
||||||
|
const displayPrompt = (prompt) => {
|
||||||
|
promptTitle.textContent = prompt.title;
|
||||||
|
promptBody.textContent = prompt.content;
|
||||||
|
promptResult.classList.remove('hidden');
|
||||||
|
};
|
||||||
|
|
||||||
|
// Copy prompt to clipboard
|
||||||
|
const copyPrompt = () => {
|
||||||
|
if (!currentPrompt) return;
|
||||||
|
|
||||||
|
const fullPrompt = `${currentPrompt.title}\n\n${currentPrompt.content}`;
|
||||||
|
|
||||||
|
navigator.clipboard.writeText(fullPrompt)
|
||||||
|
.then(() => {
|
||||||
|
copySuccess.classList.remove('hidden');
|
||||||
|
setTimeout(() => {
|
||||||
|
copySuccess.classList.add('hidden');
|
||||||
|
}, 2000);
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.error('Failed to copy prompt:', err);
|
||||||
|
alert('Failed to copy prompt to clipboard');
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Event listeners
|
||||||
|
refreshBtn.addEventListener('click', fetchTrendingKeywords);
|
||||||
|
generatePromptBtn.addEventListener('click', generatePrompt);
|
||||||
|
copyPromptBtn.addEventListener('click', copyPrompt);
|
||||||
|
|
||||||
|
// Initial load
|
||||||
|
fetchTrendingKeywords();
|
||||||
|
});
|
||||||
57
index.html
Normal file
57
index.html
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Trending Keywords & Article Prompts</title>
|
||||||
|
<link rel="stylesheet" href="styles.css">
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<header>
|
||||||
|
<h1>Trending Keywords</h1>
|
||||||
|
<p>Discover globally trending topics and generate article prompts</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main>
|
||||||
|
<section class="trending-section">
|
||||||
|
<h2>Today's Trending Keywords</h2>
|
||||||
|
<div class="refresh-container">
|
||||||
|
<button id="refresh-btn">Refresh Keywords</button>
|
||||||
|
<div id="loading" class="loading hidden">Loading...</div>
|
||||||
|
</div>
|
||||||
|
<div id="keywords-container" class="keywords-container">
|
||||||
|
<!-- Keywords will be populated here -->
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="prompt-section">
|
||||||
|
<h2>Article Prompt Generator</h2>
|
||||||
|
<div class="selected-keyword">
|
||||||
|
<p>Selected keyword: <span id="selected-keyword-text">None selected</span></p>
|
||||||
|
</div>
|
||||||
|
<button id="generate-prompt-btn" disabled>Generate Article Prompt</button>
|
||||||
|
|
||||||
|
<div id="prompt-result" class="prompt-result hidden">
|
||||||
|
<h3>Your ChatGPT Prompt</h3>
|
||||||
|
<div class="prompt-content">
|
||||||
|
<h4 id="prompt-title"></h4>
|
||||||
|
<div id="prompt-body"></div>
|
||||||
|
<div class="copy-container">
|
||||||
|
<button id="copy-prompt-btn">Copy Prompt</button>
|
||||||
|
<span id="copy-success" class="copy-success hidden">Copied!</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer>
|
||||||
|
<p>© 2023 Trending Keywords App</p>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="app.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
145
public/app.js
Normal file
145
public/app.js
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
// DOM Elements
|
||||||
|
const keywordsContainer = document.getElementById('keywords-container');
|
||||||
|
const refreshBtn = document.getElementById('refresh-btn');
|
||||||
|
const loading = document.getElementById('loading');
|
||||||
|
const selectedKeywordText = document.getElementById('selected-keyword-text');
|
||||||
|
const generatePromptBtn = document.getElementById('generate-prompt-btn');
|
||||||
|
const promptResult = document.getElementById('prompt-result');
|
||||||
|
const promptTitle = document.getElementById('prompt-title');
|
||||||
|
const promptBody = document.getElementById('prompt-body');
|
||||||
|
const copyPromptBtn = document.getElementById('copy-prompt-btn');
|
||||||
|
const copySuccess = document.getElementById('copy-success');
|
||||||
|
|
||||||
|
// State
|
||||||
|
let selectedKeyword = null;
|
||||||
|
let currentPrompt = null;
|
||||||
|
|
||||||
|
// Fetch trending keywords
|
||||||
|
const fetchTrendingKeywords = async () => {
|
||||||
|
try {
|
||||||
|
loading.classList.remove('hidden');
|
||||||
|
keywordsContainer.innerHTML = '';
|
||||||
|
|
||||||
|
const response = await fetch('/api/trending-keywords');
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.keywords && data.keywords.length > 0) {
|
||||||
|
renderKeywords(data.keywords);
|
||||||
|
} else {
|
||||||
|
keywordsContainer.innerHTML = '<p>No trending keywords found. Please try again later.</p>';
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching trending keywords:', error);
|
||||||
|
keywordsContainer.innerHTML = '<p>Failed to load trending keywords. Please try again.</p>';
|
||||||
|
} finally {
|
||||||
|
loading.classList.add('hidden');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Render keywords
|
||||||
|
const renderKeywords = (keywords) => {
|
||||||
|
keywordsContainer.innerHTML = '';
|
||||||
|
|
||||||
|
keywords.forEach(keyword => {
|
||||||
|
const keywordCard = document.createElement('div');
|
||||||
|
keywordCard.className = 'keyword-card';
|
||||||
|
keywordCard.dataset.keyword = keyword.keyword;
|
||||||
|
|
||||||
|
keywordCard.innerHTML = `
|
||||||
|
<div class="keyword-name">${keyword.keyword}</div>
|
||||||
|
<div class="keyword-score">Trend score: ${keyword.score}</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
keywordCard.addEventListener('click', () => selectKeyword(keyword.keyword, keywordCard));
|
||||||
|
|
||||||
|
keywordsContainer.appendChild(keywordCard);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Select a keyword
|
||||||
|
const selectKeyword = (keyword, card) => {
|
||||||
|
// Remove selected class from all cards
|
||||||
|
document.querySelectorAll('.keyword-card').forEach(k => k.classList.remove('selected'));
|
||||||
|
|
||||||
|
// Add selected class to clicked card
|
||||||
|
card.classList.add('selected');
|
||||||
|
|
||||||
|
// Update selected keyword
|
||||||
|
selectedKeyword = keyword;
|
||||||
|
selectedKeywordText.textContent = keyword;
|
||||||
|
|
||||||
|
// Enable generate prompt button
|
||||||
|
generatePromptBtn.disabled = false;
|
||||||
|
|
||||||
|
// Hide prompt result if visible
|
||||||
|
promptResult.classList.add('hidden');
|
||||||
|
};
|
||||||
|
|
||||||
|
// Generate article prompt
|
||||||
|
const generatePrompt = async () => {
|
||||||
|
if (!selectedKeyword) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
loading.classList.remove('hidden');
|
||||||
|
generatePromptBtn.disabled = true;
|
||||||
|
|
||||||
|
const response = await fetch('/api/generate-prompt', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ keyword: selectedKeyword }),
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.prompt) {
|
||||||
|
currentPrompt = data.prompt;
|
||||||
|
displayPrompt(data.prompt);
|
||||||
|
} else {
|
||||||
|
alert('Failed to generate prompt. Please try again.');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error generating prompt:', error);
|
||||||
|
alert('Failed to generate prompt. Please try again.');
|
||||||
|
} finally {
|
||||||
|
loading.classList.add('hidden');
|
||||||
|
generatePromptBtn.disabled = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Display the generated prompt
|
||||||
|
const displayPrompt = (prompt) => {
|
||||||
|
promptTitle.textContent = prompt.title;
|
||||||
|
promptBody.textContent = prompt.content;
|
||||||
|
promptResult.classList.remove('hidden');
|
||||||
|
};
|
||||||
|
|
||||||
|
// Copy prompt to clipboard
|
||||||
|
const copyPrompt = () => {
|
||||||
|
if (!currentPrompt) return;
|
||||||
|
|
||||||
|
const fullPrompt = `${currentPrompt.title}\n\n${currentPrompt.content}`;
|
||||||
|
|
||||||
|
navigator.clipboard.writeText(fullPrompt)
|
||||||
|
.then(() => {
|
||||||
|
copySuccess.classList.remove('hidden');
|
||||||
|
setTimeout(() => {
|
||||||
|
copySuccess.classList.add('hidden');
|
||||||
|
}, 2000);
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.error('Failed to copy prompt:', err);
|
||||||
|
alert('Failed to copy prompt to clipboard');
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Event listeners
|
||||||
|
refreshBtn.addEventListener('click', fetchTrendingKeywords);
|
||||||
|
generatePromptBtn.addEventListener('click', generatePrompt);
|
||||||
|
copyPromptBtn.addEventListener('click', copyPrompt);
|
||||||
|
|
||||||
|
// Initial load
|
||||||
|
fetchTrendingKeywords();
|
||||||
|
});
|
||||||
57
public/index.html
Normal file
57
public/index.html
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Trending Keywords & Article Prompts</title>
|
||||||
|
<link rel="stylesheet" href="styles.css">
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<header>
|
||||||
|
<h1>Trending Keywords</h1>
|
||||||
|
<p>Discover globally trending topics and generate article prompts</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main>
|
||||||
|
<section class="trending-section">
|
||||||
|
<h2>Today's Trending Keywords</h2>
|
||||||
|
<div class="refresh-container">
|
||||||
|
<button id="refresh-btn">Refresh Keywords</button>
|
||||||
|
<div id="loading" class="loading hidden">Loading...</div>
|
||||||
|
</div>
|
||||||
|
<div id="keywords-container" class="keywords-container">
|
||||||
|
<!-- Keywords will be populated here -->
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="prompt-section">
|
||||||
|
<h2>Article Prompt Generator</h2>
|
||||||
|
<div class="selected-keyword">
|
||||||
|
<p>Selected keyword: <span id="selected-keyword-text">None selected</span></p>
|
||||||
|
</div>
|
||||||
|
<button id="generate-prompt-btn" disabled>Generate Article Prompt</button>
|
||||||
|
|
||||||
|
<div id="prompt-result" class="prompt-result hidden">
|
||||||
|
<h3>Your ChatGPT Prompt</h3>
|
||||||
|
<div class="prompt-content">
|
||||||
|
<h4 id="prompt-title"></h4>
|
||||||
|
<div id="prompt-body"></div>
|
||||||
|
<div class="copy-container">
|
||||||
|
<button id="copy-prompt-btn">Copy Prompt</button>
|
||||||
|
<span id="copy-success" class="copy-success hidden">Copied!</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer>
|
||||||
|
<p>© 2023 Trending Keywords App</p>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="app.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
182
public/styles.css
Normal file
182
public/styles.css
Normal file
@@ -0,0 +1,182 @@
|
|||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: 'Poppins', sans-serif;
|
||||||
|
background-color: #f5f7fa;
|
||||||
|
color: #333;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
header {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 40px;
|
||||||
|
padding: 20px 0;
|
||||||
|
background: linear-gradient(135deg, #6e8efb, #a777e3);
|
||||||
|
color: white;
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
header h1 {
|
||||||
|
font-size: 2.5rem;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
header p {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
section {
|
||||||
|
background: white;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 25px;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
color: #444;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.refresh-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
background-color: #6e8efb;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 10px 20px;
|
||||||
|
border-radius: 5px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 500;
|
||||||
|
transition: background-color 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:hover {
|
||||||
|
background-color: #5a7df7;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:disabled {
|
||||||
|
background-color: #cccccc;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading {
|
||||||
|
margin-left: 15px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.keywords-container {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.keyword-card {
|
||||||
|
background: #f9f9f9;
|
||||||
|
border: 1px solid #eee;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 15px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: transform 0.2s, box-shadow 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.keyword-card:hover {
|
||||||
|
transform: translateY(-3px);
|
||||||
|
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.keyword-card.selected {
|
||||||
|
border: 2px solid #6e8efb;
|
||||||
|
background-color: #f0f4ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.keyword-name {
|
||||||
|
font-weight: 500;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.keyword-score {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: #888;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected-keyword {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
padding: 10px;
|
||||||
|
background-color: #f0f4ff;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prompt-result {
|
||||||
|
margin-top: 30px;
|
||||||
|
border: 1px solid #eee;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prompt-content {
|
||||||
|
background-color: #f9f9f9;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 5px;
|
||||||
|
margin-top: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#prompt-title {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
#prompt-body {
|
||||||
|
white-space: pre-wrap;
|
||||||
|
line-height: 1.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.copy-container {
|
||||||
|
margin-top: 20px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.copy-success {
|
||||||
|
margin-left: 10px;
|
||||||
|
color: #4CAF50;
|
||||||
|
}
|
||||||
|
|
||||||
|
footer {
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 40px;
|
||||||
|
padding: 20px 0;
|
||||||
|
color: #777;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.keywords-container {
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
|
header h1 {
|
||||||
|
font-size: 2rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
187
styles.css
Normal file
187
styles.css
Normal file
@@ -0,0 +1,187 @@
|
|||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: 'Poppins', sans-serif;
|
||||||
|
background-color: #f5f7fa;
|
||||||
|
color: #333;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
header {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 40px;
|
||||||
|
padding: 20px 0;
|
||||||
|
background: linear-gradient(135deg, #6e8efb, #a777e3);
|
||||||
|
color: white;
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
header h1 {
|
||||||
|
font-size: 2.5rem;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
header p {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
section {
|
||||||
|
background: white;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 25px;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
color: #444;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.refresh-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
background-color: #6e8efb;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 10px 20px;
|
||||||
|
border-radius: 5px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 500;
|
||||||
|
transition: background-color 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:hover {
|
||||||
|
background-color: #5a7df7;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:disabled {
|
||||||
|
background-color: #cccccc;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading {
|
||||||
|
margin-left: 15px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.keywords-container {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.keyword-card {
|
||||||
|
background: #f9f9f9;
|
||||||
|
border: 1px solid #eee;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 15px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: transform 0.2s, box-shadow 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.keyword-card:hover {
|
||||||
|
transform: translateY(-3px);
|
||||||
|
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.keyword-card.selected {
|
||||||
|
border: 2px solid #6e8efb;
|
||||||
|
background-color: #f0f4ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.keyword-name {
|
||||||
|
font-weight: 500;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.keyword-metrics {
|
||||||
|
margin-top: 8px;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: #666;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.opportunity-score {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.highlight {
|
||||||
|
color: #6e8efb;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected-keyword {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
padding: 10px;
|
||||||
|
background-color: #f0f4ff;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prompt-result {
|
||||||
|
margin-top: 30px;
|
||||||
|
border: 1px solid #eee;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prompt-content {
|
||||||
|
background-color: #f9f9f9;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 5px;
|
||||||
|
margin-top: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#prompt-title {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
#prompt-body {
|
||||||
|
white-space: pre-wrap;
|
||||||
|
line-height: 1.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.copy-container {
|
||||||
|
margin-top: 20px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.copy-success {
|
||||||
|
margin-left: 10px;
|
||||||
|
color: #4CAF50;
|
||||||
|
}
|
||||||
|
|
||||||
|
footer {
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 40px;
|
||||||
|
padding: 20px 0;
|
||||||
|
color: #777;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user