Skip to content
Merged
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
79 changes: 79 additions & 0 deletions src/components/LoadingFade.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import {
ReactNode,
useLayoutEffect,
useRef,
useState,
} from 'react';
import { AnimatePresence, m } from 'framer-motion';

export interface LoadingFadeProps {
loading: boolean;
skeleton: ReactNode;
children: ReactNode;
durationMs?: number; // fade (default 300 ms)
delayMs?: number; // delay before fade starts (default 0 ms)
}

export const LoadingFade = ({
loading,
skeleton,
children,
durationMs = 300,
delayMs = 0,
}: LoadingFadeProps) => {
const durationSec = durationMs / 1000;
const delaySec = delayMs / 1000;

const skelRef = useRef<HTMLDivElement>(null);
const [minHeight, setMinHeight] = useState<number>();
const [overlaySkeleton, setOverlaySkeleton] = useState(false);

useLayoutEffect(() => {
if (loading && skelRef.current && minHeight === undefined) {
const h = skelRef.current.getBoundingClientRect().height;
if (h) {
setMinHeight(h);
setOverlaySkeleton(true);
}
}
}, [loading, minHeight]);

const handleSkeletonExit = () => {
setMinHeight(undefined);
setOverlaySkeleton(false);
};

return (
<div style={{ position: 'relative', width: '100%', minHeight }}>
<AnimatePresence onExitComplete={handleSkeletonExit}>
{loading && (
<m.div
key="skeleton"
ref={skelRef}
initial={{ opacity: 1 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: durationSec, delay: delaySec }}
style={{
position: overlaySkeleton ? 'absolute' : 'static',
inset: overlaySkeleton ? 0 : 'unset',
width: '100%',
pointerEvents: 'none',
}}
>
{skeleton}
</m.div>
)}
</AnimatePresence>

<m.div
initial={{ opacity: 0 }}
animate={{ opacity: loading ? 0 : 1 }}
transition={{ duration: durationSec, delay: delaySec }}
style={{ width: '100%' }}
>
{children}
</m.div>
</div>
);
};
8 changes: 3 additions & 5 deletions src/components/carousel/variants/carousel-top-titles.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,13 @@ export default function CarouselTopTitles({ posts, category }: Readonly<Carousel
alignItems: 'stretch',
},
'.slick-slide': {
height: 'auto',
height: 'fit-content',
},
'.slick-list': {
height: 'auto',
},
'.slick-slide > div': {
height: '100%',
minHeight: '100%',
maxHeight: '100%',
height: 'auto',
},
}}
>
Expand All @@ -47,7 +45,7 @@ export default function CarouselTopTitles({ posts, category }: Readonly<Carousel
>
<Carousel ref={carousel.carouselRef} {...carousel.carouselSettings}>
{posts.map((post: Post) => (
<Box key={`${category}-${post.id}`} sx={{ px: 0.75 }}>
<Box key={`${category}-${post.id}`} sx={{ px: 0.75, height: '100%' }}>
<PosterTopTitles post={post} />
</Box>
))}
Expand Down
10 changes: 3 additions & 7 deletions src/components/follow-unfollow-button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
// REDUX IMPORTS
import { openLoginModal } from '@redux/auth';
import { useDispatch } from 'react-redux';
import { addFollower, removeFollower } from '@redux/followers';

// LOCAL IMPORTS
import Box from '@mui/material/Box';
Expand All @@ -31,6 +30,7 @@

interface FollowUnfollowButtonProps {
profileId: string;
onActionFinish?: () => void;
followButtonMinWidth?: number;
size?: 'small' | 'medium' | 'large';
}
Expand All @@ -41,6 +41,7 @@
profileId,
size = 'medium',
followButtonMinWidth = 120,
onActionFinish = () => {},

Check failure on line 44 in src/components/follow-unfollow-button.tsx

View workflow job for this annotation

GitHub Actions / Static Analysis and Test | Node 20

Unexpected empty arrow function
}: PropsWithChildren<FollowUnfollowButtonProps>) => {
const dispatch = useDispatch();
const [loadProfile, { data: profileData, loading: profileLoading }] = useGetUserLazyQuery();
Expand Down Expand Up @@ -86,12 +87,7 @@

setIsFollowed(result?.data?.toggleFollow);
handleUpdateProfile();

if (result?.data?.toggleFollow) {
dispatch(addFollower(profile));
} else {
dispatch(removeFollower(profileId));
}
onActionFinish();

// Send notification to the profile being followed
const notificationPayload = generatePayload(
Expand Down
4 changes: 2 additions & 2 deletions src/components/poster/variants/poster-top-titles.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ const PosterTopTitles = ({ post }: { post: Post }) => {

return (
<Stack
sx={{ position: 'relative', display: 'flex', alignItems: 'center', justifyContent: 'center' }}
sx={{ position: 'relative', display: 'flex', alignItems: 'center', justifyContent: 'center', height: '100%', overflow: 'hidden' }}
alignItems={'stretch'}
spacing={{ xs: 1, sm: 2, md: 4 }}
>
Expand All @@ -47,7 +47,7 @@ const PosterTopTitles = ({ post }: { post: Post }) => {
top: 0,
left: 0,
width: '100%',
height: '100%',
height: '99%',
opacity: 0.2,
filter: "blur(5px) !important",
backgroundSize: 'cover',
Expand Down
84 changes: 38 additions & 46 deletions src/components/publication-detail-main/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ import { SubscribeToUnlockCard } from '@src/components/subscribe-to-unlock-card/
import Popover from '@mui/material/Popover';
import { useNotifications } from '@src/hooks/use-notifications.ts';
import { openLoginModal } from '@redux/auth';
import { useDispatch, useSelector } from 'react-redux';
import { useDispatch } from 'react-redux';
import { useNotificationPayload } from '@src/hooks/use-notification-payload.ts';
import AvatarProfile from "@src/components/avatar/avatar.tsx";
import { PublicationDetailProps } from '@src/components/publication-detail-main/types.ts';
Expand All @@ -56,8 +56,6 @@ import {
} from '@src/graphql/generated/hooks.tsx';
import { resolveSrc } from '@src/utils/image.ts';
import { useBookmarks } from '@src/hooks/use-bookmark.ts';
import { RootState } from '@redux/store.ts';
import { decrementCounterLikes, incrementCounterLikes, setCounterLikes } from '@redux/comments';

// ----------------------------------------------------------------------

Expand All @@ -73,8 +71,8 @@ export default function PublicationDetailMain({
const [openConfirmModal, setOpenConfirmModal] = useState(false);
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
const [hasLiked, setHasLiked] = useState(false);
const likesCount = useSelector((s: RootState) => s.comments.counterLikes[post.id] ?? post.likeCount);
const counters = useSelector((s: RootState) => s.comments.counterLikes);
const [likesCount, setLikesCount] = useState(post.likeCount);
const [commentCount, setCommentCount] = useState(post.commentCount);

const router = useRouter();
const theme = useTheme();
Expand All @@ -83,68 +81,61 @@ export default function PublicationDetailMain({
const [ hidePost ] = useHidePostMutation();
const { sendNotification } = useNotifications();
const { generatePayload } = useNotificationPayload(sessionData);
const [getIsPostLiked, { data: postLikedData, loading: postLikedLoading }] = useGetIsPostLikedLazyQuery()
const [getIsPostLiked, { loading: postLikedLoading }] = useGetIsPostLikedLazyQuery()
const [ togglePostLike, { loading: togglePostLikeLoading } ] = useTogglePostLikeMutation()
const { has, loading: loadingList } = useBookmarks();
const { toggle, loading: loadingToggle } = useToggleBookmark();
const commentCount = useSelector((s: RootState) => s.comments.postCommentCount[post.id] ?? post.commentCount);

const isBookmarked = has(post.id);
const isLoading = togglePostLikeLoading || postLikedLoading
const variants = theme.direction === 'rtl' ? varFade().inLeft : varFade().inRight;
const openMenu = Boolean(anchorEl);

const toggleReaction = async () => {
const handleToggleLike = async () => {
if (!sessionData?.authenticated) return dispatch(openLoginModal());

// Send a notification to the profile owner using the sendNotification function from useNotifications hook
const payloadForNotification = generatePayload(
'LIKE',
{
id: post.author.address,
displayName: post.author.displayName ?? 'Watchit',
avatar: resolveSrc(post.author.profilePicture || post.author.address, 'profile'),
},
{
rawDescription: `${sessionData?.user?.displayName} liked ${post.title}`,
root_id: post.id,
post_title: post.title,
}
);

try {
const res = await togglePostLike({
variables: {
input: {
postId: post.id
}
}
});
const res = await togglePostLike({ variables: { input: { postId: post.id } } });
const isNowLiked = res.data?.togglePostLike ?? false;

const isLiked = res?.data?.togglePostLike ?? false;
setHasLiked(isNowLiked);
setLikesCount((prev) => prev + (isNowLiked ? 1 : -1));

setHasLiked(isLiked);
dispatch(isLiked ? incrementCounterLikes(post.id) : decrementCounterLikes(post.id));
if (isNowLiked) {
// Send a notification to the profile owner using the sendNotification function from useNotifications hook
const payloadForNotification = generatePayload(
'LIKE',
{
id: post.author.address,
displayName: post.author.displayName ?? 'Watchit',
avatar: resolveSrc(post.author.profilePicture || post.author.address, 'profile'),
},
{
rawDescription: `${sessionData?.user?.displayName} liked ${post.title}`,
root_id: post.id,
post_title: post.title,
}
);

// Send notification to the author when not already liked
if (res?.data?.togglePostLike) {
sendNotification(post.author.address, sessionData?.user?.address ?? '', payloadForNotification);
}
} catch (err) {
console.error('Error toggling reaction:', err);
console.error(err);
}
};

useEffect(() => {
setHasLiked(postLikedData?.getIsPostLiked ?? false);
}, [postLikedData]);
const handleCommentSuccess = (wasReply = false) => {
if (!wasReply) {
// Es un comentario raíz → solo incrementamos el counter del post
setCommentCount((c) => (c ?? 0) + 1);
}
};

useEffect(() => {
getIsPostLiked({ variables: { postId: post.id } });
if (post.likeCount !== undefined) {
dispatch(setCounterLikes({ publicationId: post.id, likes: post.likeCount }));
}
}, [post.likeCount, post.id]);
getIsPostLiked({ variables: { postId: post.id } }).then((res) =>
setHasLiked(res.data?.getIsPostLiked ?? false),
);
}, [post.id]);

const handleHide = async () => {
await hidePost({ variables: { postId: post.id } });
Expand Down Expand Up @@ -350,7 +341,7 @@ export default function PublicationDetailMain({
height: '40px',
minWidth: '40px',
}}
onClick={toggleReaction}
onClick={handleToggleLike}
disabled={isLoading}
>
{isLoading ? (
Expand Down Expand Up @@ -444,6 +435,7 @@ export default function PublicationDetailMain({
displayName: post.author.displayName ?? 'Watchit',
avatar: resolveSrc(post.author.profilePicture || post.author.address, 'profile'),
}}
onSuccess={() => handleCommentSuccess(false)}
/>
) : (
<Typography
Expand All @@ -462,7 +454,7 @@ export default function PublicationDetailMain({
)}
</Box>
<Box sx={{ display: 'flex', flexDirection: 'column', mt: 2, pr: 1 }}>
<PostCommentList publicationId={post.id} showReplies />
<PostCommentList publicationId={post.id} showReplies onReplyCreated={() => handleCommentSuccess(true)} />
</Box>
</Box>
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ export const SubscribeToUnlockCard = ({
size="lg"
/>
)}
{isAuthorized && (
{isJoinButtonVisible && (
<Box sx={{ mt: 3, borderRadius: 1 }}>
<Typography variant="body2" color="textSecondary">
Join now for just <strong>{totalCostMMC} MMC/month</strong> and access to{' '}
Expand Down
9 changes: 9 additions & 0 deletions src/components/types.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { ReactNode } from 'react';

export interface HandleActionErrorProps extends Error{
requestedAmount?: {
asset?: { symbol: string };
Expand All @@ -9,3 +11,10 @@ export interface ActivateSubscriptionProfileModalProps {
isOpen: boolean;
onClose: () => void;
}

export interface LoadingFadeProps {
loading: boolean;
skeleton: ReactNode;
children: ReactNode;
duration?: number;
}
4 changes: 4 additions & 0 deletions src/config-global.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ export interface GlobalConstants {
ATTESTATION_BASE_URL: string;
FROM_BLOCK: bigint | string | number;
GQL_ENDPOINT: string;
OPEN_COLLECTIVE: string;
TERMS_AND_CONDITIONS: string;
}

export const GLOBAL_CONSTANTS: GlobalConstants = {
Expand Down Expand Up @@ -101,6 +103,8 @@ export const GLOBAL_CONSTANTS: GlobalConstants = {
process.env.VITE_URL_ATTESTATION_BASE || import.meta.env.VITE_URL_ATTESTATION_BASE || "",
FROM_BLOCK: process.env.VITE_BLOCK_FROM || import.meta.env.VITE_BLOCK_FROM || 0n,
GQL_ENDPOINT: process.env.VITE_GQL_ENDPOINT || import.meta.env.VITE_GQL_ENDPOINT || 'http://localhost:4000/graphql',
OPEN_COLLECTIVE: process.env.VITE_URL_OPEN_COLLECTIVE || import.meta.env.VITE_URL_OPEN_COLLECTIVE || '',
TERMS_AND_CONDITIONS: process.env.VITE_URL_TERMS_AND_CONDITIONS || import.meta.env.VITE_URL_TERMS_AND_CONDITIONS || '',
};

export const PATH_AFTER_LOGIN = paths.dashboard.root;
Loading
Loading