refactor supabase fuck - pictures | posts
This commit is contained in:
parent
7004b3f2d1
commit
0b263ed008
@ -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');
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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'));
|
||||
|
||||
|
||||
@ -115,8 +115,6 @@ export const ListLayout = ({
|
||||
categorySlugs
|
||||
});
|
||||
|
||||
// console.log('posts', feedPosts);
|
||||
|
||||
const handleItemClick = (item: any) => {
|
||||
if (isMobile) {
|
||||
navigate(`/post/${item.id}`);
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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);
|
||||
});
|
||||
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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();
|
||||
};
|
||||
|
||||
|
||||
@ -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;
|
||||
};
|
||||
|
||||
|
||||
@ -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]);
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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'));
|
||||
|
||||
Loading…
Reference in New Issue
Block a user