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: 2 additions & 0 deletions src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { Metadata } from "next";
import { Manrope } from "next/font/google";
import "./globals.css";
import { Providers } from "@/components/providers/Providers";
import { RateLimitBanner } from "@/components/ui/RateLimitBanner";

const manrope = Manrope({
subsets: ["latin"],
Expand All @@ -24,6 +25,7 @@ export default function RootLayout({
<html lang="en" className={manrope.variable}>
<body className="font-sans antialiased bg-gray-50 dark:bg-gray-900">
<Providers>
<RateLimitBanner />
{children}
</Providers>
</body>
Expand Down
20 changes: 20 additions & 0 deletions src/components/providers/Providers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { Provider } from 'react-redux';
import { Toaster } from 'react-hot-toast';
import { store } from '@/store';
import { initializeAuth } from '@/store/slices/authSlice';
import { initializeUI, setRateLimited } from '@/store/slices/uiSlice';
import { initializeUI } from '@/store/slices/uiSlice';
import { SocketProvider } from './SocketProvider';
import { TransactionToastListener } from '@/components/TransactionToastListener';
Expand All @@ -19,6 +20,25 @@ export function Providers({ children }: ProvidersProps) {
store.dispatch(initializeAuth());
// Initialize UI state (theme, etc.)
store.dispatch(initializeUI());

// Global API event listeners
const handleRateLimit = () => {
store.dispatch(setRateLimited(true));
};

const handleSuccess = () => {
// Auto-clear rate limit on successful request?
// Or maybe let the user dismiss it.
// For now, let's keep it until manual dismissal or timeout.
};

window.addEventListener('api-rate-limit' as any, handleRateLimit);
window.addEventListener('api-success' as any, handleSuccess);

return () => {
window.removeEventListener('api-rate-limit' as any, handleRateLimit);
window.removeEventListener('api-success' as any, handleSuccess);
};
}, []);

return (
Expand Down
56 changes: 56 additions & 0 deletions src/components/ui/RateLimitBanner.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
'use client';

import React from 'react';
import { useAppDispatch, useAppSelector } from '@/store';
import { setRateLimited } from '@/store/slices/uiSlice';
import { AlertCircle, X, Clock } from 'lucide-react';
import { motion, AnimatePresence } from 'framer-motion';

export const RateLimitBanner: React.FC = () => {
const dispatch = useAppDispatch();
const isRateLimited = useAppSelector((state) => state.ui.isRateLimited);

const handleDismiss = () => {
dispatch(setRateLimited(false));
};

return (
<AnimatePresence>
{isRateLimited && (
<motion.div
initial={{ height: 0, opacity: 0 }}
animate={{ height: 'auto', opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
transition={{ duration: 0.3, ease: 'easeInOut' }}
className="relative z-[100] w-full overflow-hidden"
>
<div className="bg-amber-500/10 border-b border-amber-500/20 px-4 py-3 backdrop-blur-md">
<div className="max-w-7xl mx-auto flex items-center justify-between gap-4">
<div className="flex items-center gap-3">
<div className="flex-shrink-0 w-8 h-8 rounded-full bg-amber-500/20 flex items-center justify-center text-amber-500">
<Clock size={18} className="animate-pulse" />
</div>
<div>
<h3 className="text-sm font-semibold text-amber-200">System Rate Limit Active</h3>
<p className="text-xs text-amber-200/70">
We've received too many requests from your IP. Please wait a moment before trying again.
</p>
</div>
</div>

<button
onClick={handleDismiss}
className="flex-shrink-0 p-1 rounded-lg hover:bg-amber-500/20 text-amber-500/60 hover:text-amber-500 transition-colors"
aria-label="Dismiss"
>
<X size={18} />
</button>
</div>
</div>
</motion.div>
)}
</AnimatePresence>
);
};

export default RateLimitBanner;
18 changes: 18 additions & 0 deletions src/services/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,15 @@ class ApiService {

// Response interceptor for automatic token refresh
this.api.interceptors.response.use(
(response) => {
// Successful response - potentially clear rate limit if it was set
if (typeof window !== 'undefined') {
window.dispatchEvent(new CustomEvent('api-success'));
}
return response;
},
(error: any) => {
if (error.response?.status === 401) {
(response) => response,
async (error) => {
const originalRequest = error.config;
Expand Down Expand Up @@ -91,6 +100,15 @@ class ApiService {
if (typeof window !== 'undefined') {
window.location.href = '/auth/login';
}
} else if (error.response?.status === 429) {
// Rate limited
if (typeof window !== 'undefined') {
window.dispatchEvent(new CustomEvent('api-rate-limit', {
detail: {
message: error.response?.data?.message || 'You are being rate limited. Please slow down.'
}
}));
}

return Promise.reject(refreshError);
}
Expand Down
11 changes: 8 additions & 3 deletions src/store/slices/uiSlice.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { createSlice, PayloadAction, createAction } from '@reduxjs/toolkit';

export interface ThemeState {
export interface UIState {
mode: 'light' | 'dark';
isRateLimited: boolean;
}

const initialState: ThemeState = {
const initialState: UIState = {
mode: 'dark', // Default to dark mode
isRateLimited: false,
};

// Create action for initialization
Expand Down Expand Up @@ -38,6 +40,9 @@ export const uiSlice = createSlice({
localStorage.setItem('theme', state.mode);
}
},
setRateLimited: (state, action: PayloadAction<boolean>) => {
state.isRateLimited = action.payload;
},
},
extraReducers: (builder) => {
builder.addCase(initializeUI, (state, action) => {
Expand All @@ -46,5 +51,5 @@ export const uiSlice = createSlice({
},
});

export const { toggleTheme, setTheme } = uiSlice.actions;
export const { toggleTheme, setTheme, setRateLimited } = uiSlice.actions;
export default uiSlice.reducer;