diff --git a/app.js b/app.js new file mode 100644 index 0000000..70d5379 --- /dev/null +++ b/app.js @@ -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 = ` +
${keyword.keyword}
+
+
Opportunity: ${score}
+
Search: ${searchVolume}
+
Results: ${resultCount}
+
Competition: ${competitionLevel}
+
+ `; + + 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(); +}); \ No newline at end of file diff --git a/index.html b/index.html new file mode 100644 index 0000000..a5767e0 --- /dev/null +++ b/index.html @@ -0,0 +1,57 @@ + + + + + + Trending Keywords & Article Prompts + + + + +
+
+

Trending Keywords

+

Discover globally trending topics and generate article prompts

+
+ +
+ + +
+

Article Prompt Generator

+
+

Selected keyword: None selected

+
+ + + +
+
+ + +
+ + + + \ No newline at end of file diff --git a/public/app.js b/public/app.js new file mode 100644 index 0000000..43e5cdb --- /dev/null +++ b/public/app.js @@ -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 = '

No trending keywords found. Please try again later.

'; + } + } catch (error) { + console.error('Error fetching trending keywords:', error); + keywordsContainer.innerHTML = '

Failed to load trending keywords. Please try again.

'; + } 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 = ` +
${keyword.keyword}
+
Trend score: ${keyword.score}
+ `; + + 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(); +}); \ No newline at end of file diff --git a/public/index.html b/public/index.html new file mode 100644 index 0000000..a5767e0 --- /dev/null +++ b/public/index.html @@ -0,0 +1,57 @@ + + + + + + Trending Keywords & Article Prompts + + + + +
+
+

Trending Keywords

+

Discover globally trending topics and generate article prompts

+
+ +
+ + +
+

Article Prompt Generator

+
+

Selected keyword: None selected

+
+ + + +
+
+ + +
+ + + + \ No newline at end of file diff --git a/public/styles.css b/public/styles.css new file mode 100644 index 0000000..ad044e2 --- /dev/null +++ b/public/styles.css @@ -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; + } +} \ No newline at end of file diff --git a/styles.css b/styles.css new file mode 100644 index 0000000..156fd9a --- /dev/null +++ b/styles.css @@ -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; +} \ No newline at end of file