Frontend
O CSGOFlip possui dois frontends: o site principal para jogadores e o painel administrativo.
Projetos
| Projeto | Diretório | Stack |
|---|---|---|
| Site Principal | /client | Next.js 14, Tailwind, Framer Motion |
| Admin Dashboard | /next-shadcn-admin-dashboard | Next.js 16, shadcn/ui, React Query |
Site Principal (Client)
Stack
| Tecnologia | Uso |
|---|---|
| Next.js 14 | Framework React com App Router |
| Tailwind CSS | Estilos utilitários |
| Framer Motion | Animações |
| Socket.io Client | WebSocket |
| React Query | Data fetching |
| Zustand | State management |
Estrutura
client/
├── app/
│ ├── (auth)/
│ │ └── login/
│ ├── (main)/
│ │ ├── layout.tsx
│ │ ├── page.tsx # Home
│ │ ├── cases/
│ │ ├── case/[id]/
│ │ ├── battles/
│ │ ├── batalha/[id]/
│ │ ├── upgrades/
│ │ ├── raffles/
│ │ ├── inventory/
│ │ ├── deposit/
│ │ ├── withdraw/
│ │ └── profile/
│ ├── api/
│ └── globals.css
├── components/
│ ├── ui/ # Componentes base
│ ├── layout/ # Header, Footer, Sidebar
│ ├── cases/ # Componentes de caixas
│ ├── battles/ # Componentes de batalhas
│ └── shared/ # Componentes compartilhados
├── contexts/
│ ├── AuthContext.tsx
│ ├── WebSocketContext.tsx
│ ├── LiveDropsContext.tsx
│ └── BalanceContext.tsx
├── hooks/
│ ├── useAuth.ts
│ ├── useBalance.ts
│ ├── useWebSocket.ts
│ └── useBattleSocket.ts
├── lib/
│ ├── api.ts
│ ├── utils.ts
│ └── constants.ts
└── public/
├── images/
└── sounds/Contexts
AuthContext
tsx
// contexts/AuthContext.tsx
interface AuthContextValue {
user: User | null;
isLoading: boolean;
isAuthenticated: boolean;
login: () => void;
logout: () => Promise<void>;
refreshUser: () => Promise<void>;
}
export function AuthProvider({ children }: { children: React.ReactNode }) {
const [user, setUser] = useState<User | null>(null);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
fetchCurrentUser();
}, []);
const fetchCurrentUser = async () => {
try {
const response = await api.get('/auth/me');
setUser(response.data);
} catch {
setUser(null);
} finally {
setIsLoading(false);
}
};
const login = () => {
window.location.href = `${API_URL}/auth/steam/login`;
};
const logout = async () => {
await api.post('/auth/logout');
setUser(null);
};
return (
<AuthContext.Provider
value={{
user,
isLoading,
isAuthenticated: !!user,
login,
logout,
refreshUser: fetchCurrentUser,
}}
>
{children}
</AuthContext.Provider>
);
}WebSocketContext
tsx
// contexts/WebSocketContext.tsx
interface WebSocketContextValue {
socket: Socket | null;
isConnected: boolean;
balance: number;
}
export function WebSocketProvider({ children }: { children: React.ReactNode }) {
const [socket, setSocket] = useState<Socket | null>(null);
const [isConnected, setIsConnected] = useState(false);
const [balance, setBalance] = useState(0);
useEffect(() => {
const socketInstance = io(WS_URL, {
withCredentials: true,
transports: ['websocket', 'polling'],
});
socketInstance.on('connect', () => setIsConnected(true));
socketInstance.on('disconnect', () => setIsConnected(false));
socketInstance.on('balance:updated', (data) => setBalance(data.balance));
setSocket(socketInstance);
return () => {
socketInstance.disconnect();
};
}, []);
return (
<WebSocketContext.Provider value={{ socket, isConnected, balance }}>
{children}
</WebSocketContext.Provider>
);
}Componentes Principais
Roulette
tsx
// components/cases/Roulette.tsx
interface RouletteProps {
items: CaseItem[];
result: OpenCaseResult;
onComplete: () => void;
}
export function Roulette({ items, result, onComplete }: RouletteProps) {
const [offset, setOffset] = useState(0);
const [isSpinning, setIsSpinning] = useState(false);
const startSpin = () => {
setIsSpinning(true);
// Calcular offset final baseado no roll
const finalOffset = calculateOffset(items, result.roll);
setTimeout(() => {
setOffset(finalOffset);
}, 100);
setTimeout(() => {
setIsSpinning(false);
onComplete();
}, 10000);
};
return (
<div className="roulette-container overflow-hidden relative">
{/* Indicador central */}
<div className="absolute left-1/2 top-0 bottom-0 w-0.5 bg-primary z-10" />
{/* Track */}
<motion.div
className="roulette-track flex"
animate={{ x: -offset }}
transition={{
duration: isSpinning ? 10 : 0,
ease: [0.1, 0.8, 0.2, 1],
}}
>
{[...Array(5)].flatMap((_, i) =>
items.map((item, j) => (
<RouletteCard key={`${i}-${j}`} item={item} />
))
)}
</motion.div>
</div>
);
}BattleArena
tsx
// components/battles/BattleArena.tsx
interface BattleArenaProps {
battle: Battle;
}
export function BattleArena({ battle }: BattleArenaProps) {
const [currentRound, setCurrentRound] = useState(1);
const [results, setResults] = useState<RoundResult[]>([]);
useBattleSocket({
battleId: battle.id,
onRound: (data) => {
setCurrentRound(data.roundNumber);
setResults(prev => [...prev, ...data.results]);
},
onFinished: (data) => {
// Mostrar resultado
},
});
return (
<div className="battle-arena grid grid-cols-2 gap-8">
{/* Time 1 */}
<div className="team-1">
{battle.participants
.filter(p => p.team === 1)
.map(participant => (
<ParticipantSlot
key={participant.id}
participant={participant}
result={results.find(r => r.participantId === participant.id)}
/>
))}
</div>
{/* VS + Placar */}
<div className="versus flex items-center justify-center">
<Scoreboard battle={battle} currentRound={currentRound} />
</div>
{/* Time 2 */}
<div className="team-2">
{battle.participants
.filter(p => p.team === 2)
.map(participant => (
<ParticipantSlot
key={participant.id}
participant={participant}
result={results.find(r => r.participantId === participant.id)}
/>
))}
</div>
</div>
);
}LiveDropsFeed
tsx
// components/shared/LiveDropsFeed.tsx
export function LiveDropsFeed() {
const { drops } = useLiveDrops();
return (
<div className="live-drops-feed">
<h3 className="flex items-center gap-2 mb-4">
<span className="w-2 h-2 bg-green-500 rounded-full animate-pulse" />
Live Drops
</h3>
<div className="space-y-2 max-h-96 overflow-y-auto">
<AnimatePresence mode="popLayout">
{drops.map(drop => (
<motion.div
key={drop.id}
layout
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0 }}
className="drop-card"
>
<DropCard drop={drop} />
</motion.div>
))}
</AnimatePresence>
</div>
</div>
);
}Hooks
useBalance
tsx
// hooks/useBalance.ts
export function useBalance() {
const { balance } = useWebSocket();
const { user } = useAuth();
// Saldo inicial do usuário, atualizado via WebSocket
const [localBalance, setLocalBalance] = useState(user?.balanceCents || 0);
useEffect(() => {
if (balance !== undefined) {
setLocalBalance(balance * 100); // Converter para centavos
}
}, [balance]);
const formattedBalance = useMemo(() => {
return (localBalance / 100).toLocaleString('pt-BR', {
style: 'currency',
currency: 'BRL',
});
}, [localBalance]);
return {
balanceCents: localBalance,
formattedBalance,
};
}useBattleSocket
tsx
// hooks/useBattleSocket.ts
interface UseBattleSocketOptions {
battleId: string;
onRound?: (data: RoundData) => void;
onFinished?: (data: FinishedData) => void;
}
export function useBattleSocket({
battleId,
onRound,
onFinished,
}: UseBattleSocketOptions) {
const { socket } = useWebSocket();
useEffect(() => {
if (!socket) return;
socket.emit('join:battle', { battleId });
const handleRound = (data: RoundData) => {
if (data.battleId === battleId && onRound) {
onRound(data);
}
};
const handleFinished = (data: FinishedData) => {
if (data.battleId === battleId && onFinished) {
onFinished(data);
}
};
socket.on('battle:round', handleRound);
socket.on('battle:finished', handleFinished);
return () => {
socket.emit('leave:battle', { battleId });
socket.off('battle:round', handleRound);
socket.off('battle:finished', handleFinished);
};
}, [socket, battleId, onRound, onFinished]);
}Seções
- Componentes - Biblioteca de componentes
- Estado - Gerenciamento de estado
- Animações - Framer Motion
- API Client - Configuração do cliente HTTP
Admin Dashboard
O painel administrativo é um projeto separado em /next-shadcn-admin-dashboard.
Stack
| Tecnologia | Uso |
|---|---|
| Next.js 16 | Framework |
| shadcn/ui | Componentes UI |
| React Query | Data fetching |
| Zustand | State management |
| TanStack Table | Tabelas de dados |
| Recharts | Gráficos |
Características
- Dashboard com métricas em tempo real
- Gestão de usuários (ban/unban)
- Gestão de caixas e itens (CRUD)
- Fila de aprovação de saques
- Histórico de transações
- Logs de auditoria
- Relatórios exportáveis
