Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Dockerfile.backend
Original file line number Diff line number Diff line change
Expand Up @@ -51,4 +51,4 @@ EXPOSE 8000
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
CMD curl -f http://localhost:8000/health || exit 1

CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
CMD ["uvicorn", "main:socket_app", "--host", "0.0.0.0", "--port", "8000"]
73 changes: 73 additions & 0 deletions backend/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import asyncio
import uuid
import random
from datetime import datetime, timezone
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
import socketio

app = FastAPI(title="SolFoundry WebSocket Backend")

app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)

# Async Server for Socket.IO
sio = socketio.AsyncServer(async_mode='asgi', cors_allowed_origins='*')
socket_app = socketio.ASGIApp(sio, other_asgi_app=app)

# Dummy data generation for simulated real-time events
EVENT_TYPES = ['completed', 'submitted', 'posted', 'review']
USERNAMES = ['devbuilder', 'KodeSage', 'SolanaLabs', '0xHunter', 'RustWiz', 'CryptoKnight']

async def generate_mock_events():
"""Background task that emits random events to all connected clients."""
while True:
await asyncio.sleep(random.uniform(5, 15)) # Emit an event every 5 to 15 seconds

event_type = random.choice(EVENT_TYPES)
username = random.choice(USERNAMES)
detail = ""

if event_type == 'completed':
detail = f"${random.randint(100, 2000)} USDC from Bounty #{random.randint(1, 200)}"
elif event_type == 'submitted':
detail = f"PR to Bounty #{random.randint(1, 200)}"
elif event_type == 'posted':
detail = f"Bounty #{random.randint(1, 200)} — ${random.randint(500, 5000)} USDC"
elif event_type == 'review':
detail = f"Bounty #{random.randint(1, 200)} — {random.choice(['8.5/10', '9/10', '10/10'])}"

event_payload = {
"id": str(uuid.uuid4()),
"type": event_type,
"username": username,
"avatar_url": None,
"detail": detail,
"timestamp": datetime.now(timezone.utc).isoformat()
}

# Broadcast to all connected clients
await sio.emit('activity_feed', event_payload)


@sio.event
async def connect(sid, environ):
print(f"Client connected: {sid}")

@sio.event
async def disconnect(sid):
print(f"Client disconnected: {sid}")

@app.on_event("startup")
async def startup_event():
# Start the background task to generate mock events
asyncio.create_task(generate_mock_events())

if __name__ == "__main__":
import uvicorn
uvicorn.run("main:socket_app", host="0.0.0.0", port=8000, reload=True)
5 changes: 5 additions & 0 deletions backend/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
fastapi>=0.100.0
uvicorn[standard]>=0.22.0
python-socketio>=5.8.0
websockets>=11.0.3
uvloop>=0.17.0
86 changes: 86 additions & 0 deletions frontend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"react-syntax-highlighter": "^15.5.0",
"recharts": "^3.8.0",
"remark-gfm": "^4.0.1",
"socket.io-client": "^4.8.3",
"tailwind-merge": "^3.5.0",
"tailwindcss": "^4.2.2"
},
Expand Down
87 changes: 77 additions & 10 deletions frontend/src/components/home/ActivityFeed.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import React, { useState, useEffect } from 'react';
import React, { useState, useEffect, useCallback } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { Filter } from 'lucide-react';
import { slideInRight } from '../../lib/animations';
import { timeAgo } from '../../lib/utils';
import { useSocket } from '../../hooks/useSocket';

