Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ dist
dist-ssr
*.local

# Package manager lock files (project uses pnpm)
package-lock.json
yarn.lock

# Editor directories and files
.vscode/*
!.vscode/copilot
Expand Down
43 changes: 19 additions & 24 deletions src/Components/FetchCard/FetchCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,15 @@ import { useEffect, useState } from "react";
import { HiCalendar } from "react-icons/hi2";
import { queries, utils } from "../../contexts/supabase/supabase";
import { Tables } from "../../contexts/supabase/database";
import type { stdProfileInfo } from "../../contexts/supabase/supabase";
import PostViewer from "../PostViewer/PostViewer";
import { formatDate } from "../../utils/date";

interface FetchCardProps {
profileId: string;
}

interface ProfileData {
interface FetchCardData {
profile: Tables<"profiles">;
featuredByHandles: string[];
pinnedPost: Tables<"posts"> | null;
Expand All @@ -19,40 +20,34 @@ interface ProfileData {
}

export default function FetchCard({ profileId }: FetchCardProps) {
const [profileData, setProfileData] = useState<ProfileData | null>(null);
const [profileData, setProfileData] = useState<FetchCardData | null>(null);
const [isLoading, setIsLoading] = useState(true);

useEffect(() => {
async function fetchAllData() {
setIsLoading(true);
try {
// Get basic profile first to get the handle
const profile = await queries.profiles.get(profileId);
const [pinnedPostResult, allPostsResult, featuredProfilesResult, featuredCountResult, categoriesResult] =
await Promise.allSettled([
profile.pinned_posts?.length
? queries.posts.get(profile.pinned_posts[0]).catch(() => null)
: Promise.resolve(null),
queries.authors.postsOf(profileId).catch(() => []),
queries.features.byUser(profileId).catch(() => []),
queries.features.byUserCount(profileId).catch(() => 0),
queries.profilesCategories.get(profileId).catch(() => []),
]);
const allPostsData = allPostsResult.status === "fulfilled" ? allPostsResult.value : [];
const pinnedPostIds = profile.pinned_posts ?? [];
const recentPosts = allPostsData
.filter((post) => post.parent_post === null)
.filter((post) => !pinnedPostIds.includes(post.id))
.sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime())

// Use our optimized function to get comprehensive profile data
const fullProfileInfo = await queries.views.standardProfileInfo(profile.handle);

// Get additional data that's not in standardProfileInfo
const featuredProfiles = await queries.features.byUser(profileId).catch(() => []);

// Transform the data to match the expected format
const recentPosts = fullProfileInfo.mainPosts
.filter(post => post.parent_post === null)
.slice(0, 2);

setProfileData({
profile,
pinnedPost: pinnedPostResult.status === "fulfilled" ? pinnedPostResult.value : null,
profile: fullProfileInfo.profile,
pinnedPost: fullProfileInfo.pinnedPosts[0] || null,
recentPosts,
featuredByHandles:
featuredProfilesResult.status === "fulfilled" ? featuredProfilesResult.value.map((p) => p.handle) : [],
featuredCount: featuredCountResult.status === "fulfilled" ? featuredCountResult.value : 0,
profileCategories: categoriesResult.status === "fulfilled" ? categoriesResult.value : [],
featuredByHandles: featuredProfiles.map(p => p.handle),
featuredCount: fullProfileInfo.featuredCount,
profileCategories: fullProfileInfo.categories,
});
} catch {
setProfileData(null);
Expand Down
123 changes: 61 additions & 62 deletions src/Components/PostViewer/PostViewer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
import { useAuth } from "../../contexts/auth/AuthContext";
import { queries, supabase, utils } from "../../contexts/supabase/supabase";
import { Tables } from "../../contexts/supabase/database";
import { formatDatePost } from "../../utils/date";

import MediaCarousel from "../MediaCarousel/MediaCarousel";
import PostAdd from "../PostAdd/PostAdd";
Expand Down Expand Up @@ -75,7 +76,7 @@ export default function PostViewer(props: PostViewerProps) {
useEffect(() => {
async function fetchPostInfo() {
try {
// Si c'est un retweet simple, on récupère les infos du post original
// If it's a simple retweet, fetch info from the original post
if (queries.posts.isSimpleRetweet(props.post)) {
const originalPost = await queries.posts.getOriginalPost(props.post);
if (originalPost) {
Expand All @@ -88,11 +89,11 @@ export default function PostViewer(props: PostViewerProps) {
setLikeCount(originalPostInfo.likesCount);
setRetweetCount(originalPostInfo.rtCount);

// Récupérer l'auteur du retweet
// Get the retweet author
const retweetAuthors = await queries.authors.ofPost(props.post.id);
setRetweetedBy(retweetAuthors[0] || null);

// Pour un retweet simple, l'utilisateur n'est jamais le dernier auteur du post original
// For simple retweets, the user is never the last author of the original post
setIsLastAuthor(false);
return;
} else {
Expand All @@ -104,7 +105,7 @@ export default function PostViewer(props: PostViewerProps) {
setRetweetedBy(null);
}

// Pour les posts normaux ou les quote retweets
// For normal posts or quote retweets
const postInfo = await queries.views.standardPostInfo(props.post.id);
setAuthors(postInfo.profiles);
setCategories(postInfo.categories);
Expand Down Expand Up @@ -195,7 +196,7 @@ export default function PostViewer(props: PostViewerProps) {
// Fetch media
useEffect(() => {
async function fetchMediaUrls() {
// Si c'est un retweet simple, utiliser l'ID du post original
// If it's a simple retweet, use the original post ID
let postIdToUse = props.post.id;
if (queries.posts.isSimpleRetweet(props.post) && originalPost) {
postIdToUse = originalPost.id;
Expand Down Expand Up @@ -240,7 +241,7 @@ export default function PostViewer(props: PostViewerProps) {
}

try {
// Pour les retweets simples, vérifier sur le post original
// For simple retweets, check on the original post
let targetPostId = props.post.id;
if (queries.posts.isSimpleRetweet(props.post) && originalPost) {
targetPostId = originalPost.id;
Expand All @@ -263,24 +264,24 @@ export default function PostViewer(props: PostViewerProps) {
void checkUserActions();
}, [props.post.id, props.post, auth.user, originalPost]);

// Récupérer le post cité si c'est un quote tweet
// Fetch quoted post if this is a quote tweet
useEffect(() => {
async function fetchQuotedPost() {
if (queries.posts.isQuoteRetweet(props.post)) {
try {
const quoted = await queries.posts.getOriginalPost(props.post);
setQuotedPost(quoted);
const quotedPostData = await queries.posts.getOriginalPost(props.post);
setQuotedPost(quotedPostData);

if (quoted) {
if (quotedPostData) {
// Use standardPostInfo for quoted post data
const quotedPostInfo = await queries.views.standardPostInfo(quoted.id);
const quotedPostInfo = await queries.views.standardPostInfo(quotedPostData.id);
setQuotedPostAuthors(quotedPostInfo.profiles);
setQuotedPostCategories(quotedPostInfo.categories);

// Récupérer les médias du post cité
// Fetch quoted post media
try {
setLoadingQuotedMedia(true);
const { data, error } = await supabase.storage.from("post-media").list(quoted.id, {
const { data, error } = await supabase.storage.from("post-media").list(quotedPostData.id, {
limit: 10,
offset: 0,
sortBy: { column: "name", order: "asc" },
Expand All @@ -289,7 +290,7 @@ export default function PostViewer(props: PostViewerProps) {
if (!error && data.length > 0) {
const urls = data.map(
(file) =>
supabase.storage.from("post-media").getPublicUrl(`${quoted.id}/${file.name}`).data.publicUrl,
supabase.storage.from("post-media").getPublicUrl(`${quotedPostData.id}/${file.name}`).data.publicUrl,
);
setQuotedPostMediaUrls(urls);
} else {
Expand Down Expand Up @@ -318,16 +319,43 @@ export default function PostViewer(props: PostViewerProps) {
void fetchQuotedPost();
}, [props.post]);

const handleReplySuccess = () => {
const handleReplySuccess = async () => {
setShowReplyForm(false);
queries.posts
.getChildren(props.post.id)
.then((childPosts) => {
setChildren(childPosts);
})
.catch(() => {
setChildren([]);
try {
const childPosts = await queries.posts.getChildren(props.post.id);

// Apply the same logic as fetchChildren for consistency
const childrenWithLikes = await Promise.all(
childPosts.map(async (child) => {
try {
const childInfo = await queries.views.standardPostInfo(child.id);
return {
...child,
likeCount: childInfo.likesCount,
};
} catch {
return {
...child,
likeCount: 0,
};
}
}),
);

// Sort by number of likes (descending), then by creation date (most recent in case of tie)
const sortedChildren = childrenWithLikes.sort((a, b) => {
// First by number of likes (descending)
if (b.likeCount !== a.likeCount) {
return b.likeCount - a.likeCount;
}
// In case of tie, by creation date (most recent first)
return new Date(b.created_at).getTime() - new Date(a.created_at).getTime();
});

setChildren(sortedChildren);
} catch {
setChildren([]);
}
};

const handlePostClick = (e: React.MouseEvent) => {
Expand Down Expand Up @@ -374,7 +402,7 @@ export default function PostViewer(props: PostViewerProps) {
try {
setIsLiking(true);

// Déterminer quel post liker (original pour les retweets simples)
// Determine which post to like (original for simple retweets)
let targetPostId = props.post.id;
if (queries.posts.isSimpleRetweet(props.post) && originalPost) {
targetPostId = originalPost.id;
Expand Down Expand Up @@ -470,35 +498,6 @@ export default function PostViewer(props: PostViewerProps) {
setIsAbandoning(false);
}
};
const formatPostDate = (date: Date): string => {
try {
if (isNaN(date.getTime())) {
return "Invalid date";
}

const now = new Date();
const diffInHours = Math.floor((now.getTime() - date.getTime()) / (1000 * 60 * 60));

if (diffInHours < 1) {
const diffInMinutes = Math.floor((now.getTime() - date.getTime()) / (1000 * 60));
return diffInMinutes < 1 ? "now" : `${diffInMinutes.toString()}m`;
} else if (diffInHours < 24) {
return `${diffInHours.toString()}h`;
} else if (diffInHours < 24 * 7) {
const diffInDays = Math.floor(diffInHours / 24);
return `${diffInDays.toString()}d`;
} else {
return date.toLocaleDateString("en-US", {
month: "short",
day: "numeric",
year: date.getFullYear() !== now.getFullYear() ? "numeric" : undefined,
});
}
} catch {
return "Invalid date";
}
};

return (
<div className="w-full">
{/* Parent posts */}
Expand Down Expand Up @@ -543,14 +542,14 @@ export default function PostViewer(props: PostViewerProps) {
onClick={handlePostClick}
>
{" "}
{/* Indicateur de retweet simple */}
{/* Simple retweet indicator */}
{queries.posts.isSimpleRetweet(props.post) && retweetedBy && (
<div className="mb-2 flex items-center gap-1 text-sm text-gray-500">
<HiOutlineArrowPath className="h-4 w-4" />
<span>{retweetedBy.handle} retweeted</span>
</div>
)}
{/* Menu burger */}
{/* Dropdown menu */}
<div className="absolute top-3 right-3 z-10">
<button
className="btn btn-ghost btn-sm btn-circle hover:bg-gray-100"
Expand Down Expand Up @@ -589,7 +588,7 @@ export default function PostViewer(props: PostViewerProps) {
// Author-only options
if (isAuthor) {
if (isSimpleRetweet) {
// Menu spécial pour les retweets : seulement abandon ownership
// Special menu for retweets: only abandon ownership
menuItems.push({
title: isAbandoning ? "Abandoning..." : "Abandon retweet",
icon: HiOutlineUserMinus,
Expand All @@ -601,7 +600,7 @@ export default function PostViewer(props: PostViewerProps) {
},
});
} else {
// Menu normal pour les posts originaux
// Normal menu for original posts
menuItems.push(
{
title: "Edit",
Expand Down Expand Up @@ -662,7 +661,7 @@ export default function PostViewer(props: PostViewerProps) {
</span>
<span className="text-gray-500">·</span>
<span className="text-gray-500" title={dateCreation.toLocaleDateString()}>
{formatPostDate(dateCreation)}
{formatDatePost(dateCreation)}
</span>
{isPinned && (
<>
Expand Down Expand Up @@ -749,7 +748,7 @@ export default function PostViewer(props: PostViewerProps) {
@{quotedPostAuthors[0]?.handle ?? "Unknown"}
</span>
<span className="text-gray-500">·</span>
<span className="text-gray-500">{formatPostDate(new Date(quotedPost.created_at))}</span>
<span className="text-gray-500">{formatDatePost(new Date(quotedPost.created_at))}</span>
</div>
{quotedPost.body && (
<div className="mt-1 line-clamp-3 text-sm break-words whitespace-pre-wrap text-gray-700">
Expand Down Expand Up @@ -891,15 +890,15 @@ export default function PostViewer(props: PostViewerProps) {
void (async () => {
if (hasRetweeted) {
try {
// Déterminer quel post utiliser pour chercher les retweets
// Determine which post to use for finding retweets
let targetPostId = props.post.id;
if (queries.posts.isSimpleRetweet(props.post) && originalPost) {
targetPostId = originalPost.id;
}
// Trouver le retweet de l'utilisateur et l'abandonner
// Find the user's retweet and abandon it
const retweets = await queries.posts.getRetweetsOf(targetPostId);

// Rechercher le retweet de l'utilisateur actuel
// Search for the current user's retweet
let userRetweetId: string | null = null;
for (const rt of retweets) {
const retweetAuthors = await queries.authors.ofPost(rt.id);
Expand Down Expand Up @@ -927,7 +926,7 @@ export default function PostViewer(props: PostViewerProps) {
alert(`Error: ${errorMessage}`);
}
} else {
// Ouvrir le dialog pour retweeter
// Open the retweet dialog
const modal = document.getElementById(`retweet-modal-${props.post.id}`) as HTMLDialogElement;
modal.showModal();
}
Expand Down
Loading