266 lines
10 KiB
TypeScript
266 lines
10 KiB
TypeScript
import { supabase as defaultSupabase } from "@/integrations/supabase/client";
|
|
import { UserProfile } from "@/modules/posts/views/types";
|
|
import { MediaType, MediaItem } from "@/types";
|
|
import { SupabaseClient } from "@supabase/supabase-js";
|
|
import { fetchWithDeduplication, getAuthToken } from "@/lib/db";
|
|
|
|
export interface FeedPost {
|
|
id: string; // Post ID
|
|
title: string;
|
|
description: string | null;
|
|
created_at: string;
|
|
user_id: string;
|
|
pictures: MediaItem[]; // All visible pictures
|
|
cover: MediaItem; // The selected cover picture
|
|
likes_count: number;
|
|
comments_count: number;
|
|
type: MediaType;
|
|
author?: UserProfile;
|
|
settings?: any;
|
|
is_liked?: boolean;
|
|
category_paths?: any[][]; // Array of category paths (each path is root -> leaf)
|
|
meta?: any; // Additional metadata
|
|
}
|
|
|
|
export const fetchPostDetailsAPI = async (id: string, options: { sizes?: string, formats?: string } = {}) => {
|
|
const params = new URLSearchParams();
|
|
if (options.sizes) params.set('sizes', options.sizes);
|
|
if (options.formats) params.set('formats', options.formats);
|
|
|
|
const { getCurrentLang } = await import('@/i18n');
|
|
params.set('lang', getCurrentLang());
|
|
|
|
const qs = params.toString();
|
|
const url = `/api/posts/${id}${qs ? `?${qs}` : ''}`;
|
|
|
|
// We rely on the browser/hook to handle auth headers if global fetch is intercepted,
|
|
// OR we explicitly get session?
|
|
// Usually standard `fetch` in our app might not send auth if using implicit flows or we need to pass headers.
|
|
// In `useFeedData`, we manually added headers.
|
|
// Let's assume we need to handle auth here or use a helper that does.
|
|
// To keep it simple for now, we'll import `supabase` and get session.
|
|
|
|
const token = await getAuthToken();
|
|
const headers: Record<string, string> = {};
|
|
if (token) headers['Authorization'] = `Bearer ${token}`;
|
|
|
|
const res = await fetch(url, { headers });
|
|
if (!res.ok) {
|
|
if (res.status === 404) return null;
|
|
throw new Error(`Failed to fetch post: ${res.statusText}`);
|
|
}
|
|
|
|
return res.json();
|
|
};
|
|
|
|
/** Fetch posts with cover pictures, paginated */
|
|
export const fetchPostsList = async (options: { page?: number; limit?: number; userId?: string } = {}) => {
|
|
const params = new URLSearchParams();
|
|
params.append('page', String(options.page ?? 0));
|
|
params.append('limit', String(options.limit ?? 30));
|
|
if (options.userId) params.append('userId', options.userId);
|
|
|
|
const res = await fetch(`/api/posts?${params}`);
|
|
if (!res.ok) throw new Error(`Failed to fetch posts: ${res.statusText}`);
|
|
return await res.json() as { data: any[]; count: number; page: number; limit: number };
|
|
};
|
|
|
|
export const fetchPostById = async (id: string, client?: SupabaseClient) => {
|
|
// Use API-mediated fetching instead of direct Supabase calls
|
|
// This returns enriched FeedPost data including category_paths, author info, etc.
|
|
return fetchWithDeduplication(`post-${id}`, async () => {
|
|
const data = await fetchPostDetailsAPI(id);
|
|
if (!data) return null;
|
|
return data;
|
|
}, 1);
|
|
};
|
|
|
|
|
|
export const fetchFullPost = async (postId: string, client?: SupabaseClient) => {
|
|
const supabase = client || defaultSupabase;
|
|
return fetchWithDeduplication(`full-post-${postId}`, async () => {
|
|
const { data, error } = await supabase
|
|
.from('posts')
|
|
.select(`*, pictures (*)`)
|
|
.eq('id', postId)
|
|
.single();
|
|
|
|
if (error) throw error;
|
|
return data;
|
|
});
|
|
};
|
|
|
|
export const deletePost = async (id: string, client?: SupabaseClient) => {
|
|
const token = await getAuthToken();
|
|
if (!token) throw new Error('No active session');
|
|
|
|
const response = await fetch(`/api/posts/${id}`, {
|
|
method: 'DELETE',
|
|
headers: {
|
|
'Authorization': `Bearer ${token}`
|
|
}
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`Failed to delete post: ${response.statusText}`);
|
|
}
|
|
|
|
return await response.json();
|
|
};
|
|
export const createPost = async (postData: { title: string, description?: string, settings?: any, meta?: any }) => {
|
|
const token = await getAuthToken();
|
|
if (!token) throw new Error('No active session');
|
|
|
|
const response = await fetch(`/api/posts`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Authorization': `Bearer ${token}`
|
|
},
|
|
body: JSON.stringify(postData)
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`Failed to create post: ${response.statusText}`);
|
|
}
|
|
|
|
return await response.json();
|
|
};
|
|
|
|
export const updatePostDetails = async (postId: string, updates: { title?: string, description?: string, settings?: any, meta?: any }, client?: SupabaseClient) => {
|
|
const token = await getAuthToken();
|
|
if (!token) throw new Error('No active session');
|
|
|
|
const response = await fetch(`/api/posts/${postId}`, {
|
|
method: 'PATCH',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Authorization': `Bearer ${token}`
|
|
},
|
|
body: JSON.stringify(updates)
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`Failed to update post details: ${response.statusText}`);
|
|
}
|
|
|
|
return await response.json();
|
|
};
|
|
|
|
|
|
// Map FeedPost[] back to MediaItemType[] for backward compatibility w/ PhotoGrid
|
|
export const mapFeedPostsToMediaItems = (posts: FeedPost[], sortBy: 'latest' | 'top' = 'latest'): any[] => {
|
|
return posts.map(post => {
|
|
let cover = post.cover;
|
|
|
|
// Use server-provided cover (respects is_selected versions).
|
|
// Only fall back to position-based selection if no cover was provided.
|
|
if (!cover && post.pictures && post.pictures.length > 0) {
|
|
const validPics = post.pictures.filter((p: any) => p.visible !== false);
|
|
|
|
if (validPics.length > 0) {
|
|
const sortedByPosition = [...validPics].sort((a, b) => (a.position || 0) - (b.position || 0));
|
|
cover = sortedByPosition[0];
|
|
} else {
|
|
cover = post.pictures[0]; // Fallback to any picture
|
|
}
|
|
}
|
|
|
|
if (!cover) {
|
|
// Support items without covers that should still be displayed
|
|
const allowedWithoutCover = ['page-vfs-folder', 'page-vfs-file', 'page-external', 'page-intern', 'page-github', 'place-search'];
|
|
if (allowedWithoutCover.includes(post.type)) {
|
|
return {
|
|
id: post.id,
|
|
picture_id: post.id,
|
|
title: post.title,
|
|
description: post.description,
|
|
image_url: post.type === 'place-search' ? '' : (post.meta?.url || ''),
|
|
thumbnail_url: null,
|
|
type: post.type as MediaType,
|
|
meta: post.meta,
|
|
created_at: post.created_at,
|
|
user_id: post.user_id,
|
|
likes_count: post.likes_count,
|
|
comments: [{ count: post.comments_count }],
|
|
responsive: undefined,
|
|
job: undefined,
|
|
author: post.author,
|
|
versionCount: 1,
|
|
_searchSource: (post as any)._searchSource
|
|
};
|
|
}
|
|
return null;
|
|
}
|
|
|
|
const versionCount = post.pictures ? post.pictures.length : 1;
|
|
|
|
const coverType = ((cover as any).type || (cover as any).mediaType) as MediaType;
|
|
const finalType = (post.type && post.type.startsWith('page-')) ? (post.type as MediaType) : coverType;
|
|
|
|
return {
|
|
id: post.id,
|
|
picture_id: cover.id,
|
|
title: post.title,
|
|
description: post.description,
|
|
image_url: cover.image_url,
|
|
thumbnail_url: cover.thumbnail_url,
|
|
type: finalType,
|
|
meta: { ...(post.meta || {}), ...(cover.meta || {}) },
|
|
created_at: post.created_at,
|
|
user_id: post.user_id,
|
|
likes_count: post.likes_count,
|
|
comments: [{ count: post.comments_count }],
|
|
responsive: (cover as any).responsive,
|
|
job: (cover as any).job,
|
|
author: post.author,
|
|
versionCount,
|
|
_searchSource: (post as any)._searchSource
|
|
};
|
|
}).filter(item => item !== null);
|
|
};
|
|
|
|
// Augment posts if they come from API/Hydration (missing cover/author)
|
|
export const augmentFeedPosts = (posts: any[]): FeedPost[] => {
|
|
return posts.map(p => {
|
|
// Check if we need to augment (heuristic: missing cover)
|
|
if (!p.cover) {
|
|
const pics = p.pictures || [];
|
|
const validPics = pics.filter((pic: any) => pic.visible !== false)
|
|
.sort((a: any, b: any) => (a.position || 0) - (b.position || 0));
|
|
|
|
return {
|
|
...p,
|
|
cover: validPics[0] || pics[0], // fallback to first if none visible?
|
|
author: p.author || (p.author ? {
|
|
user_id: p.author.user_id,
|
|
username: p.author.username,
|
|
display_name: p.author.display_name,
|
|
avatar_url: p.author.avatar_url
|
|
} : undefined)
|
|
};
|
|
}
|
|
return p;
|
|
});
|
|
};
|
|
|
|
export const updatePostMeta = async (postId: string, meta: any, client?: SupabaseClient) => {
|
|
const token = await getAuthToken();
|
|
if (!token) throw new Error('No active session');
|
|
|
|
const response = await fetch(`/api/posts/${postId}`, {
|
|
method: 'PATCH',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Authorization': `Bearer ${token}`
|
|
},
|
|
body: JSON.stringify({ meta })
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`Failed to update post meta: ${response.statusText}`);
|
|
}
|
|
|
|
return await response.json();
|
|
};
|