Skip to content

Frontend

O CSGOFlip possui dois frontends: o site principal para jogadores e o painel administrativo.

Projetos

ProjetoDiretórioStack
Site Principal/clientNext.js 14, Tailwind, Framer Motion
Admin Dashboard/next-shadcn-admin-dashboardNext.js 16, shadcn/ui, React Query

Site Principal (Client)

Stack

TecnologiaUso
Next.js 14Framework React com App Router
Tailwind CSSEstilos utilitários
Framer MotionAnimações
Socket.io ClientWebSocket
React QueryData fetching
ZustandState 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

Admin Dashboard

O painel administrativo é um projeto separado em /next-shadcn-admin-dashboard.

Stack

TecnologiaUso
Next.js 16Framework
shadcn/uiComponentes UI
React QueryData fetching
ZustandState management
TanStack TableTabelas de dados
RechartsGrá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

Documentação Técnica CSGOFlip