refactor supabase fuck - pictures | posts

This commit is contained in:
lovebird 2026-02-17 19:19:09 +01:00
parent 7004b3f2d1
commit 0b263ed008
16 changed files with 248 additions and 318 deletions

View File

@ -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<string, Comment>();
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');

View File

@ -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<CreationWizardPopupProps> = ({
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,

View File

@ -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<ImageWizardProps> = ({
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<ImageWizardProps> = ({
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<ImageWizardProps> = ({
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'));

View File

@ -115,8 +115,6 @@ export const ListLayout = ({
categorySlugs
});
// console.log('posts', feedPosts);
const handleItemClick = (item: any) => {
if (isMobile) {
navigate(`/post/${item.id}`);

View File

@ -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,

View File

@ -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<PostPickerProps> = ({ 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);

View File

@ -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<string, string>();
postsData?.forEach(post => {
feedPosts.forEach(post => {
postsMap.set(post.id, post.title);
});

View File

@ -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,

View File

@ -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<string, any>;
likedCommentIds: string[];
}> => {
const token = await getAuthToken();
const headers: Record<string, string> = {};
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();
};

View File

@ -274,3 +274,34 @@ export const updateUserVariables = async (userId: string, variables: Record<stri
throw error;
}
};
/** Update the current user's profile via API */
export const updateProfileAPI = async (profileData: {
username?: string | null;
display_name?: string | null;
bio?: string | null;
avatar_url?: string | null;
settings?: any;
}): Promise<any> => {
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<void> => {
const { error } = await defaultSupabase.auth.updateUser({ email: newEmail });
if (error) throw error;
};

View File

@ -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]);

View File

@ -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<PostRendererProps> = (props) => {
@ -53,15 +53,6 @@ export const ThumbsRenderer: React.FC<PostRendererProps> = (props) => {
>
<LayoutGrid className="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="sm"
className="h-8 w-8 p-0 text-muted-foreground"
onClick={() => onViewModeChange('article')}
title="Article View"
>
<StretchHorizontal className="h-4 w-4" />
</Button>
</div>
</div>
</div>

View File

@ -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<CompactPostHeaderProps> = ({
>
<LayoutGrid className="h-4 w-4" />
</Button>
<Button
variant="secondary"
size="sm"
className="h-8 w-8 p-0 text-muted-foreground"
onClick={() => onViewModeChange('article')}
title="Article View"
>
<StretchHorizontal className="h-4 w-4" />
</Button>
</div>
<ExportDropdown

View File

@ -114,7 +114,7 @@ export const Gallery: React.FC<GalleryProps> = ({
videoPosterUrl,
showDesktopLayout = true,
thumbnailLayout = 'strip',
imageFit = 'cover',
imageFit = 'contain',
thumbnailsPosition = 'bottom',
thumbnailsOrientation = 'horizontal',
zoomEnabled = false,

View File

@ -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

View File

@ -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'));