api.ts 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261
  1. const API_BASE = process.env.NEXT_PUBLIC_API_URL || '';
  2. interface FetchOptions extends RequestInit {
  3. token?: string;
  4. }
  5. async function apiFetch<T = unknown>(
  6. endpoint: string,
  7. options: FetchOptions = {}
  8. ): Promise<T> {
  9. const { token, ...fetchOptions } = options;
  10. const headers: Record<string, string> = {
  11. 'Content-Type': 'application/json',
  12. ...(options.headers as Record<string, string> || {}),
  13. };
  14. if (token) {
  15. headers['Authorization'] = `Bearer ${token}`;
  16. }
  17. const res = await fetch(`${API_BASE}${endpoint}`, {
  18. ...fetchOptions,
  19. headers,
  20. credentials: 'include',
  21. });
  22. if (!res.ok) {
  23. const error = await res.json().catch(() => ({ error: res.statusText }));
  24. throw new Error(error.error || `HTTP ${res.status}`);
  25. }
  26. return res.json();
  27. }
  28. // ── Auth ─────────────────────────────────────────────────────────────────────
  29. export const authApi = {
  30. register: (data: { email: string; name: string; password: string }) =>
  31. apiFetch<{ user: User; token: string }>('/api/auth/register', {
  32. method: 'POST',
  33. body: JSON.stringify(data),
  34. }),
  35. login: (data: { email: string; password: string }) =>
  36. apiFetch<{ user: User; token: string }>('/api/auth/login', {
  37. method: 'POST',
  38. body: JSON.stringify(data),
  39. }),
  40. logout: () =>
  41. apiFetch('/api/auth/logout', { method: 'POST' }),
  42. me: (token: string) =>
  43. apiFetch<{ user: User }>('/api/auth/me', { token }),
  44. };
  45. // ── Projects ─────────────────────────────────────────────────────────────────
  46. export const projectsApi = {
  47. list: (token: string) =>
  48. apiFetch<{ projects: Project[] }>('/api/projects', { token }),
  49. create: (token: string, data: { name: string; description?: string }) =>
  50. apiFetch<{ project: Project }>('/api/projects', {
  51. method: 'POST',
  52. body: JSON.stringify(data),
  53. token,
  54. }),
  55. get: (token: string, id: string) =>
  56. apiFetch<{ project: Project }>(`/api/projects/${id}`, { token }),
  57. update: (token: string, id: string, data: { name?: string; description?: string }) =>
  58. apiFetch<{ project: Project }>(`/api/projects/${id}`, {
  59. method: 'PUT',
  60. body: JSON.stringify(data),
  61. token,
  62. }),
  63. delete: (token: string, id: string) =>
  64. apiFetch(`/api/projects/${id}`, { method: 'DELETE', token }),
  65. inviteMember: (token: string, projectId: string, email: string, role: string) =>
  66. apiFetch(`/api/projects/${projectId}/members`, {
  67. method: 'POST',
  68. body: JSON.stringify({ email, role }),
  69. token,
  70. }),
  71. };
  72. // ── Assets ───────────────────────────────────────────────────────────────────
  73. export const assetsApi = {
  74. list: (token: string, projectId: string) =>
  75. apiFetch<{ assets: Asset[] }>(`/api/assets?projectId=${projectId}`, { token }),
  76. get: (token: string, id: string) =>
  77. apiFetch<{ asset: AssetWithComments }>(`/api/assets/${id}`, { token }),
  78. upload: (token: string, formData: FormData) =>
  79. fetch(`${API_BASE}/api/assets/upload`, {
  80. method: 'POST',
  81. headers: { Authorization: `Bearer ${token}` },
  82. body: formData,
  83. }).then(r => r.json()),
  84. updateStatus: (token: string, id: string, status: string) =>
  85. apiFetch<{ asset: Asset }>(`/api/assets/${id}/status`, {
  86. method: 'PUT',
  87. body: JSON.stringify({ status }),
  88. token,
  89. }),
  90. delete: (token: string, id: string) =>
  91. apiFetch(`/api/assets/${id}`, { method: 'DELETE', token }),
  92. };
  93. // ── Comments ─────────────────────────────────────────────────────────────────
  94. export const commentsApi = {
  95. list: (token: string, assetId: string, resolved?: boolean) => {
  96. const q = resolved !== undefined ? `?resolved=${resolved}` : '';
  97. return apiFetch<{ comments: Comment[] }>(`/api/assets/${assetId}/comments${q}`, { token });
  98. },
  99. create: (token: string, assetId: string, data: {
  100. content: string;
  101. timestamp?: number;
  102. annotations?: AnnotationData[];
  103. parentId?: string;
  104. }) =>
  105. apiFetch<{ comment: Comment }>(`/api/assets/${assetId}/comments`, {
  106. method: 'POST',
  107. body: JSON.stringify(data),
  108. token,
  109. }),
  110. resolve: (token: string, id: string) =>
  111. apiFetch<{ comment: Comment }>(`/api/comments/${id}/resolve`, { method: 'PUT', token }),
  112. updateAnnotations: (token: string, id: string, annotations: AnnotationData[]) =>
  113. apiFetch<{ comment: Comment }>(`/api/comments/${id}/annotations`, {
  114. method: 'PUT',
  115. body: JSON.stringify({ annotations }),
  116. token,
  117. }),
  118. delete: (token: string, id: string) =>
  119. apiFetch(`/api/comments/${id}`, { method: 'DELETE', token }),
  120. };
  121. // ── Users ────────────────────────────────────────────────────────────────────
  122. export const usersApi = {
  123. list: (token: string) =>
  124. apiFetch<{ users: AdminUser[] }>('/api/users', { token }),
  125. getMe: (token: string) =>
  126. apiFetch<{ user: AdminUser }>('/api/users/me', { token }),
  127. updateMe: (token: string, data: {
  128. name?: string;
  129. avatarUrl?: string;
  130. currentPassword?: string;
  131. newPassword?: string;
  132. }) =>
  133. apiFetch<{ user: AdminUser }>('/api/users/me', {
  134. method: 'PUT',
  135. body: JSON.stringify(data),
  136. token,
  137. }),
  138. updateRole: (token: string, id: string, role: string) =>
  139. apiFetch<{ user: AdminUser }>(`/api/users/${id}/role`, {
  140. method: 'PUT',
  141. body: JSON.stringify({ role }),
  142. token,
  143. }),
  144. updateActive: (token: string, id: string, active: boolean) =>
  145. apiFetch<{ user: AdminUser }>(`/api/users/${id}/active`, {
  146. method: 'PUT',
  147. body: JSON.stringify({ active }),
  148. token,
  149. }),
  150. deleteUser: (token: string, id: string) =>
  151. apiFetch(`/api/users/${id}`, { method: 'DELETE', token }),
  152. };
  153. // ── Types ─────────────────────────────────────────────────────────────────────
  154. export interface User {
  155. id: string;
  156. email: string;
  157. name: string;
  158. role: string;
  159. avatarUrl?: string | null;
  160. }
  161. export interface AdminUser extends User {
  162. active: boolean;
  163. createdAt: string;
  164. _count?: {
  165. memberships: number;
  166. comments: number;
  167. };
  168. }
  169. export interface Project {
  170. id: string;
  171. name: string;
  172. description?: string | null;
  173. createdAt: string;
  174. members: Array<{ id: string; role: string; user: User }>;
  175. _count?: { assets: number };
  176. }
  177. export interface Asset {
  178. id: string;
  179. projectId: string;
  180. title: string;
  181. filename: string;
  182. filePath: string;
  183. thumbnail?: string | null;
  184. hlsPath?: string | null;
  185. duration?: number | null;
  186. fps?: number;
  187. mimeType: string;
  188. status: string;
  189. createdAt: string;
  190. _count?: { comments: number };
  191. }
  192. export interface AssetWithComments extends Asset {
  193. project: Project;
  194. comments: Comment[];
  195. }
  196. export interface Comment {
  197. id: string;
  198. assetId: string;
  199. userId: string;
  200. content: string;
  201. timestamp?: number | null;
  202. annotations?: AnnotationData[] | null;
  203. resolved: boolean;
  204. parentId?: string | null;
  205. createdAt: string;
  206. user: User;
  207. replies?: Comment[];
  208. }
  209. export interface AnnotationData {
  210. type: 'pen' | 'arrow' | 'rect' | 'ellipse' | 'text';
  211. color: string;
  212. points?: [number, number][];
  213. text?: string;
  214. boundingBox?: { x: number; y: number; width: number; height: number };
  215. }