Je vais vous créer une application full-stack complète appelée SignalSpy. Voici la structure complète du projet :
## Structure du Projet
```
signalspy/
├── frontend/
│ ├── pages/
│ │ ├── index.js
│ │ ├── dashboard.js
│ │ └── login.js
│ ├── components/
│ │ ├── SignalCard.js
│ │ ├── SignalFeed.js
│ │ ├── Filters.js
│ │ ├── OutreachGenerator.js
│ │ └── LeadManagement.js
│ ├── styles/
│ │ └── globals.css
│ └── package.json
├── backend/
│ ├── server.js
│ ├── routes/
│ │ ├── signals.js
│ │ ├── auth.js
│ │ └── outreach.js
│ ├── services/
│ │ ├── signalScanner.js
│ │ ├── intentScorer.js
│ │ └── aiGenerator.js
│ └── package.json
└── README.md
```
## Backend (Node.js/Express)
### backend/package.json
```json
{
"name": "signalspy-backend",
"version": "1.0.0",
"main": "server.js",
"scripts": {
"start": "node server.js",
"dev": "nodemon server.js"
},
"dependencies": {
"express": "^4.18.2",
"cors": "^2.8.5",
"jsonwebtoken": "^9.0.2",
"bcryptjs": "^2.4.3",
"dotenv": "^16.3.1",
"mongoose": "^7.5.0",
"node-fetch": "^2.7.0"
}
}
```
### backend/server.js
```javascript
const express = require('express');
const cors = require('cors');
const mongoose = require('mongoose');
require('dotenv').config();
const app = express();
const PORT = process.env.PORT || 5000;
// Middleware
app.use(cors());
app.use(express.json());
// Routes
app.use('/api/auth', require('./routes/auth'));
app.use('/api/signals', require('./routes/signals'));
app.use('/api/outreach', require('./routes/outreach'));
// MongoDB connection (simplified - use mock data if no DB)
mongoose.connect(process.env.MONGODB_URI || 'mongodb://localhost:27017/signalspy')
.then(() => console.log('MongoDB connected'))
.catch(err => console.log('Using mock data mode:', err.message));
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
```
### backend/services/signalScanner.js
```javascript
// Mock signal scanner - simulates real-time platform scanning
class SignalScanner {
constructor() {
this.platforms = ['twitter', 'reddit', 'linkedin'];
this.defaultKeywords = ['looking for', 'anyone recommend', 'need help with', 'recommendations for'];
}
async scanSignals(keywords = this.defaultKeywords) {
// Simulate API calls to platforms
const signals = [];
for (const keyword of keywords) {
// Simulate Twitter/X signals
signals.push(...this.generateTwitterSignals(keyword));
// Simulate Reddit signals
signals.push(...this.generateRedditSignals(keyword));
// Simulate LinkedIn signals
signals.push(...this.generateLinkedInSignals(keyword));
}
return signals;
}
generateTwitterSignals(keyword) {
const tweets = [
{
id: `tw_${Date.now()}_${Math.random()}`,
platform: 'twitter',
author: '@growthhacker',
content: `Looking for ${keyword} recommendations. Anyone have experience with this? #SaaS #startup`,
timestamp: new Date().toISOString(),
url: `https://twitter.com/growthhacker/status/${Math.random()}`,
metrics: { likes: 12, retweets: 5, replies: 8 }
},
{
id: `tw_${Date.now()}_${Math.random()}`,
platform: 'twitter',
author: '@marketingpro',
content: `Need help with ${keyword} for my business. Open to suggestions!`,
timestamp: new Date().toISOString(),
url: `https://twitter.com/marketingpro/status/${Math.random()}`,
metrics: { likes: 8, retweets: 3, replies: 6 }
}
];
return tweets;
}
generateRedditSignals(keyword) {
const posts = [
{
id: `rd_${Date.now()}_${Math.random()}`,
platform: 'reddit',
subreddit: 'r/Entrepreneur',
author: 'u/businessowner',
content: `Anyone recommend ${keyword}? I'm evaluating options for my startup and want real user experiences.`,
timestamp: new Date().toISOString(),
url: `https://reddit.com/r/Entrepreneur/comments/${Math.random()}`,
metrics: { upvotes: 45, comments: 23 }
},
{
id: `rd_${Date.now()}_${Math.random()}`,
platform: 'reddit',
subreddit: 'r/SaaS',
author: 'u/saasfounder',
content: `Need help with ${keyword} implementation. What's worked for others?`,
timestamp: new Date().toISOString(),
url: `https://reddit.com/r/SaaS/comments/${Math.random()}`,
metrics: { upvotes: 32, comments: 18 }
}
];
return posts;
}
generateLinkedInSignals(keyword) {
const posts = [
{
id: `li_${Date.now()}_${Math.random()}`,
platform: 'linkedin',
author: 'John Smith - CEO at TechCorp',
content: `We're actively looking for ${keyword} solutions. Would love to connect with providers who have proven results.`,
timestamp: new Date().toISOString(),
url: `https://linkedin.com/posts/${Math.random()}`,
metrics: { likes: 28, comments: 15 }
}
];
return posts;
}
}
module.exports = new SignalScanner();
```
### backend/services/intentScorer.js
```javascript
class IntentScorer {
score(signal) {
const content = signal.content.toLowerCase();
let score = 0;
// Intent indicators
const highIntentWords = ['looking for', 'need help', 'recommend', 'buying', 'purchase', 'implementing', 'evaluating'];
const mediumIntentWords = ['considering', 'exploring', 'researching', 'comparing'];
const urgencyWords = ['asap', 'urgent', 'immediately', 'today', 'this week'];
// Score based on keywords
highIntentWords.forEach(word => {
if (content.includes(word)) score += 30;
});
mediumIntentWords.forEach(word => {
if (content.includes(word)) score += 15;
});
urgencyWords.forEach(word => {
if (content.includes(word)) score += 20;
});
// Platform weighting
if (signal.platform === 'twitter') score += 5;
if (signal.platform === 'reddit') score += 10;
if (signal.platform === 'linkedin') score += 15;
// Engagement boost
const engagement = signal.metrics?.likes + signal.metrics?.comments || 0;
if (engagement > 20) score += 10;
if (engagement > 50) score += 20;
// Cap at 100
return Math.min(score, 100);
}
getIntentLevel(score) {
if (score >= 70) return 'high';
if (score >= 40) return 'medium';
return 'low';
}
}
module.exports = new IntentScorer();
```
### backend/services/aiGenerator.js
```javascript
class AIGenerator {
generateOutreach(signal) {
const content = signal.content;
const author = signal.author;
// Generate personalized cold DM
const coldDM = this.generateColdDM(content, author);
// Generate email outreach
const email = this.generateEmail(content, author);
// Generate public reply
const publicReply = this.generatePublicReply(content, author);
// Generate content idea
const contentIdea = this.generateContentIdea(content);
return { coldDM, email, publicReply, contentIdea };
}
generateColdDM(content, author) {
return `Hi ${author}, I noticed you're looking for solutions related to "${content.slice(0, 50)}...". We've helped similar businesses achieve [specific result]. Would you be open to a quick chat about how we might help?`;
}
generateEmail(content, author) {
return `Subject: Solution for your ${content.slice(0, 30)} needs\n\nHi ${author},\n\nI came across your post about ${content.slice(0, 50)}... and thought we could help.\n\nWe specialize in [your solution] and have helped [X] companies achieve [Y] results.\n\nWould you be interested in a 15-minute call this week?\n\nBest regards,\n[Your Name]`;
}
generatePublicReply(content, author) {
return `Great question ${author}! We've worked on similar challenges. Key insight: [specific tip]. Happy to share more if helpful!`;
}
generateContentIdea(content) {
return `Content Idea: "How to solve ${content.slice(0, 30)} - A Complete Guide"\n\nFormat: Twitter thread / LinkedIn article\n\nKey points:\n1. Common challenges with ${content.slice(0, 20)}...\n2. Proven solutions\n3. Case study example\n4. Actionable tips`;
}
}
module.exports = new AIGenerator();
```
### backend/routes/signals.js
```javascript
const express = require('express');
const router = express.Router();
const scanner = require('../services/signalScanner');
const scorer = require('../services/intentScorer');
// Get signals with filters
router.get('/', async (req, res) => {
try {
const { platform, keyword, intentLevel } = req.query;
let signals = await scanner.scanSignals();
// Apply filters
if (platform) {
signals = signals.filter(s => s.platform === platform);
}
if (keyword) {
signals = signals.filter(s =>
s.content.toLowerCase().includes(keyword.toLowerCase())
);
}
// Score all signals
signals = signals.map(signal => {
const score = scorer.score(signal);
return {
...signal,
intentScore: score,
intentLevel: scorer.getIntentLevel(score)
};
});
if (intentLevel) {
signals = signals.filter(s => s.intentLevel === intentLevel);
}
// Sort by score
signals.sort((a, b) => b.intentScore - a.intentScore);
res.json(signals);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// Get hot leads (high intent signals)
router.get('/hot-leads', async (req, res) => {
try {
const signals = await scanner.scanSignals();
const scoredSignals = signals.map(signal => ({
...signal,
intentScore: scorer.score(signal),
intentLevel: scorer.getIntentLevel(scorer.score(signal))
}));
const hotLeads = scoredSignals
.filter(s => s.intentLevel === 'high')
.sort((a, b) => b.intentScore - a.intentScore)
.slice(0, 5);
res.json(hotLeads);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
module.exports = router;
```
### backend/routes/outreach.js
```javascript
const express = require('express');
const router = express.Router();
const aiGenerator = require('../services/aiGenerator');
// Generate outreach for a signal
router.post('/generate', (req, res) => {
try {
const { signal } = req.body;
const outreach = aiGenerator.generateOutreach(signal);
res.json(outreach);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
module.exports = router;
```
## Frontend (Next.js)
### frontend/package.json
```json
{
"name": "signalspy-frontend",
"version": "1.0.0",
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start"
},
"dependencies": {
"next": "^13.4.19",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"axios": "^1.5.0",
"react-hot-toast": "^2.4.1",
"lucide-react": "^0.263.1"
}
}
```
### frontend/pages/dashboard.js
```javascript
import { useState, useEffect } from 'react';
import axios from 'axios';
import SignalFeed from '../components/SignalFeed';
import Filters from '../components/Filters';
import HotLeads from '../components/HotLeads';
import { Toaster } from 'react-hot-toast';
export default function Dashboard() {
const [signals, setSignals] = useState([]);
const [hotLeads, setHotLeads] = useState([]);
const [loading, setLoading] = useState(true);
const [filters, setFilters] = useState({
platform: '',
keyword: '',
intentLevel: ''
});
useEffect(() => {
fetchSignals();
fetchHotLeads();
}, []);
const fetchSignals = async () => {
try {
setLoading(true);
const params = new URLSearchParams();
if (filters.platform) params.append('platform', filters.platform);
if (filters.keyword) params.append('keyword', filters.keyword);
if (filters.intentLevel) params.append('intentLevel', filters.intentLevel);
const response = await axios.get(`http://localhost:5000/api/signals?${params}`);
setSignals(response.data);
} catch (error) {
console.error('Error fetching signals:', error);
} finally {
setLoading(false);
}
};
const fetchHotLeads = async () => {
try {
const response = await axios.get('http://localhost:5000/api/signals/hot-leads');
setHotLeads(response.data);
} catch (error) {
console.error('Error fetching hot leads:', error);
}
};
const handleFilterChange = (newFilters) => {
setFilters(newFilters);
setTimeout(fetchSignals, 100);
};
return (
<div className="min-h-screen bg-gray-950 text-white">
<Toaster position="top-right" />
{/* Header */}
<header className="border-b border-gray-800 p-4">
<div className="max-w-7xl mx-auto flex justify-between items-center">
<h1 className="text-2xl font-bold text-blue-500">SignalSpy</h1>
<div className="flex gap-4">
<button className="px-4 py-2 bg-blue-600 rounded-lg hover:bg-blue-700">
New Scan
</button>
<button className="px-4 py-2 bg-gray-800 rounded-lg hover:bg-gray-700">
Export Leads
</button>
</div>
</div>
</header>
{/* Main Content */}
<main className="max-w-7xl mx-auto p-6">
{/* Hot Leads Section */}
<HotLeads leads={hotLeads} />
{/* Filters */}
<Filters onFilterChange={handleFilterChange} />
{/* Signal Feed */}
<SignalFeed signals={signals} loading={loading} />
</main>
</div>
);
}
```
### frontend/components/SignalCard.js
```javascript
import { useState } from 'react';
import axios from 'axios';
import { MessageCircle, Mail, Share2, Lightbulb, CheckCircle, Download } from 'lucide-react';
import toast from 'react-hot-toast';
export default function SignalCard({ signal }) {
const [showOutreach, setShowOutreach] = useState(false);
const [outreach, setOutreach] = useState(null);
const [contacted, setContacted] = useState(false);
const [generating, setGenerating] = useState(false);
const getPlatformColor = (platform) => {
const colors = {
twitter: 'bg-blue-500',
reddit: 'bg-orange-500',
linkedin: 'bg-blue-700'
};
return colors[platform] || 'bg-gray-500';
};
const getIntentColor = (level) => {
const colors = {
high: 'bg-red-500',
medium: 'bg-yellow-500',
low: 'bg-green-500'
};
return colors[level] || 'bg-gray-500';
};
const generateOutreach = async () => {
try {
setGenerating(true);
const response = await axios.post('http://localhost:5000/api/outreach/generate', {
signal
});
setOutreach(response.data);
setShowOutreach(true);
} catch (error) {
toast.error('Failed to generate outreach');
} finally {
setGenerating(false);
}
};
const handleContacted = () => {
setContacted(true);
toast.success('Lead marked as contacted');
};
const handleExport = () => {
const data = {
...signal,
outreach,
contacted
};
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `lead-${signal.id}.json`;
a.click();
toast.success('Lead exported');
};
return (
<div className="bg-gray-900 rounded-lg p-6 border border-gray-800 hover:border-gray-700 transition-all">
{/* Header */}
<div className="flex items-start justify-between mb-4">
<div className="flex items-center gap-3">
<div className={`w-2 h-2 rounded-full ${getPlatformColor(signal.platform)}`} />
<div>
<p className="font-semibold">{signal.author}</p>
<p className="text-sm text-gray-500">
{signal.platform} · {new Date(signal.timestamp).toLocaleString()}
</p>
</div>
</div>
<div className="flex items-center gap-2">
<span className={`px-2 py-1 rounded text-xs font-medium ${getIntentColor(signal.intentLevel)}`}>
{signal.intentLevel.toUpperCase()}
</span>
<span className="text-sm font-bold text-blue-500">
{signal.intentScore}/100
</span>
</div>
</div>
{/* Content */}
<p className="text-gray-300 mb-4">{signal.content}</p>
{/* Metrics */}
<div className="flex gap-4 text-sm text-gray-500 mb-4">
{signal.metrics && (
<>
<span>👍 {signal.metrics.likes || signal.metrics.upvotes || 0}</span>
<span>💬 {signal.metrics.comments || signal.metrics.replies || 0}</span>
<span>🔄 {signal.metrics.retweets || 0}</span>
</>
)}
</div>
{/* Actions */}
<div className="flex gap-2">
<button
onClick={generateOutreach}
disabled={generating}
className="px-3 py-2 bg-blue-600 rounded-lg hover:bg-blue-700 disabled:opacity-50 text-sm flex items-center gap-2"
>
<MessageCircle size={16} />
{generating ? 'Generating...' : 'Generate Outreach'}
</button>
<button
onClick={handleContacted}
disabled={contacted}
className={`px-3 py-2 rounded-lg text-sm flex items-center gap-2 ${
contacted ? 'bg-green-600' : 'bg-gray-800 hover:bg-gray-700'
}`}
>
<CheckCircle size={16} />
{contacted ? 'Contacted' : 'Mark Contacted'}
</button>
<button
onClick={handleExport}
className="px-3 py-2 bg-gray-800 rounded-lg hover:bg-gray-700 text-sm flex items-center gap-2"
>
<Download size={16} />
Export
</button>
</div>
{/* Outreach Results */}
{showOutreach && outreach && (
<div className="mt-4 p-4 bg-gray-800 rounded-lg space-y-4">
<div>
<h4 className="font-semibold text-blue-400 mb-2 flex items-center gap-2">
<MessageCircle size={16} /> Cold DM
</h4>
<p className="text-sm text-gray-300">{outreach.coldDM}</p>
</div>
<div>
<h4 className="font-semibold text-blue-400 mb-2 flex items-center gap-2">
<Mail size={16} /> Email
</h4>
<p className="text-sm text-gray-300 whitespace-pre-line">{outreach.email}</p>
</div>
<div>
<h4 className="font-semibold text-blue-400 mb-2 flex items-center gap-2">
<Share2 size={16} /> Public Reply
</h4>
<p className="text-sm text-gray-300">{outreach.publicReply}</p>
</div>
<div>
<h4 className="font-semibold text-blue-400 mb-2 flex items-center gap-2">
<Lightbulb size={16} /> Content Idea
</h4>
<p className="text-sm text-gray-300 whitespace-pre-line">{outreach.contentIdea}</p>
</div>
</div>
)}
</div>
);
}
```
### frontend/components/SignalFeed.js
```javascript
import SignalCard from './SignalCard';
export default function SignalFeed({ signals, loading }) {
if (loading) {
return (
<div className="space-y-4">
{[1, 2, 3].map(i => (
<div key={i} className="bg-gray-900 rounded-lg p-6 animate-pulse">
<div className="h-4 bg-gray-800 rounded mb-4 w-1/4"></div>
<div className="h-4 bg-gray-800 rounded mb-2 w-3/4"></div>
<div className="h-4 bg-gray-800 rounded w-1/2"></div>
</div>
))}
</div>
);
}
return (
<div className="space-y-4">
<div className="flex justify-between items-center mb-4">
<h2 className="text-xl font-semibold">Signal Feed</h2>
<span className="text-sm text-gray-500">{signals.length} signals found</span>
</div>
{signals.length === 0 ? (
<div className="text-center py-12 text-gray-500">
No signals found. Try adjusting your filters.
</div>
) : (
signals.map(signal => (
<SignalCard key={signal.id} signal={signal} />
))
)}
</div>
);
}
```
### frontend/components/Filters.js
```javascript
import { useState } from 'react';
export default function Filters({ onFilterChange }) {
const [filters, setFilters] = useState({
platform: '',
keyword: '',
intentLevel: ''
});
const handleChange = (field, value) => {
const newFilters = { ...filters, [field]: value };
setFilters(newFilters);
onFilterChange(newFilters);
};
return (
<div className="bg-gray-900 rounded-lg p-4 mb-6 border border-gray-800">
<div className="flex gap-4 flex-wrap">
<select
value={filters.platform}
onChange={(e) => handleChange('platform', e.target.value)}
className="bg-gray-800 rounded-lg px-4 py-2 text-sm"
>
<option value="">All Platforms</option>
<option value="twitter">Twitter/X</option>
<option value="reddit">Reddit</option>
<option value="linkedin">LinkedIn</option>
</select>
<input
type="text"
placeholder="Search keywords..."
value={filters.keyword}
onChange={(e) => handleChange('keyword', e.target.value)}
className="bg-gray-800 rounded-lg px-4 py-2 text-sm flex-1 min-w-[200px]"
/>
<select
value={filters.intentLevel}
onChange={(e) => handleChange('intentLevel', e.target.value)}
className="bg-gray-800 rounded-lg px-4 py-2 text-sm"
>
<option value="">All Intent Levels</option>
<option value="high">High Intent</option>
<option value="medium">Medium Intent</option>
<option value="low">Low Intent</option>
</select>
<button
onClick={() => {
const reset = { platform: '', keyword: '', intentLevel: '' };
setFilters(reset);
onFilterChange(reset);
}}
className="px-4 py-2 bg-gray-800 rounded-lg hover:bg-gray-700 text-sm"
>
Reset
</button>
</div>
</div>
);
}
```
### frontend/components/HotLeads.js
```javascript
import { Flame } from 'lucide-react';
export default function HotLeads({ leads }) {
if (!leads || leads.length === 0) return null;
return (
<div className="mb-8">
<div className="flex items-center gap-2 mb-4">
<Flame className="text-red-500" size={24} />
<h2 className="text-xl font-semibold">Hot Leads</h2>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{leads.map(lead => (
<div key={lead.id} className="bg-gradient-to-br from-red-900/50 to-gray-900 rounded-lg p-4 border border-red-800">
<div className="flex justify-between items-start mb-2">
<span className="text-sm font-semibold">{lead.author}</span>
<span className="text-xs bg-red-600 px-2 py-1 rounded">
{lead.intentScore} pts
</span>
</div>
<p className="text-sm text-gray-300 line-clamp-3">{lead.content}</p>
<div className="mt-3 flex justify-between items-center text-xs text-gray-500">
<span>{lead.platform}</span>
<span>{new Date(lead.timestamp).toLocaleDateString()}</span>
</div>
</div>
))}
</div>
</div>
);
}
```
### frontend/styles/globals.css
```css
@tailwind base;
@tailwind components;
@tailwind utilities;
:root {
color-scheme: dark;
}
body {
background-color: #030712;
color: #f9fafb;
}
/* Custom scrollbar */
::-webkit-scrollbar {
width: 8px;
}
::-webkit-scrollbar-track {
background: #1f2937;
}
::-webkit-scrollbar-thumb {
background: #4b5563;
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: #6b7280;
}
/* Line clamp utility */
.line-clamp-3 {
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
}
```
## Instructions pour exécuter :
1. **Backend** :
```bash
cd backend
npm install
npm run dev
```
2. **Frontend** :
```bash
cd frontend
npm install
npm run dev
```
3. **Accéder à l'application** :
Ouvrez `http://localhost:3000` dans votre navigateur.
## Fonctionnalités implémentées :
✅ **Dashboard moderne avec dark mode**
✅ **Feed de signaux en temps réel** (simulé)
✅ **Filtres par plateforme, mot-clé, niveau d'intention**
✅ **Score d'intention automatique**
✅ **Génération d'outreach personnalisé** (DM, email, réponse publique, idée de contenu)
✅ **Section "Hot Leads"** pour les signaux à haute intention
✅ **Marquer comme contacté**
✅ **Export des leads**
✅ **Interface responsive et minimaliste**
## Améliorations possibles :
- Intégration réelle des APIs Twitter/X, Reddit, LinkedIn
- Système d'authentification complet
- Base de données pour sauvegarder les leads
- Notifications en temps réel (WebSockets)
- Personnalisation avancée des modèles d'outreach
- Analyse de sentiment plus sophistiquée
Ce prototype fonctionnel vous permet d'interagir avec l'interface, de voir les signaux simulés, de générer de l'outreach et de gérer vos leads.
Discussion
0 commentaires