Skip to content
1 change: 1 addition & 0 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@

<div id="root" style="opacity:0"></div>

<script>performance.mark('html:start');</script>
<script type="module" src="/src/index.tsx"></script>
<script>
function showApp() {
Expand Down
6 changes: 6 additions & 0 deletions src/components/app-ready.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@ export default function AppReady() {
useEffect(() => {
requestAnimationFrame(() =>
requestAnimationFrame(() => {
performance.mark('react:first-paint');
performance.measure('html→entry','html:start','entry:begin');
performance.measure('entry→react','entry:begin','react:first-paint');
console.table(performance.getEntriesByType('measure').map(m=>({
name:m.name, ms: Math.round(m.duration)
})));
window.dispatchEvent(new Event('app:ready'));
})
);
Expand Down
101 changes: 73 additions & 28 deletions src/components/leave-tip-card.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,17 @@
import { FC, useState } from 'react';
import { FC, useEffect, useMemo, useState } from 'react';
import { Typography, Box, TextField, Stack, Paper } from '@mui/material';
import Card from '@mui/material/Card';
import CardContent from '@mui/material/CardContent';
import LoadingButton from '@mui/lab/LoadingButton';
import { useDispatch } from 'react-redux';
import { openLoginModal } from '@redux/auth';
import { useAuth } from '@src/hooks/use-auth.ts';
import { useTransfer } from '@src/hooks/protocol/use-transfer.ts';
import { Post } from '@src/graphql/generated/graphql.ts';
import { notifyError, notifySuccess } from '@src/libs/notifications/internal-notifications.ts';
import { ERRORS } from '@src/libs/notifications/errors.ts';
import { SUCCESS } from '@src/libs/notifications/success.ts';
import { GetTipsByBakerForPostDocument, useCreateTipMutation } from '@src/graphql/generated/hooks.tsx';

const tipOptions = [
{ value: '10', title: '10', subtitle: 'A token of appreciation' },
Expand All @@ -12,22 +20,75 @@ const tipOptions = [
];

export const LeaveTipCard: FC<{ post: Post }> = ({ post }) => {
const dispatch = useDispatch();
const { session } = useAuth();
const { transfer, loading: transferLoading, error } = useTransfer();
const [createTip] = useCreateTipMutation();

const [selectedTip, setSelectedTip] = useState('10');
const [customTip, setCustomTip] = useState('');
const [successMessage, setSuccessMessage] = useState(false);

useEffect(() => {
if (error) {
notifyError(error as ERRORS);
}
}, [error]);

const amount = useMemo(() => {
// Si hay custom, usa custom. Sino, usa el seleccionado.
const raw = selectedTip ? selectedTip : customTip;
const parsed = Number(raw);
// Evita NaN o negativos
return Number.isFinite(parsed) && parsed > 0 ? parsed : 0;
}, [selectedTip, customTip]);

const recipient = post?.author?.address ?? '';

const handleTipChange = (value: string) => {
setSelectedTip(value);
setCustomTip('');
setSuccessMessage(false);
};

const handleCustomTipChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setSelectedTip('');
setCustomTip(event.target.value);
setSuccessMessage(false);
};

const handleSendTip = async () => {
if (!session?.authenticated) return dispatch(openLoginModal());
if (!recipient || amount <= 0) return;

try {
const pTransfer = transfer({ amount, recipient });

const pCreateTip = pTransfer.then(() =>
createTip({
variables: {
input: {
postId: post.id,
creator: recipient,
amount,
txHash: null,
message: 'tip',
},
},
refetchQueries: [
{ query: GetTipsByBakerForPostDocument, variables: { postId: post.id } },
],
awaitRefetchQueries: true,
})
);

await Promise.all([pTransfer, pCreateTip]);

notifySuccess(SUCCESS.TIP_CREATED_SUCCESSFULLY);
} catch (e) {
console.error('Transfer error:', e);
}
};

const isDisabled = transferLoading || amount <= 0 || !recipient;

return (
<Card sx={{ width: '100%', maxWidth: { lg: 400 }, margin: 'auto', backgroundColor: '#2B2D31' }}>
<CardContent>
Expand All @@ -37,6 +98,7 @@ export const LeaveTipCard: FC<{ post: Post }> = ({ post }) => {
<Typography variant="body2" color="textSecondary" sx={{ mb: 3 }}>
Choose an amount to leave a tip and support the content you love.
</Typography>

<Stack spacing={2}>
<Stack spacing={2} direction="row">
{tipOptions.map((option) => (
Expand All @@ -56,56 +118,39 @@ export const LeaveTipCard: FC<{ post: Post }> = ({ post }) => {
'&:hover': { opacity: 1 },
}}
>
<Typography variant="body1" fontWeight="bold" align={'center'}>
<Typography variant="body1" fontWeight="bold" align="center">
{option.title}
</Typography>
<Typography
variant="subtitle2"
align={'center'}
style={{
color: 'text.secondary',
fontSize: '0.7rem',
}}
>
<Typography variant="subtitle2" align="center" sx={{ fontSize: '0.7rem' }}>
MMC
</Typography>
</Paper>
))}
</Stack>

<Box>
<TextField
type="number"
placeholder="Enter custom tip in MMC"
fullWidth
value={customTip}
onChange={handleCustomTipChange}
InputProps={{
inputProps: { min: 1 },
}}
InputProps={{ inputProps: { min: 1 } }}
/>
</Box>
</Stack>

<Stack direction="row" justifyContent="center" sx={{ mt: 4 }}>
<LoadingButton
variant="contained"
disabled={true}
sx={{ width: '100%', py: 1.5 }}
loading={false}
loading={transferLoading}
disabled={isDisabled}
onClick={handleSendTip}
>
Leave a Tip
</LoadingButton>
</Stack>

<Typography variant="body2" color="text.secondary" align="center" sx={{ mt: 2 }}>
This feature is coming in the next release!
</Typography>

{successMessage && (
<Typography variant="body2" color="success.main" align="center" sx={{ mt: 2 }}>
Tip sent successfully! Thank you for your support.
</Typography>
)}
</CardContent>
</Card>
);
Expand Down
137 changes: 89 additions & 48 deletions src/components/nav-section/vertical/nav-item.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { useRef, useState, useCallback } from 'react';
// @mui
import Box from '@mui/material/Box';
import Link from '@mui/material/Link';
import Tooltip from '@mui/material/Tooltip';
import Popover from '@mui/material/Popover';
import Stack from '@mui/material/Stack';
import ListItemText from '@mui/material/ListItemText';
// routes
import { RouterLink } from '@src/routes/components';
Expand All @@ -18,18 +20,26 @@ type Props = NavItemProps & {
};

export default function NavItem({
item,
open,
depth,
active,
config,
externalLink,
...other
}: Props) {
item,
open,
depth,
active,
config,
externalLink,
...other
}: Props) {
const { title, path, icon, info, children, disabled, caption, roles } = item;

const subItem = depth !== 1;

const anchorRef = useRef<HTMLSpanElement | null>(null);
const [popoverOpen, setPopoverOpen] = useState(false);

const handleOpen = useCallback(() => setPopoverOpen(true), []);
const handleClose = useCallback(() => setPopoverOpen(false), []);

const comingSoonText = caption || '✨ Coming soon';

const renderContent = (
<StyledItem
disableGutters
Expand All @@ -52,13 +62,7 @@ export default function NavItem({
{!(config.hiddenLabel && !subItem) && (
<ListItemText
primary={title}
secondary={
caption ? (
<Tooltip title={caption} placement="top-start">
<span>{caption}</span>
</Tooltip>
) : null
}
secondary={disabled ? null : undefined}
primaryTypographyProps={{
noWrap: true,
typography: 'body2',
Expand Down Expand Up @@ -95,44 +99,81 @@ export default function NavItem({
return null;
}

// External link
if (externalLink)
return (
<Link
href={path}
target="_blank"
rel="noopener"
underline="none"
color="inherit"
sx={{
...(disabled && {
cursor: 'default',
}),
}}
>
const commonLinkProps = {
underline: 'none' as const,
color: 'inherit',
'aria-disabled': disabled ? 'true' : undefined,
onClick: (e: React.MouseEvent) => {
if (disabled) {
e.preventDefault();
e.stopPropagation();
}
},
sx: {
...(disabled && {
cursor: 'not-allowed',
pointerEvents: 'auto',
}),
},
};

let clickableEl: React.ReactNode;

if (externalLink) {
clickableEl = (
<Link href={path} target="_blank" rel="noopener" {...commonLinkProps}>
{renderContent}
</Link>
);
} else if (children) {
clickableEl = renderContent;
} else {
clickableEl = (
<Link component={RouterLink} href={path ?? '/'} {...commonLinkProps}>
{renderContent}
</Link>
);
}

// Has child
if (children) {
return renderContent;
if (!disabled) {
return <>{clickableEl}</>;
}

// Default
return (
<Link
component={RouterLink}
href={path ?? '/'}
underline="none"
color="inherit"
sx={{
...(disabled && {
cursor: 'default',
}),
}}
>
{renderContent}
</Link>
<>
<Box
component="span"
ref={anchorRef}
onMouseEnter={handleOpen}
onMouseLeave={handleClose}
sx={{ display: 'inline-flex', width: '100%' }}
>
{clickableEl}
</Box>

<Popover
open={popoverOpen}
anchorEl={anchorRef.current}
anchorOrigin={{ vertical: 'center', horizontal: 'right' }}
transformOrigin={{ vertical: 'center', horizontal: 'left' }}
slotProps={{
paper: {
onMouseEnter: handleOpen,
onMouseLeave: handleClose,
sx: {
backgroundColor: 'rgba(0,0,0,0.6)',
padding: '8px 8px',
borderRadius: 1,
...(popoverOpen && { pointerEvents: 'auto' }),
},
},
}}
sx={{ pointerEvents: 'none' }}
>
<Stack spacing={1} direction="row" alignItems="center">
<small>{comingSoonText}</small>
</Stack>
</Popover>
</>
);
}
Loading
Loading