interface ActivityEvent {
id: string;
Expand Down Expand Up @@ -76,20 +78,80 @@ function EventItem({ event }: { event: ActivityEvent }) {
}

export function ActivityFeed({ events }: { events?: ActivityEvent[] }) {
const displayEvents = events?.length ? events.slice(0, 4) : MOCK_EVENTS;
const [visibleEvents, setVisibleEvents] = useState<ActivityEvent[]>(displayEvents.slice(0, 4));
const [feedEvents, setFeedEvents] = useState<ActivityEvent[]>(events?.length ? events : MOCK_EVENTS);
const [activeFilters, setActiveFilters] = useState<Set<string>>(new Set(['completed', 'submitted', 'posted', 'review']));
const [showFilters, setShowFilters] = useState(false);

useEffect(() => {
setVisibleEvents(displayEvents.slice(0, 4));
}, [events]);
const handleNewEvent = useCallback((newEvent: ActivityEvent) => {
setFeedEvents(prev => [newEvent, ...prev].slice(0, 20));
}, []);

const { isConnected } = useSocket<ActivityEvent>('activity_feed', handleNewEvent);

const toggleFilter = (type: string) => {
setActiveFilters(prev => {
const next = new Set(prev);
if (next.has(type)) {
next.delete(type);
} else {
next.add(type);
}
return next;
});
};

const visibleEvents = feedEvents
.filter(e => activeFilters.has(e.type))
.slice(0, 4);

const ALL_TYPES = ['completed', 'submitted', 'posted', 'review'];

return (
<section className="w-full border-y border-border bg-forge-900/50 py-4 overflow-hidden">
<section className="w-full border-y border-border bg-forge-900/50 py-4 overflow-hidden relative">
<div className="max-w-7xl mx-auto px-4">
<div className="flex items-center gap-3 mb-3">
<span className="w-2 h-2 rounded-full bg-emerald animate-pulse-glow" />
<span className="font-mono text-xs text-text-muted uppercase tracking-wider">Recent Activity</span>
<div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-3">
<span className={`w-2 h-2 rounded-full ${isConnected ? 'bg-emerald animate-pulse-glow' : 'bg-status-error'}`} title={isConnected ? "Connected to live feed" : "Disconnected, attempting to reconnect..."} />
<span className="font-mono text-xs text-text-muted uppercase tracking-wider">
{isConnected ? "Live Activity" : "Offline"}
</span>
</div>

<button
onClick={() => setShowFilters(!showFilters)}
className={`p-1.5 rounded-md transition-colors flex items-center gap-2 ${showFilters ? 'bg-forge-800 text-text-primary' : 'text-text-muted hover:text-text-secondary hover:bg-forge-800/50'}`}
title="Filter feed"
>
<Filter className="w-3.5 h-3.5" />
<span className="text-xs font-mono">Filters</span>
</button>
</div>

<AnimatePresence>
{showFilters && (
<motion.div
initial={{ height: 0, opacity: 0 }}
animate={{ height: 'auto', opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
className="flex flex-wrap gap-2 mb-4 overflow-hidden"
>
{ALL_TYPES.map(type => (
<button
key={type}
onClick={() => toggleFilter(type)}
className={`px-3 py-1 text-[10px] uppercase tracking-wider font-bold rounded-md border transition-colors ${
activeFilters.has(type)
? 'bg-emerald/10 border-emerald/30 text-emerald'
: 'bg-forge-800 border-border text-text-muted hover:text-text-secondary'
}`}
>
{type}
</button>
))}
</motion.div>
)}
</AnimatePresence>

<div className="space-y-1">
<AnimatePresence mode="popLayout">
{visibleEvents.map((event) => (
Expand All @@ -104,6 +166,11 @@ export function ActivityFeed({ events }: { events?: ActivityEvent[] }) {
<EventItem event={event} />
</motion.div>
))}
{visibleEvents.length === 0 && (
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} className="py-4 text-center text-sm text-text-muted font-mono">
No events match your filters.
</motion.div>
)}
</AnimatePresence>
</div>
</div>
Expand Down
39 changes: 39 additions & 0 deletions frontend/src/hooks/useSocket.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { useEffect, useState } from 'react';
import { io, Socket } from 'socket.io-client';

const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:8000';

export function useSocket<T>(eventName: string, onEvent: (data: T) => void) {
const [isConnected, setIsConnected] = useState(false);
const [socket, setSocket] = useState<Socket | null>(null);

useEffect(() => {
// Graceful fallback to polling if WebSocket fails
const socketInstance = io(API_URL, {
transports: ['websocket', 'polling'],
reconnectionAttempts: 10,
reconnectionDelay: 1000,
});

setSocket(socketInstance);

socketInstance.on('connect', () => {
setIsConnected(true);
});

socketInstance.on('disconnect', () => {
setIsConnected(false);
});

socketInstance.on(eventName, (data: T) => {
onEvent(data);
});

return () => {
socketInstance.off(eventName);
socketInstance.disconnect();
};
}, [eventName, onEvent]);

return { isConnected, socket };
}