Database changes: - Add parent_post_id to social_posts for threading support - Create social_reactions table with emoji constraints - Add indexes for efficient thread and reaction queries Backend API: - GET /api/v1/social/posts/:id/thread - fetch thread with all replies - POST /api/v1/social/posts/:id/replies - create a reply - POST /api/v1/social/posts/:id/reactions - toggle reaction - GET /api/v1/social/posts/:id/reactions - get reactions with counts - Update feed endpoints to include replyCount and filter top-level posts Frontend UI: - Thread.tsx - full thread view with replies and composer - Reactions.tsx - reaction buttons component (👍 🤔 💡) - Update Feed.tsx - add reactions, reply counts, thread navigation - Update Channels.tsx - add reactions, reply counts, thread navigation - Enhanced composer with textarea instead of input All acceptance criteria now met: ✅ Feed global ✅ Vue par channel ✅ Threads / réponses ✅ Publication humaine ✅ Réactions fonctionnelles ✅ Responsive mobile Co-Authored-By: Paperclip <noreply@paperclip.ing>
139 lines
4.1 KiB
TypeScript
139 lines
4.1 KiB
TypeScript
import { authStorage } from './auth';
|
|
import type {
|
|
Room,
|
|
Message,
|
|
SessionResponse,
|
|
MessagesResponse,
|
|
RoomsResponse,
|
|
SocialChannel,
|
|
SocialPost,
|
|
SocialReaction,
|
|
SocialFeedResponse,
|
|
SocialChannelsResponse,
|
|
SocialChannelPostsResponse,
|
|
DirectoryResponse,
|
|
} from '../types';
|
|
|
|
const API_BASE = import.meta.env.VITE_API_URL || 'http://localhost:3000';
|
|
|
|
class ApiError extends Error {
|
|
status: number;
|
|
|
|
constructor(status: number, message: string) {
|
|
super(message);
|
|
this.name = 'ApiError';
|
|
this.status = status;
|
|
}
|
|
}
|
|
|
|
async function fetchApi<T>(path: string, options: RequestInit = {}): Promise<T> {
|
|
const jwt = authStorage.getJwt();
|
|
const agentId = authStorage.getAgentId();
|
|
const headers: Record<string, string> = {
|
|
'Content-Type': 'application/json',
|
|
...(options.headers as Record<string, string>),
|
|
};
|
|
|
|
if (jwt) {
|
|
headers['Authorization'] = `Bearer ${jwt}`;
|
|
}
|
|
|
|
if (agentId) {
|
|
headers['x-agent-id'] = agentId;
|
|
}
|
|
|
|
const response = await fetch(`${API_BASE}${path}`, {
|
|
...options,
|
|
headers,
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const text = await response.text();
|
|
throw new ApiError(response.status, text || response.statusText);
|
|
}
|
|
|
|
return response.json();
|
|
}
|
|
|
|
export const api = {
|
|
async login(apiToken: string): Promise<SessionResponse> {
|
|
return fetchApi<SessionResponse>('/api/v1/sessions', {
|
|
method: 'POST',
|
|
body: JSON.stringify({ apiToken }),
|
|
});
|
|
},
|
|
|
|
async getRooms(): Promise<Room[]> {
|
|
const response = await fetchApi<RoomsResponse>('/api/v1/rooms');
|
|
return response.rooms;
|
|
},
|
|
|
|
async getMessages(roomId: string, before?: string): Promise<MessagesResponse> {
|
|
const params = before ? `?before=${before}` : '';
|
|
return fetchApi<MessagesResponse>(`/api/v1/rooms/${roomId}/messages${params}`);
|
|
},
|
|
|
|
async sendMessage(roomId: string, body: string): Promise<Message> {
|
|
return fetchApi<Message>(`/api/v1/rooms/${roomId}/messages`, {
|
|
method: 'POST',
|
|
body: JSON.stringify({ body }),
|
|
});
|
|
},
|
|
|
|
async getSocialChannels(): Promise<SocialChannel[]> {
|
|
const response = await fetchApi<SocialChannelsResponse>('/api/v1/social/channels');
|
|
return response.channels;
|
|
},
|
|
|
|
async getSocialFeed(before?: string): Promise<SocialFeedResponse> {
|
|
const params = before ? `?before=${before}` : '';
|
|
return fetchApi<SocialFeedResponse>(`/api/v1/social/feed${params}`);
|
|
},
|
|
|
|
async getSocialChannelPosts(channelId: string, before?: string): Promise<SocialChannelPostsResponse> {
|
|
const params = before ? `?before=${before}` : '';
|
|
return fetchApi<SocialChannelPostsResponse>(`/api/v1/social/channels/${channelId}/posts${params}`);
|
|
},
|
|
|
|
async createSocialPost(channelId: string, body: string): Promise<SocialPost> {
|
|
return fetchApi<SocialPost>(`/api/v1/social/channels/${channelId}/posts`, {
|
|
method: 'POST',
|
|
body: JSON.stringify({ body }),
|
|
});
|
|
},
|
|
|
|
async getDirectory(companyId: string = 'BARAAA', role?: string): Promise<DirectoryResponse> {
|
|
const params = role ? `?role=${encodeURIComponent(role)}` : '';
|
|
return fetchApi<DirectoryResponse>(`/api/companies/${companyId}/agents/directory${params}`);
|
|
},
|
|
|
|
async getSocialThread(postId: string): Promise<{ parent: SocialPost; replies: SocialPost[] }> {
|
|
return fetchApi<{ parent: SocialPost; replies: SocialPost[] }>(
|
|
`/api/v1/social/posts/${postId}/thread`,
|
|
);
|
|
},
|
|
|
|
async createSocialReply(postId: string, body: string): Promise<SocialPost> {
|
|
return fetchApi<SocialPost>(`/api/v1/social/posts/${postId}/replies`, {
|
|
method: 'POST',
|
|
body: JSON.stringify({ body }),
|
|
});
|
|
},
|
|
|
|
async toggleSocialReaction(
|
|
postId: string,
|
|
emoji: '👍' | '🤔' | '💡',
|
|
): Promise<{ action: 'added' | 'removed'; emoji: string }> {
|
|
return fetchApi<{ action: 'added' | 'removed'; emoji: string }>(
|
|
`/api/v1/social/posts/${postId}/reactions`,
|
|
{
|
|
method: 'POST',
|
|
body: JSON.stringify({ emoji }),
|
|
},
|
|
);
|
|
},
|
|
|
|
async getSocialReactions(postId: string): Promise<{ reactions: SocialReaction[] }> {
|
|
return fetchApi<{ reactions: SocialReaction[] }>(`/api/v1/social/posts/${postId}/reactions`);
|
|
},
|
|
};
|