YeboLink Dashboard Deep Dive
The YeboLink Dashboard is a React-based web application for managing messaging, contacts, credits, and account settings.
Technology Stack
- Framework: React 18 + TypeScript
- Build: Vite
- Styling: TailwindCSS
- State Management: TanStack Query (React Query)
- Routing: React Router v6
- Icons: Lucide React
- HTTP Client: Axios
- Hosting: Cloudflare Pages
Project Structure
yebolink-dashboard/
├── src/
│ ├── App.tsx # Main app with routing
│ ├── main.tsx # Entry point
│ ├── components/
│ │ ├── Layout.tsx # Page layout wrapper
│ │ ├── ProtectedRoute.tsx # Auth guard
│ │ └── ui/ # Reusable UI components
│ ├── lib/
│ │ ├── api.ts # Axios instance
│ │ └── auth.tsx # Auth context
│ └── pages/
│ ├── Analytics.tsx
│ ├── BillingSuccess.tsx
│ ├── Contacts.tsx
│ ├── Credits.tsx
│ ├── Dashboard.tsx
│ ├── Login.tsx
│ ├── Messages.tsx
│ ├── Onboarding.tsx
│ ├── SendMessage.tsx
│ ├── Settings.tsx
│ └── Signup.tsx
├── public/
├── index.html
├── package.json
├── tailwind.config.js
├── tsconfig.json
└── vite.config.tsApplication Entry (App.tsx)
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { AuthProvider } from '@/lib/auth'
import { ToastProvider } from '@/components/ui/toast'
import { ProtectedRoute } from '@/components/ProtectedRoute'
// Pages
import Login from '@/pages/Login'
import Signup from '@/pages/Signup'
import Onboarding from '@/pages/Onboarding'
import Dashboard from '@/pages/Dashboard'
import Messages from '@/pages/Messages'
import SendMessage from '@/pages/SendMessage'
import Contacts from '@/pages/Contacts'
import Credits from '@/pages/Credits'
import Analytics from '@/pages/Analytics'
import Settings from '@/pages/Settings'
import BillingSuccess from '@/pages/BillingSuccess'
const queryClient = new QueryClient({
defaultOptions: {
queries: {
refetchOnWindowFocus: false,
retry: 1,
},
},
})
function App() {
return (
<QueryClientProvider client={queryClient}>
<AuthProvider>
<ToastProvider>
<BrowserRouter>
<Routes>
{/* Public routes */}
<Route path="/login" element={<Login />} />
<Route path="/signup" element={<Signup />} />
{/* Protected routes */}
<Route path="/onboarding" element={<ProtectedRoute><Onboarding /></ProtectedRoute>} />
<Route path="/dashboard" element={<ProtectedRoute><Dashboard /></ProtectedRoute>} />
<Route path="/messages" element={<ProtectedRoute><Messages /></ProtectedRoute>} />
<Route path="/send" element={<ProtectedRoute><SendMessage /></ProtectedRoute>} />
<Route path="/contacts" element={<ProtectedRoute><Contacts /></ProtectedRoute>} />
<Route path="/credits" element={<ProtectedRoute><Credits /></ProtectedRoute>} />
<Route path="/analytics" element={<ProtectedRoute><Analytics /></ProtectedRoute>} />
<Route path="/settings" element={<ProtectedRoute><Settings /></ProtectedRoute>} />
<Route path="/billing/success" element={<ProtectedRoute><BillingSuccess /></ProtectedRoute>} />
{/* Redirects */}
<Route path="/" element={<Navigate to="/dashboard" replace />} />
<Route path="*" element={<Navigate to="/dashboard" replace />} />
</Routes>
</BrowserRouter>
</ToastProvider>
</AuthProvider>
</QueryClientProvider>
)
}Authentication (lib/auth.tsx)
Context-based auth with JWT token management:
interface AuthContextType {
user: User | null
login: (email: string, password: string) => Promise<void>
signup: (data: SignupData) => Promise<void>
logout: () => void
refreshToken: () => Promise<void>
isLoading: boolean
}
export function AuthProvider({ children }: { children: React.ReactNode }) {
const [user, setUser] = useState<User | null>(null)
const [isLoading, setIsLoading] = useState(true)
// Check for existing session on mount
useEffect(() => {
const token = localStorage.getItem('token')
if (token) {
api.get('/auth/me')
.then(res => setUser(res.data.workspace))
.catch(() => localStorage.removeItem('token'))
.finally(() => setIsLoading(false))
} else {
setIsLoading(false)
}
}, [])
const login = async (email: string, password: string) => {
const res = await api.post('/auth/login', { email, password })
localStorage.setItem('token', res.data.token)
localStorage.setItem('refreshToken', res.data.refreshToken)
setUser(res.data.workspace)
}
const logout = () => {
const refreshToken = localStorage.getItem('refreshToken')
api.post('/auth/logout', { refreshToken }).catch(() => {})
localStorage.removeItem('token')
localStorage.removeItem('refreshToken')
setUser(null)
}
// ...
}API Client (lib/api.ts)
Axios instance with auth interceptors:
import axios from 'axios'
const api = axios.create({
baseURL: import.meta.env.VITE_API_URL || 'https://api.yebolink.com/api/v1',
})
// Add auth token to requests
api.interceptors.request.use((config) => {
const token = localStorage.getItem('token')
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
// Dashboard can also use API key for message sending
if (config.headers['X-Use-API-Key']) {
const apiKey = localStorage.getItem('apiKey')
if (apiKey) {
config.headers['X-API-Key'] = apiKey
}
delete config.headers['X-Use-API-Key']
}
return config
})
// Handle auth errors
api.interceptors.response.use(
(response) => response.data,
async (error) => {
if (error.response?.status === 401) {
// Try refresh token
const refreshToken = localStorage.getItem('refreshToken')
if (refreshToken) {
try {
const res = await axios.post(`${api.defaults.baseURL}/auth/refresh`, {
refreshToken,
})
localStorage.setItem('token', res.data.token)
localStorage.setItem('refreshToken', res.data.refreshToken)
// Retry original request
return api(error.config)
} catch {
// Refresh failed, logout
localStorage.clear()
window.location.href = '/login'
}
}
}
throw error
}
)
export default apiPages
Dashboard (Dashboard.tsx)
Main dashboard with metrics, quick actions, and recent messages.
Features:
- Credit balance with low-balance warning
- Quick send modal (SMS, WhatsApp, Email)
- Bulk send modal with template personalization
- Contact import modal
- Recent messages list
- Stats cards (total sent, delivery rate, contacts)
Key Components:
// Quick Send Modal
function QuickSendModal({ onClose }: { onClose: () => void }) {
const [channel, setChannel] = useState<'sms' | 'whatsapp' | 'email'>('sms')
const [to, setTo] = useState('')
const [text, setText] = useState('')
const mutation = useMutation({
mutationFn: () => api.post('/messages/send', {
to,
channel,
content: { text }
}),
onSuccess: () => {
toast({ title: '✓ Message sent' })
queryClient.invalidateQueries({ queryKey: ['messages'] })
queryClient.invalidateQueries({ queryKey: ['balance'] })
onClose()
},
})
// ...render form
}Data Fetching:
// Balance
const { data: balanceData } = useQuery({
queryKey: ['balance'],
queryFn: () => api.get('/account/balance'),
})
// Stats
const { data: statsData } = useQuery({
queryKey: ['stats'],
queryFn: () => api.get('/account/stats'),
})
// Recent messages
const { data: messages } = useQuery({
queryKey: ['messages', 'recent'],
queryFn: () => api.get('/messages', { params: { limit: 15 } }),
})Messages (Messages.tsx)
Full message history with filtering and pagination.
Features:
- Filter by channel, status, date range
- Pagination
- Message details modal
- Resend failed messages
Send Message (SendMessage.tsx)
Dedicated send page (alternative to dashboard quick send).
Features:
- Channel selection with visual cards
- Recipient input with validation
- Message composer with character count
- Preview before sending
- Send confirmation
Contacts (Contacts.tsx)
Contact management page.
Features:
- Contact list with search
- Create/edit contact modals
- Bulk import from CSV/paste
- Tag management
- Export contacts
Credits (Credits.tsx)
Credit purchase and billing management.
Features:
- Available packages with localized pricing
- Stripe checkout integration
- Transaction history
- Auto-top-up settings (future)
Stripe Checkout:
const handlePurchase = async (credits: number) => {
const res = await api.post('/billing/checkout', { credits })
// Redirect to Stripe
window.location.href = res.data.url
}Analytics (Analytics.tsx)
Detailed messaging analytics.
Features:
- Messages over time (line chart)
- Delivery status breakdown (pie chart)
- Hourly distribution
- Channel performance
- Export reports
Settings (Settings.tsx)
Account and workspace settings.
Features:
- Profile editing (name, phone)
- SMS sender name configuration with AI verification
- API key management
- Webhook configuration
- Password change
- Account deletion
Sender Name Verification:
const verifySenderName = async (name: string) => {
const res = await api.post('/account/verify-sender', { sender_name: name })
if (!res.data.approved) {
toast({
title: 'Sender name not approved',
description: res.data.reason,
variant: 'destructive'
})
return false
}
return res.data.sanitized
}Onboarding (Onboarding.tsx)
New user onboarding flow.
Steps:
- Company information (name, industry)
- Contact details (phone, country)
- Use cases selection
- Monthly volume estimate
- SMS sender name setup
Login / Signup
Authentication pages with validation.
function Login() {
const { login } = useAuth()
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const [error, setError] = useState('')
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
try {
await login(email, password)
navigate('/dashboard')
} catch (err: any) {
setError(err.response?.data?.error || 'Login failed')
}
}
// ...render form
}UI Components
Layout
Page wrapper with sidebar navigation:
export function Layout({ children }: { children: React.ReactNode }) {
return (
<div className="min-h-screen bg-gray-50">
<Sidebar />
<main className="lg:pl-64 pt-16 lg:pt-0">
<div className="max-w-7xl mx-auto px-4 py-6">
{children}
</div>
</main>
</div>
)
}Protected Route
Auth guard component:
export function ProtectedRoute({ children }: { children: React.ReactNode }) {
const { user, isLoading } = useAuth()
const location = useLocation()
if (isLoading) {
return <LoadingSpinner />
}
if (!user) {
return <Navigate to="/login" state={{ from: location }} replace />
}
// Check onboarding
if (!user.onboarding_completed && location.pathname !== '/onboarding') {
return <Navigate to="/onboarding" replace />
}
return <>{children}</>
}Toast Notifications
export function useToast() {
const [toasts, setToasts] = useState<Toast[]>([])
const toast = ({ title, description, variant = 'default' }) => {
const id = Date.now()
setToasts(prev => [...prev, { id, title, description, variant }])
setTimeout(() => {
setToasts(prev => prev.filter(t => t.id !== id))
}, 5000)
}
return { toast, toasts }
}State Management
React Query Patterns
Fetching with caching:
const { data, isLoading, error, refetch } = useQuery({
queryKey: ['messages', filters],
queryFn: () => api.get('/messages', { params: filters }),
staleTime: 30000, // 30 seconds
})Mutations with optimistic updates:
const mutation = useMutation({
mutationFn: (data) => api.post('/messages/send', data),
onMutate: async (newMessage) => {
// Optimistic update
await queryClient.cancelQueries({ queryKey: ['messages'] })
const previous = queryClient.getQueryData(['messages'])
queryClient.setQueryData(['messages'], old => [...old, newMessage])
return { previous }
},
onError: (err, newMessage, context) => {
// Rollback on error
queryClient.setQueryData(['messages'], context?.previous)
},
onSettled: () => {
// Refetch to sync with server
queryClient.invalidateQueries({ queryKey: ['messages'] })
},
})Styling
TailwindCSS with custom design system:
// tailwind.config.js
module.exports = {
content: ['./src/**/*.{ts,tsx}'],
theme: {
extend: {
colors: {
primary: {
50: '#f5f3ff',
500: '#8b5cf6',
600: '#7c3aed',
700: '#6d28d9',
},
},
},
},
plugins: [],
}Common patterns:
// Card component
<div className="bg-white rounded-2xl border border-gray-200 p-6 shadow-sm">
{children}
</div>
// Button component
<button className="px-4 py-2 bg-purple-600 text-white rounded-xl
font-semibold hover:bg-purple-700 transition-colors">
Send
</button>
// Input component
<input className="w-full px-4 py-3 rounded-xl border border-gray-200
bg-gray-50 focus:border-purple-500 focus:ring-2
focus:ring-purple-200 outline-none transition-all" />Environment Variables
# .env.production
VITE_API_URL=https://api.yebolink.com/api/v1
VITE_STRIPE_PUBLISHABLE_KEY=pk_live_...Build & Deploy
# Development
npm run dev
# Production build
npm run build
# Deploy to Cloudflare Pages
npx wrangler pages deploy dist --project-name=yebolink-dashboard