Initial commit: HotWives Platform completa
- Backend completo com Express, TypeScript e Prisma - Sistema de autenticação JWT - API REST com todas as funcionalidades - Sistema de mensagens e chat em tempo real (Socket.io) - Upload e gerenciamento de fotos - Sistema de perfis com verificação - Busca avançada com filtros - Sistema de eventos - Dashboard administrativo - Frontend Next.js 14 com TypeScript - Design moderno com Tailwind CSS - Componentes UI com Radix UI - Tema dark/light - Configuração Nginx pronta para produção - Scripts de instalação e deploy - Documentação completa
This commit is contained in:
118
frontend/lib/api.ts
Normal file
118
frontend/lib/api.ts
Normal file
@@ -0,0 +1,118 @@
|
||||
import axios from 'axios'
|
||||
|
||||
const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3001/api'
|
||||
|
||||
const api = axios.create({
|
||||
baseURL: API_URL,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
|
||||
// Interceptor para adicionar token
|
||||
api.interceptors.request.use((config) => {
|
||||
if (typeof window !== 'undefined') {
|
||||
const token = localStorage.getItem('token')
|
||||
if (token) {
|
||||
config.headers.Authorization = `Bearer ${token}`
|
||||
}
|
||||
}
|
||||
return config
|
||||
})
|
||||
|
||||
// Interceptor para tratar erros
|
||||
api.interceptors.response.use(
|
||||
(response) => response,
|
||||
(error) => {
|
||||
if (error.response?.status === 401) {
|
||||
if (typeof window !== 'undefined') {
|
||||
localStorage.removeItem('token')
|
||||
window.location.href = '/login'
|
||||
}
|
||||
}
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
|
||||
export default api
|
||||
|
||||
// Auth endpoints
|
||||
export const authAPI = {
|
||||
register: (data: any) => api.post('/auth/register', data),
|
||||
login: (data: any) => api.post('/auth/login', data),
|
||||
forgotPassword: (data: any) => api.post('/auth/forgot-password', data),
|
||||
resetPassword: (data: any) => api.post('/auth/reset-password', data),
|
||||
}
|
||||
|
||||
// User endpoints
|
||||
export const userAPI = {
|
||||
getMe: () => api.get('/users/me'),
|
||||
updateMe: (data: any) => api.put('/users/me', data),
|
||||
getFavorites: () => api.get('/users/favorites'),
|
||||
addFavorite: (userId: string) => api.post(`/users/favorites/${userId}`),
|
||||
removeFavorite: (userId: string) => api.delete(`/users/favorites/${userId}`),
|
||||
blockUser: (userId: string, data: any) => api.post(`/users/blocks/${userId}`, data),
|
||||
unblockUser: (userId: string) => api.delete(`/users/blocks/${userId}`),
|
||||
}
|
||||
|
||||
// Profile endpoints
|
||||
export const profileAPI = {
|
||||
getProfile: (username: string) => api.get(`/profiles/${username}`),
|
||||
updateProfile: (data: any) => api.put('/profiles/me', data),
|
||||
uploadAvatar: (file: File) => {
|
||||
const formData = new FormData()
|
||||
formData.append('photo', file)
|
||||
return api.post('/profiles/me/avatar', formData, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' },
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
// Search endpoints
|
||||
export const searchAPI = {
|
||||
searchProfiles: (params: any) => api.get('/search/profiles', { params }),
|
||||
searchEvents: (params: any) => api.get('/search/events', { params }),
|
||||
}
|
||||
|
||||
// Message endpoints
|
||||
export const messageAPI = {
|
||||
getConversations: () => api.get('/messages/conversations'),
|
||||
getConversation: (userId: string) => api.get(`/messages/conversation/${userId}`),
|
||||
sendMessage: (data: any) => api.post('/messages/send', data),
|
||||
}
|
||||
|
||||
// Event endpoints
|
||||
export const eventAPI = {
|
||||
getEvents: (params?: any) => api.get('/events', { params }),
|
||||
getEvent: (id: string) => api.get(`/events/${id}`),
|
||||
createEvent: (data: any) => {
|
||||
const formData = new FormData()
|
||||
Object.keys(data).forEach(key => {
|
||||
if (data[key] !== null && data[key] !== undefined) {
|
||||
formData.append(key, data[key])
|
||||
}
|
||||
})
|
||||
return api.post('/events', formData, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' },
|
||||
})
|
||||
},
|
||||
joinEvent: (id: string, data?: any) => api.post(`/events/${id}/join`, data),
|
||||
leaveEvent: (id: string) => api.post(`/events/${id}/leave`),
|
||||
}
|
||||
|
||||
// Photo endpoints
|
||||
export const photoAPI = {
|
||||
getPhotos: (userId?: string) => api.get('/photos', { params: { userId } }),
|
||||
uploadPhoto: (file: File, data: any) => {
|
||||
const formData = new FormData()
|
||||
formData.append('photo', file)
|
||||
Object.keys(data).forEach(key => {
|
||||
formData.append(key, data[key])
|
||||
})
|
||||
return api.post('/photos', formData, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' },
|
||||
})
|
||||
},
|
||||
deletePhoto: (id: string) => api.delete(`/photos/${id}`),
|
||||
}
|
||||
|
||||
45
frontend/lib/socket.ts
Normal file
45
frontend/lib/socket.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { io, Socket } from 'socket.io-client'
|
||||
|
||||
const SOCKET_URL = process.env.NEXT_PUBLIC_SOCKET_URL || 'http://localhost:3001'
|
||||
|
||||
let socket: Socket | null = null
|
||||
|
||||
export const connectSocket = (token: string) => {
|
||||
if (!socket) {
|
||||
socket = io(SOCKET_URL, {
|
||||
auth: {
|
||||
token,
|
||||
},
|
||||
})
|
||||
|
||||
socket.on('connect', () => {
|
||||
console.log('Socket connected')
|
||||
})
|
||||
|
||||
socket.on('disconnect', () => {
|
||||
console.log('Socket disconnected')
|
||||
})
|
||||
|
||||
socket.on('connect_error', (error) => {
|
||||
console.error('Socket connection error:', error)
|
||||
})
|
||||
}
|
||||
|
||||
return socket
|
||||
}
|
||||
|
||||
export const disconnectSocket = () => {
|
||||
if (socket) {
|
||||
socket.disconnect()
|
||||
socket = null
|
||||
}
|
||||
}
|
||||
|
||||
export const getSocket = () => socket
|
||||
|
||||
export default {
|
||||
connect: connectSocket,
|
||||
disconnect: disconnectSocket,
|
||||
get: getSocket,
|
||||
}
|
||||
|
||||
55
frontend/lib/utils.ts
Normal file
55
frontend/lib/utils.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import { type ClassValue, clsx } from "clsx"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
||||
|
||||
export function formatDate(date: Date | string): string {
|
||||
const d = new Date(date)
|
||||
return new Intl.DateTimeFormat('pt-BR', {
|
||||
day: '2-digit',
|
||||
month: '2-digit',
|
||||
year: 'numeric',
|
||||
}).format(d)
|
||||
}
|
||||
|
||||
export function formatDateTime(date: Date | string): string {
|
||||
const d = new Date(date)
|
||||
return new Intl.DateTimeFormat('pt-BR', {
|
||||
day: '2-digit',
|
||||
month: '2-digit',
|
||||
year: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
}).format(d)
|
||||
}
|
||||
|
||||
export function formatRelativeTime(date: Date | string): string {
|
||||
const d = new Date(date)
|
||||
const now = new Date()
|
||||
const diff = now.getTime() - d.getTime()
|
||||
|
||||
const seconds = Math.floor(diff / 1000)
|
||||
const minutes = Math.floor(seconds / 60)
|
||||
const hours = Math.floor(minutes / 60)
|
||||
const days = Math.floor(hours / 24)
|
||||
|
||||
if (days > 7) {
|
||||
return formatDate(d)
|
||||
} else if (days > 0) {
|
||||
return `${days}d atrás`
|
||||
} else if (hours > 0) {
|
||||
return `${hours}h atrás`
|
||||
} else if (minutes > 0) {
|
||||
return `${minutes}min atrás`
|
||||
} else {
|
||||
return 'agora'
|
||||
}
|
||||
}
|
||||
|
||||
export function truncate(str: string, length: number): string {
|
||||
if (str.length <= length) return str
|
||||
return str.slice(0, length) + '...'
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user