diff --git a/frontend/hooks/useUpdateAsset.ts b/frontend/hooks/useUpdateAsset.ts new file mode 100644 index 00000000..d0f886b7 --- /dev/null +++ b/frontend/hooks/useUpdateAsset.ts @@ -0,0 +1,36 @@ +'use client'; + +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { assetApiClient } from '@/lib/api/assets'; +import { queryKeys } from '@/lib/query/keys'; +import type { Asset } from '@/lib/query/types/asset'; + +export function useUpdateAsset(assetId: string) { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: (data: Record) => + assetApiClient.updateAsset(assetId, data), + + onMutate: async (newData) => { + // Optimistic update + await queryClient.cancelQueries({ queryKey: queryKeys.assets.detail(assetId) }); + const previousAsset = queryClient.getQueryData(queryKeys.assets.detail(assetId)); + if (previousAsset) { + queryClient.setQueryData(queryKeys.assets.detail(assetId), { ...previousAsset, ...newData }); + } + return { previousAsset }; + }, + + onError: (_err, _newData, context) => { + // Rollback on error + if (context?.previousAsset) { + queryClient.setQueryData(queryKeys.assets.detail(assetId), context.previousAsset); + } + }, + + onSettled: () => { + queryClient.invalidateQueries({ queryKey: queryKeys.assets.detail(assetId) }); + }, + }); +} \ No newline at end of file diff --git a/frontend/lib/api/asset-photos.ts b/frontend/lib/api/asset-photos.ts new file mode 100644 index 00000000..f1c379fc --- /dev/null +++ b/frontend/lib/api/asset-photos.ts @@ -0,0 +1,27 @@ +import { api } from '../api'; + +export interface AssetPhoto { + id: string; + url: string; + isPrimary: boolean; + createdAt: string; +} + +export const assetPhotosApiClient = { + list: (assetId: string): Promise => + api.get(`/assets/${assetId}/photos`).then((r) => r.data), + + upload: (assetId: string, file: File): Promise => { + const form = new FormData(); + form.append('file', file); + return api.post(`/assets/${assetId}/photos`, form, { + headers: { 'Content-Type': 'multipart/form-data' }, + }).then((r) => r.data); + }, + + delete: (assetId: string, photoId: string): Promise => + api.delete(`/assets/${assetId}/photos/${photoId}`).then(() => undefined), + + setPrimary: (assetId: string, photoId: string): Promise => + api.patch(`/assets/${assetId}/photos/${photoId}/primary`).then((r) => r.data), +}; \ No newline at end of file diff --git a/frontend/lib/api/http-client.ts b/frontend/lib/api/http-client.ts new file mode 100644 index 00000000..d9a74a94 --- /dev/null +++ b/frontend/lib/api/http-client.ts @@ -0,0 +1,20 @@ +/** + * Single HTTP client for the frontend. + * + * All API requests should go through the `api` axios instance from `lib/api.ts`. + * Per-resource modules in `lib/api/*.ts` wrap specific endpoints. + * + * The legacy `apiClient.request` wrapper in `lib/api/client.ts` is deprecated; + * new code should use the resource modules directly. Raw `fetch('/api/...')` + * calls should be replaced with `api.get/post/patch/delete(...)` to ensure + * the correct base URL and auth token are attached. + * + * @example + * import { api } from '@/lib/api'; + * const { data } = await api.get('/assets'); + * + * @example + * import { assetApiClient } from '@/lib/api/assets'; + * const assets = await assetApiClient.getAssets(); + */ +export { api } from '../api'; \ No newline at end of file diff --git a/frontend/next.config.ts b/frontend/next.config.ts index de2d453e..0aa712e8 100644 --- a/frontend/next.config.ts +++ b/frontend/next.config.ts @@ -2,6 +2,29 @@ import type { NextConfig } from "next"; const nextConfig: NextConfig = { output: "standalone", + poweredByHeader: false, + images: { + remotePatterns: [ + { protocol: "https", hostname: "**" }, + ], + }, + async headers() { + return [ + { + source: "/(.*)", + headers: [ + { key: "X-Frame-Options", value: "DENY" }, + { key: "X-Content-Type-Options", value: "nosniff" }, + { key: "Referrer-Policy", value: "strict-origin-when-cross-origin" }, + { key: "Permissions-Policy", value: "camera=(), microphone=(), geolocation=()" }, + { + key: "Content-Security-Policy", + value: "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; connect-src 'self' http://localhost:6003 ws://localhost:6003 https://horizon-testnet.stellar.org https://soroban-testnet.stellar.org; font-src 'self' data:;", + }, + ], + }, + ]; + }, }; export default nextConfig; \ No newline at end of file