diff --git a/packages/ui/src/components/Comments.tsx b/packages/ui/src/components/Comments.tsx index 697240d4..f77dcf27 100644 --- a/packages/ui/src/components/Comments.tsx +++ b/packages/ui/src/components/Comments.tsx @@ -1,5 +1,4 @@ import { useState, useEffect, useRef } from "react"; -import { supabase } from "@/integrations/supabase/client"; import { useAuth } from "@/hooks/useAuth"; import { Button } from "@/components/ui/button"; import { Textarea } from "@/components/ui/textarea"; @@ -18,6 +17,13 @@ import { transcribeAudio } from "@/lib/openai"; import { T, translate } from "@/i18n"; import { Comment } from "@/types"; +import { + fetchCommentsAPI, + addCommentAPI, + editCommentAPI, + deleteCommentAPI, + toggleCommentLikeAPI +} from "@/modules/posts/client-pictures"; interface UserProfile { user_id: string; @@ -62,15 +68,31 @@ const Comments = ({ pictureId, initialComments }: CommentsProps) => { if (initialComments) { data = initialComments; + // Still fetch profiles and likes from API for initial comments + if (data.length > 0) { + try { + const apiResult = await fetchCommentsAPI(pictureId); + setLikedComments(new Set(apiResult.likedCommentIds)); + const newProfiles = new Map(userProfiles); + Object.entries(apiResult.profiles).forEach(([userId, profile]) => { + newProfiles.set(userId, profile as UserProfile); + }); + setUserProfiles(newProfiles); + } catch { + // Profiles/likes are optional enrichment, don't fail + } + } } else { - const { data: fetchedData, error } = await supabase - .from('comments') - .select('*') - .eq('picture_id', pictureId) - .order('created_at', { ascending: true }); + // Fetch everything from API in one call + const apiResult = await fetchCommentsAPI(pictureId); + data = apiResult.comments as Comment[]; + setLikedComments(new Set(apiResult.likedCommentIds)); - if (error) throw error; - data = fetchedData as Comment[]; + const newProfiles = new Map(userProfiles); + Object.entries(apiResult.profiles).forEach(([userId, profile]) => { + newProfiles.set(userId, profile as UserProfile); + }); + setUserProfiles(newProfiles); } if (data.length === 0) { @@ -79,46 +101,6 @@ const Comments = ({ pictureId, initialComments }: CommentsProps) => { return; } - // Fetch user's likes if logged in - let userLikes: string[] = []; - if (user) { - const commentIds = data.map(c => c.id); - const { data: likesData, error: likesError } = await supabase - .from('comment_likes') - .select('comment_id') - .eq('user_id', user.id) - .in('comment_id', commentIds); // Optimize query by filtering by comment IDs - - if (!likesError && likesData) { - userLikes = likesData.map((like: { comment_id: string }) => like.comment_id); - } - } - - setLikedComments(new Set(userLikes)); - - // Fetch user profiles for all comment authors - const uniqueUserIds = [...new Set(data.map(comment => comment.user_id))]; - if (uniqueUserIds.length > 0) { - // Optimize: Check which profiles we already have - const missingUserIds = uniqueUserIds.filter(id => !userProfiles.has(id)); - - if (missingUserIds.length > 0) { - console.log('Fetching profiles for users:', missingUserIds); - const { data: profilesData, error: profilesError } = await supabase - .from('profiles') - .select('user_id, avatar_url, display_name, username') - .in('user_id', missingUserIds); - - if (!profilesError && profilesData) { - const newProfiles = new Map(userProfiles); - profilesData.forEach(profile => { - newProfiles.set(profile.user_id, profile); - }); - setUserProfiles(newProfiles); - } - } - } - // Organize comments into nested structure with max 3 levels const commentsMap = new Map(); const rootComments: Comment[] = []; @@ -141,17 +123,7 @@ const Comments = ({ pictureId, initialComments }: CommentsProps) => { const newDepth = Math.min(parent.depth! + 1, 2); commentWithReplies.depth = newDepth; - // If we're at max depth, flatten to parent's level instead of nesting deeper if (parent.depth! >= 2) { - // Find the root ancestor to add this comment to - let rootParent = parent; - // We need to trace back to the closest ancestor that can accept children (not strictly necessary if we just flatten to max depth parent) - // Actually the logic here is: if depth > 2, we attach to the parent (who is at depth 2) - // But visually we might want to keep it indented or flat? - // The original logic tried to flatten "to parent's level", effectively making it a sibling of the parent??? - // No, it pushed to `rootParent.replies`. - - // Let's stick to original logic but fix potential type issues if (parent.replies) { parent.replies.push(commentWithReplies); } @@ -182,17 +154,7 @@ const Comments = ({ pictureId, initialComments }: CommentsProps) => { if (!user || !newComment.trim()) return; try { - const { error } = await supabase - .from('comments') - .insert([{ - picture_id: pictureId, - user_id: user.id, - content: newComment.trim(), - parent_comment_id: null - }]); - - if (error) throw error; - + await addCommentAPI(pictureId, newComment.trim()); setNewComment(""); fetchComments(); toast.success(translate('Comment added!')); @@ -206,17 +168,7 @@ const Comments = ({ pictureId, initialComments }: CommentsProps) => { if (!user || !replyText.trim()) return; try { - const { error } = await supabase - .from('comments') - .insert([{ - picture_id: pictureId, - user_id: user.id, - content: replyText.trim(), - parent_comment_id: parentId - }]); - - if (error) throw error; - + await addCommentAPI(pictureId, replyText.trim(), parentId); setReplyText(""); setReplyingTo(null); fetchComments(); @@ -231,14 +183,7 @@ const Comments = ({ pictureId, initialComments }: CommentsProps) => { if (!user) return; try { - const { error } = await supabase - .from('comments') - .delete() - .eq('id', commentId) - .eq('user_id', user.id); - - if (error) throw error; - + await deleteCommentAPI(pictureId, commentId); fetchComments(); toast.success(translate('Comment deleted')); } catch (error) { @@ -251,17 +196,7 @@ const Comments = ({ pictureId, initialComments }: CommentsProps) => { if (!user || !editText.trim()) return; try { - const { error } = await supabase - .from('comments') - .update({ - content: editText.trim(), - updated_at: new Date().toISOString() - }) - .eq('id', commentId) - .eq('user_id', user.id); - - if (error) throw error; - + await editCommentAPI(pictureId, commentId, editText.trim()); toast.success(translate('Comment updated successfully')); setEditingComment(null); setEditText(""); @@ -291,64 +226,31 @@ const Comments = ({ pictureId, initialComments }: CommentsProps) => { const isLiked = likedComments.has(commentId); try { - if (isLiked) { - // Unlike the comment - const { error } = await supabase - .from('comment_likes') - .delete() - .eq('comment_id', commentId) - .eq('user_id', user.id); + const result = await toggleCommentLikeAPI(pictureId, commentId); - if (error) throw error; - - // Update local state - const newLikedComments = new Set(likedComments); - newLikedComments.delete(commentId); - setLikedComments(newLikedComments); - - // Update comments state - const updateCommentsLikes = (comments: Comment[]): Comment[] => { - return comments.map(comment => { - if (comment.id === commentId) { - return { ...comment, likes_count: Math.max(0, comment.likes_count - 1) }; - } - if (comment.replies) { - return { ...comment, replies: updateCommentsLikes(comment.replies) }; - } - return comment; - }); - }; - setComments(updateCommentsLikes); - } else { - // Like the comment - const { error } = await supabase - .from('comment_likes') - .insert([{ - comment_id: commentId, - user_id: user.id - }]); - - if (error) throw error; - - // Update local state - const newLikedComments = new Set(likedComments); + // Update local liked state + const newLikedComments = new Set(likedComments); + if (result.liked) { newLikedComments.add(commentId); - setLikedComments(newLikedComments); - - // Update comments state - const updateCommentsLikes = (comments: Comment[]): Comment[] => { - return comments.map(comment => { - if (comment.id === commentId) { - return { ...comment, likes_count: comment.likes_count + 1 }; - } - if (comment.replies) { - return { ...comment, replies: updateCommentsLikes(comment.replies) }; - } - return comment; - }); - }; - setComments(updateCommentsLikes); + } else { + newLikedComments.delete(commentId); } + setLikedComments(newLikedComments); + + // Update comments state for likes_count + const delta = result.liked ? 1 : -1; + const updateCommentsLikes = (comments: Comment[]): Comment[] => { + return comments.map(comment => { + if (comment.id === commentId) { + return { ...comment, likes_count: Math.max(0, comment.likes_count + delta) }; + } + if (comment.replies) { + return { ...comment, replies: updateCommentsLikes(comment.replies) }; + } + return comment; + }); + }; + setComments(updateCommentsLikes); } catch (error) { console.error('Error toggling like:', error); toast.error('Failed to toggle like'); diff --git a/packages/ui/src/components/CreationWizardPopup.tsx b/packages/ui/src/components/CreationWizardPopup.tsx index b9ba7856..447a4c2b 100644 --- a/packages/ui/src/components/CreationWizardPopup.tsx +++ b/packages/ui/src/components/CreationWizardPopup.tsx @@ -17,8 +17,8 @@ import { usePromptHistory } from '@/hooks/usePromptHistory'; import { useAuth } from '@/hooks/useAuth'; import { useOrganization } from '@/contexts/OrganizationContext'; import { useMediaRefresh } from '@/contexts/MediaRefreshContext'; -import { supabase } from '@/integrations/supabase/client'; import { createPicture } from '@/modules/posts/client-pictures'; +import { fetchPostById } from '@/modules/posts/client-posts'; import { toast } from 'sonner'; interface CreationWizardPopupProps { @@ -125,23 +125,18 @@ export const CreationWizardPopup: React.FC = ({ try { const toastId = toast.loading(translate('Loading post...')); - // Fetch full post with all pictures - const { data: post, error } = await supabase - .from('posts') - .select(`*, pictures (*)`) - .eq('id', postId) - .single(); + // Fetch full post via API (returns FeedPost with pictures) + const post = await fetchPostById(postId); - if (error) { + if (!post) { toast.dismiss(toastId); toast.error(translate('Failed to load post')); - console.error('Error fetching post:', error); return; } // Transform existing pictures const existingImages = (post.pictures || []) - .sort((a: any, b: any) => (a.position - b.position)) + .sort((a: any, b: any) => ((a.position || 0) - (b.position || 0))) .map((p: any) => ({ id: p.id, path: p.id, diff --git a/packages/ui/src/components/ImageWizard.tsx b/packages/ui/src/components/ImageWizard.tsx index a0f54f52..10f8c170 100644 --- a/packages/ui/src/components/ImageWizard.tsx +++ b/packages/ui/src/components/ImageWizard.tsx @@ -1,5 +1,5 @@ import React, { useEffect } from "react"; -import { supabase } from '@/integrations/supabase/client'; +import { fetchPostById } from '@/modules/posts/client-posts'; import { Button } from "@/components/ui/button"; import { useWizardContext } from "@/hooks/useWizardContext"; import { @@ -577,17 +577,14 @@ const ImageWizard: React.FC = ({ try { const toastId = toast.loading(translate('Loading post...')); - const { data: post, error } = await supabase - .from('posts') - .select(`*, pictures (*)`) - .eq('id', postId) - .single(); + // Fetch full post via API (returns FeedPost with pictures) + const post = await fetchPostById(postId); - if (error) throw error; + if (!post) throw new Error('Post not found'); // Transform existing pictures const existingImages = (post.pictures || []) - .sort((a: any, b: any) => (a.position - b.position)) + .sort((a: any, b: any) => ((a.position || 0) - (b.position || 0))) .map((p: any) => ({ id: p.id, path: p.id, @@ -612,14 +609,6 @@ const ImageWizard: React.FC = ({ setPostTitle(post.title); setPostDescription(post.description || ''); - // We need to update the hook state for editingPostId which is destructured as currentEditingPostId - // But we can't update it directly as it comes from props/hook defaults usually. - // `useImageWizardState` initializes it. We might need to force a re-init or just use a local override? - // Actually, `postTitle` and `setPostTitle` etc are from local state returned by hook. - // But `editingPostId` is derived. - // CreationWizardPopup navigates to `/wizard` with state. - // We are ALREADY in the wizard. - // We can re-navigate to self with new state? navigate('/wizard', { state: { mode: 'post', @@ -631,14 +620,6 @@ const ImageWizard: React.FC = ({ replace: true }); - // Since re-navigation might not fully reset state if component doesn't unmount or effect doesn't fire right, - // we might rely on the router state update. - // `useImageWizardState` reads `location.state`. - // So a navigate replace should trigger re-render with new state values if the hook depends on location. - // Let's check `useImageWizardState` implementation... - // We don't have access to it here, but assuming it reacts to location state or we force reload. - // Alternatively, a full window reload is clumsy. - // Let's try navigate replace. toast.dismiss(toastId); toast.success(translate('Switched to post editing mode')); diff --git a/packages/ui/src/components/ListLayout.tsx b/packages/ui/src/components/ListLayout.tsx index 658ae391..f78c4689 100644 --- a/packages/ui/src/components/ListLayout.tsx +++ b/packages/ui/src/components/ListLayout.tsx @@ -115,8 +115,6 @@ export const ListLayout = ({ categorySlugs }); - // console.log('posts', feedPosts); - const handleItemClick = (item: any) => { if (isMobile) { navigate(`/post/${item.id}`); diff --git a/packages/ui/src/components/PhotoGrid.tsx b/packages/ui/src/components/PhotoGrid.tsx index c0209316..44fab1b2 100644 --- a/packages/ui/src/components/PhotoGrid.tsx +++ b/packages/ui/src/components/PhotoGrid.tsx @@ -1,5 +1,3 @@ -import { supabase as defaultSupabase } from "@/integrations/supabase/client"; -import * as db from '../pages/Post/db'; import { UserProfile } from '../pages/Post/types'; import MediaCard from "./MediaCard"; import React, { useEffect, useState, useRef } from "react"; @@ -38,7 +36,8 @@ export interface MediaItemType { } import type { FeedSortOption } from '@/hooks/useFeedData'; -import { mapFeedPostsToMediaItems } from "@/modules/posts/client-posts"; +import { mapFeedPostsToMediaItems, fetchPostById } from "@/modules/posts/client-posts"; +import { fetchUserMediaLikes } from "@/modules/posts/client-pictures"; interface MediaGridProps { @@ -69,8 +68,6 @@ const MediaGrid = ({ categorySlugs }: MediaGridProps) => { const { user } = useAuth(); - // Use provided client or fallback to default - const supabase = supabaseClient || defaultSupabase; const navigate = useNavigate(); const { setNavigationData, navigationData } = usePostNavigation(); const { getCache, saveCache } = useFeedCache(); @@ -225,26 +222,18 @@ const MediaGrid = ({ if (!user || mediaItems.length === 0) return; try { - // Collect IDs to check (picture_id for feed, id for collection/direct pictures) - const targetIds = mediaItems - .map(item => item.picture_id || item.id) - .filter(Boolean) as string[]; + const { pictureLikes } = await fetchUserMediaLikes(user.id); - if (targetIds.length === 0) return; + // Filter to only displayed items + const targetIds = new Set( + mediaItems.map(item => item.picture_id || item.id).filter(Boolean) + ); - // Fetch likes only for the displayed items - const { data: likesData, error } = await supabase - .from('likes') - .select('picture_id') - .eq('user_id', user.id) - .in('picture_id', targetIds); - - if (error) throw error; - - // Merge new likes with existing set setUserLikes(prev => { const newSet = new Set(prev); - likesData?.forEach(l => newSet.add(l.picture_id)); + pictureLikes.forEach(id => { + if (targetIds.has(id)) newSet.add(id); + }); return newSet; }); } catch (error) { @@ -313,14 +302,10 @@ const MediaGrid = ({ try { const toastId = toast.loading('Loading post for editing...'); - // Fetch full post with all pictures - const { data: post, error } = await supabase - .from('posts') - .select(`*, pictures(*)`) - .eq('id', postId) - .single(); + // Fetch full post via API (returns FeedPost with pictures) + const post = await fetchPostById(postId); - if (error) throw error; + if (!post) throw new Error('Post not found'); if (!post.pictures || post.pictures.length === 0) { throw new Error('No pictures found in post'); @@ -328,7 +313,7 @@ const MediaGrid = ({ // Transform pictures for wizard const wizardImages = post.pictures - .sort((a: any, b: any) => (a.position - b.position)) + .sort((a: any, b: any) => ((a.position || 0) - (b.position || 0))) .map((p: any) => ({ id: p.id, path: p.id, diff --git a/packages/ui/src/components/PostPicker.tsx b/packages/ui/src/components/PostPicker.tsx index ab9591cc..91310672 100644 --- a/packages/ui/src/components/PostPicker.tsx +++ b/packages/ui/src/components/PostPicker.tsx @@ -1,10 +1,10 @@ import React, { useEffect, useState } from 'react'; -import { supabase } from '@/integrations/supabase/client'; import { useAuth } from '@/hooks/useAuth'; import { MediaType } from '@/types'; import MediaCard from '@/components/MediaCard'; import { Loader2 } from 'lucide-react'; import { T } from '@/i18n'; +import { FEED_API_ENDPOINT } from '@/constants'; interface PostPickerProps { onSelect: (postId: string) => void; @@ -23,48 +23,35 @@ const PostPicker: React.FC = ({ onSelect }) => { const fetchPosts = async () => { try { setLoading(true); - // Fetch user's posts - const { data, error } = await supabase - .from('posts') - .select(` - *, - pictures ( - id, - image_url, - thumbnail_url, - type, - meta, - position - ) - `) - .eq('user_id', user?.id) - .order('created_at', { ascending: false }); - if (error) throw error; + const params = new URLSearchParams({ + source: 'user', + sourceId: user!.id, + limit: '100' + }); - // Transform for display - const transformed = (data || []).map(post => { - // Use first picture as cover - const pics = post.pictures as any[]; - if (!pics || pics.length === 0) return null; + const res = await fetch(`${FEED_API_ENDPOINT}?${params}`); + if (!res.ok) throw new Error(`Failed to fetch posts: ${res.statusText}`); - // Sort by position to get the first one - pics.sort((a: any, b: any) => (a.position || 0) - (b.position || 0)); + const feedPosts: any[] = await res.json(); - const cover = pics[0]; + // Transform FeedPost[] for display + const transformed = feedPosts.map(post => { + const cover = post.cover; if (!cover) return null; return { id: post.id, pictureId: cover.id, title: post.title, + created_at: post.created_at, url: cover.image_url, thumbnailUrl: cover.thumbnail_url, type: cover.type as MediaType, meta: cover.meta, - likes: 0, - comments: 0, - author: user?.email || 'Me', // Simplified + likes: post.likes_count || 0, + comments: post.comments_count || 0, + author: user?.email || 'Me', authorId: user?.id || '', }; }).filter(Boolean); diff --git a/packages/ui/src/components/UserPictures.tsx b/packages/ui/src/components/UserPictures.tsx index ea6a53ff..24fc5868 100644 --- a/packages/ui/src/components/UserPictures.tsx +++ b/packages/ui/src/components/UserPictures.tsx @@ -1,10 +1,10 @@ import { useState, useEffect } from "react"; -import { supabase } from "@/integrations/supabase/client"; import { fetchUserPictures as fetchUserPicturesAPI } from "@/modules/posts/client-pictures"; import { deletePicture } from "@/modules/posts/client-pictures"; import { deletePost } from "@/modules/posts/client-posts"; import { MediaItem } from "@/types"; +import { FEED_API_ENDPOINT } from "@/constants"; import { normalizeMediaType, isVideoType, detectMediaType } from "@/lib/mediaRegistry"; import { T, translate } from "@/i18n"; import { Loader2, ImageOff, Trash2 } from "lucide-react"; @@ -46,16 +46,18 @@ const UserPictures = ({ userId, isOwner }: UserPicturesProps) => { // 1. Fetch all pictures for the user via API const pictures = await fetchUserPicturesAPI(userId) as MediaItem[]; - // 2. Fetch all posts for the user to get titles - const { data: postsData, error: postsError } = await supabase - .from('posts') - .select('id, title') - .eq('user_id', userId); - - if (postsError) throw postsError; + // 2. Fetch all posts for the user via API to get titles + const params = new URLSearchParams({ + source: 'user', + sourceId: userId, + limit: '500' + }); + const res = await fetch(`${FEED_API_ENDPOINT}?${params}`); + if (!res.ok) throw new Error(`Failed to fetch posts: ${res.statusText}`); + const feedPosts: any[] = await res.json(); const postsMap = new Map(); - postsData?.forEach(post => { + feedPosts.forEach(post => { postsMap.set(post.id, post.title); }); diff --git a/packages/ui/src/lib/registerWidgets.ts b/packages/ui/src/lib/registerWidgets.ts index 18cd2486..a1f3fb7a 100644 --- a/packages/ui/src/lib/registerWidgets.ts +++ b/packages/ui/src/lib/registerWidgets.ts @@ -107,7 +107,7 @@ export function registerAllWidgets() { showHeader: true, showFooter: true, contentDisplay: 'below', - imageFit: 'cover', + imageFit: 'contain', variables: {} }, configSchema: { @@ -148,7 +148,7 @@ export function registerAllWidgets() { { value: 'contain', label: 'Contain' }, { value: 'cover', label: 'Cover' } ], - default: 'cover' + default: 'contain' } }, minSize: { width: 300, height: 400 }, @@ -284,7 +284,7 @@ export function registerAllWidgets() { defaultProps: { pictureIds: [], thumbnailLayout: 'strip', - imageFit: 'cover', + imageFit: 'contain', thumbnailsPosition: 'bottom', thumbnailsOrientation: 'horizontal', zoomEnabled: false, diff --git a/packages/ui/src/modules/posts/client-pictures.ts b/packages/ui/src/modules/posts/client-pictures.ts index cef210e3..9a567b91 100644 --- a/packages/ui/src/modules/posts/client-pictures.ts +++ b/packages/ui/src/modules/posts/client-pictures.ts @@ -364,3 +364,89 @@ export const fetchSelectedVersions = async (rootIds: string[], client?: Supabase }); }; + +// ============================================= +// --- Comment API Functions --- +// ============================================= + +/** Helper to get auth token */ +const getAuthToken = async () => { + const { data: sessionData } = await defaultSupabase.auth.getSession(); + return sessionData.session?.access_token; +}; + +/** Fetch all comments for a picture, with profiles and current user's liked IDs */ +export const fetchCommentsAPI = async (pictureId: string): Promise<{ + comments: any[]; + profiles: Record; + likedCommentIds: string[]; +}> => { + const token = await getAuthToken(); + const headers: Record = {}; + if (token) headers['Authorization'] = `Bearer ${token}`; + + const res = await fetch(`/api/pictures/${pictureId}/comments`, { headers }); + if (!res.ok) throw new Error(`Failed to fetch comments: ${res.statusText}`); + return await res.json(); +}; + +/** Add a comment (or reply) to a picture */ +export const addCommentAPI = async (pictureId: string, content: string, parentCommentId?: string | null) => { + const token = await getAuthToken(); + if (!token) throw new Error('Not authenticated'); + + const res = await fetch(`/api/pictures/${pictureId}/comments`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${token}` + }, + body: JSON.stringify({ content, parent_comment_id: parentCommentId || null }) + }); + if (!res.ok) throw new Error(`Failed to add comment: ${res.statusText}`); + return await res.json(); +}; + +/** Edit a comment */ +export const editCommentAPI = async (pictureId: string, commentId: string, content: string) => { + const token = await getAuthToken(); + if (!token) throw new Error('Not authenticated'); + + const res = await fetch(`/api/pictures/${pictureId}/comments/${commentId}`, { + method: 'PATCH', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${token}` + }, + body: JSON.stringify({ content }) + }); + if (!res.ok) throw new Error(`Failed to edit comment: ${res.statusText}`); + return await res.json(); +}; + +/** Delete a comment */ +export const deleteCommentAPI = async (pictureId: string, commentId: string) => { + const token = await getAuthToken(); + if (!token) throw new Error('Not authenticated'); + + const res = await fetch(`/api/pictures/${pictureId}/comments/${commentId}`, { + method: 'DELETE', + headers: { 'Authorization': `Bearer ${token}` } + }); + if (!res.ok) throw new Error(`Failed to delete comment: ${res.statusText}`); + return await res.json(); +}; + +/** Toggle like on a comment */ +export const toggleCommentLikeAPI = async (pictureId: string, commentId: string): Promise<{ liked: boolean }> => { + const token = await getAuthToken(); + if (!token) throw new Error('Not authenticated'); + + const res = await fetch(`/api/pictures/${pictureId}/comments/${commentId}/like`, { + method: 'POST', + headers: { 'Authorization': `Bearer ${token}` } + }); + if (!res.ok) throw new Error(`Failed to toggle comment like: ${res.statusText}`); + return await res.json(); +}; + diff --git a/packages/ui/src/modules/user/client-user.ts b/packages/ui/src/modules/user/client-user.ts index a155cdc5..e30bf322 100644 --- a/packages/ui/src/modules/user/client-user.ts +++ b/packages/ui/src/modules/user/client-user.ts @@ -274,3 +274,34 @@ export const updateUserVariables = async (userId: string, variables: Record => { + const { data: sessionData } = await defaultSupabase.auth.getSession(); + const token = sessionData.session?.access_token; + if (!token) throw new Error('Not authenticated'); + + const res = await fetch('/api/profile', { + method: 'PATCH', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${token}` + }, + body: JSON.stringify(profileData) + }); + if (!res.ok) throw new Error(`Failed to update profile: ${res.statusText}`); + return await res.json(); +}; + +/** Update the current user's email (auth-level operation) */ +export const updateUserEmail = async (newEmail: string): Promise => { + const { error } = await defaultSupabase.auth.updateUser({ email: newEmail }); + if (error) throw error; +}; + diff --git a/packages/ui/src/pages/Post.tsx b/packages/ui/src/pages/Post.tsx index d2357f75..5bb5f4ac 100644 --- a/packages/ui/src/pages/Post.tsx +++ b/packages/ui/src/pages/Post.tsx @@ -152,7 +152,7 @@ const Post = ({ postId: propPostId, embedded = false, className }: PostProps) => const savedMode = localStorage.getItem('postViewMode'); if (savedMode === 'compact' || savedMode === 'thumbs') { setViewMode(savedMode as any); - } else if (post?.settings?.display) { + } else if (post?.settings?.display && (post.settings.display === 'compact' || post.settings.display === 'thumbs')) { setViewMode(post.settings.display); } }, [post]); diff --git a/packages/ui/src/pages/Post/renderers/ThumbsRenderer.tsx b/packages/ui/src/pages/Post/renderers/ThumbsRenderer.tsx index 742dc2a7..2158645d 100644 --- a/packages/ui/src/pages/Post/renderers/ThumbsRenderer.tsx +++ b/packages/ui/src/pages/Post/renderers/ThumbsRenderer.tsx @@ -5,7 +5,7 @@ import { normalizeMediaType, isVideoType } from "@/lib/mediaRegistry"; import { PHOTO_GRID_THUMBNAIL_WIDTH, PHOTO_GRID_IMAGE_FORMAT } from "@/constants"; import { Button } from "@/components/ui/button"; -import { LayoutGrid, StretchHorizontal, Grid, FileText, ArrowLeft } from 'lucide-react'; +import { LayoutGrid, Grid, ArrowLeft } from 'lucide-react'; import { Link } from "react-router-dom"; export const ThumbsRenderer: React.FC = (props) => { @@ -53,15 +53,6 @@ export const ThumbsRenderer: React.FC = (props) => { > - diff --git a/packages/ui/src/pages/Post/renderers/components/CompactPostHeader.tsx b/packages/ui/src/pages/Post/renderers/components/CompactPostHeader.tsx index 7e6e0e43..d0473192 100644 --- a/packages/ui/src/pages/Post/renderers/components/CompactPostHeader.tsx +++ b/packages/ui/src/pages/Post/renderers/components/CompactPostHeader.tsx @@ -1,6 +1,6 @@ import React from "react"; import { Link } from "react-router-dom"; -import { LayoutGrid, StretchHorizontal, Edit3, MoreVertical, Trash2, Save, X, Grid, FolderTree } from 'lucide-react'; +import { LayoutGrid, Edit3, MoreVertical, Trash2, Save, X, Grid, FolderTree } from 'lucide-react'; import { Button } from "@/components/ui/button"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"; import { Input } from "@/components/ui/input"; @@ -20,7 +20,7 @@ interface CompactPostHeaderProps { mediaItem: PostMediaItem; authorProfile: UserProfile; isOwner: boolean; - onViewModeChange: (mode: 'thumbs' | 'compact' | 'article') => void; + onViewModeChange: (mode: 'thumbs' | 'compact') => void; onExportMarkdown: (type: 'hugo' | 'obsidian' | 'raw') => void; onSaveChanges: () => void; onEditModeToggle: () => void; @@ -134,15 +134,6 @@ export const CompactPostHeader: React.FC = ({ > - = ({ videoPosterUrl, showDesktopLayout = true, thumbnailLayout = 'strip', - imageFit = 'cover', + imageFit = 'contain', thumbnailsPosition = 'bottom', thumbnailsOrientation = 'horizontal', zoomEnabled = false, diff --git a/packages/ui/src/pages/Post/types.ts b/packages/ui/src/pages/Post/types.ts index e9081817..8fb5e0ea 100644 --- a/packages/ui/src/pages/Post/types.ts +++ b/packages/ui/src/pages/Post/types.ts @@ -23,7 +23,7 @@ export interface PostItem { // Structured settings instead of `any` export interface PostSettings { - display?: 'compact' | 'article' | 'thumbs'; + display?: 'compact' | 'thumbs'; link?: string; // For link posts image_url?: string; thumbnail_url?: string; @@ -65,7 +65,7 @@ export interface PostRendererProps { // Handlers onEditModeToggle: () => void; onEditPost: () => void; - onViewModeChange: (mode: 'compact' | 'article' | 'thumbs') => void; + onViewModeChange: (mode: 'compact' | 'thumbs') => void; onExportMarkdown: () => void; onSaveChanges: () => void; onDeletePost: () => void; // Opens dialog diff --git a/packages/ui/src/pages/Profile.tsx b/packages/ui/src/pages/Profile.tsx index c0e4ad27..3456216c 100644 --- a/packages/ui/src/pages/Profile.tsx +++ b/packages/ui/src/pages/Profile.tsx @@ -1,5 +1,4 @@ import { useState, useEffect } from "react"; -import { supabase } from "@/integrations/supabase/client"; import { useAuth } from "@/hooks/useAuth"; import ImageGallery from "@/components/ImageGallery"; import { ImageFile } from "@/types"; @@ -17,7 +16,7 @@ import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { T, translate, getCurrentLang, supportedLanguages, setLanguage } from "@/i18n"; import { uploadImage } from '@/lib/uploadUtils'; -import { getUserSecrets, updateUserSecrets, getUserVariables, updateUserVariables } from '@/modules/user/client-user'; +import { getUserSecrets, updateUserSecrets, getUserVariables, updateUserVariables, fetchProfileAPI, updateProfileAPI, updateUserEmail } from '@/modules/user/client-user'; import { VariablesEditor } from '@/components/variables/VariablesEditor'; import { Sidebar, @@ -70,17 +69,10 @@ const Profile = () => { const fetchProfile = async () => { try { console.log('Fetching profile for user:', user?.id); - const { data, error } = await supabase - .from('profiles') - .select('username, display_name, bio, avatar_url, settings') - .eq('user_id', user?.id) - .single(); + const result = await fetchProfileAPI(user!.id); - if (error && error.code !== 'PGRST116') { - throw error; - } - - if (data) { + if (result?.profile) { + const data = result.profile; setProfile({ username: data.username || '', display_name: data.display_name || '', @@ -90,7 +82,7 @@ const Profile = () => { }); // Fetch secrets - const fetchedSecrets = await getUserSecrets(user.id); + const fetchedSecrets = await getUserSecrets(user!.id); if (fetchedSecrets) { setSecrets(fetchedSecrets); } @@ -106,32 +98,21 @@ const Profile = () => { setUpdatingProfile(true); try { - // Update profile in database - const { error: profileError } = await supabase - .from('profiles') - .upsert({ - user_id: user.id, - username: profile.username || null, - display_name: profile.display_name || null, - bio: profile.bio || null, - avatar_url: profile.avatar_url || null, - settings: profile.settings || {} - }, { - onConflict: 'user_id' - }); - - if (profileError) throw profileError; + // Update profile via API + await updateProfileAPI({ + username: profile.username || null, + display_name: profile.display_name || null, + bio: profile.bio || null, + avatar_url: profile.avatar_url || null, + settings: profile.settings || {} + }); // Update secrets await updateUserSecrets(user.id, secrets); // Update email if changed if (email !== user.email && email.trim()) { - const { error: emailError } = await supabase.auth.updateUser({ - email: email - }); - - if (emailError) throw emailError; + await updateUserEmail(email); } toast.success(translate('Profile updated successfully'));