Skip to content

Commit 0b2fed0

Browse files
authored
Merge pull request #810 from WatchItDev/next
Next
2 parents 0ceabb4 + 17b54f3 commit 0b2fed0

24 files changed

Lines changed: 1140 additions & 612 deletions

index.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,7 @@
146146

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

149+
<script>performance.mark('html:start');</script>
149150
<script type="module" src="/src/index.tsx"></script>
150151
<script>
151152
function showApp() {

src/components/app-ready.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,12 @@ export default function AppReady() {
44
useEffect(() => {
55
requestAnimationFrame(() =>
66
requestAnimationFrame(() => {
7+
performance.mark('react:first-paint');
8+
performance.measure('html→entry','html:start','entry:begin');
9+
performance.measure('entry→react','entry:begin','react:first-paint');
10+
console.table(performance.getEntriesByType('measure').map(m=>({
11+
name:m.name, ms: Math.round(m.duration)
12+
})));
713
window.dispatchEvent(new Event('app:ready'));
814
})
915
);

src/components/leave-tip-card.tsx

Lines changed: 73 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,17 @@
1-
import { FC, useState } from 'react';
1+
import { FC, useEffect, useMemo, useState } from 'react';
22
import { Typography, Box, TextField, Stack, Paper } from '@mui/material';
33
import Card from '@mui/material/Card';
44
import CardContent from '@mui/material/CardContent';
55
import LoadingButton from '@mui/lab/LoadingButton';
6+
import { useDispatch } from 'react-redux';
7+
import { openLoginModal } from '@redux/auth';
8+
import { useAuth } from '@src/hooks/use-auth.ts';
9+
import { useTransfer } from '@src/hooks/protocol/use-transfer.ts';
610
import { Post } from '@src/graphql/generated/graphql.ts';
11+
import { notifyError, notifySuccess } from '@src/libs/notifications/internal-notifications.ts';
12+
import { ERRORS } from '@src/libs/notifications/errors.ts';
13+
import { SUCCESS } from '@src/libs/notifications/success.ts';
14+
import { GetTipsByBakerForPostDocument, useCreateTipMutation } from '@src/graphql/generated/hooks.tsx';
715

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

1422
export const LeaveTipCard: FC<{ post: Post }> = ({ post }) => {
23+
const dispatch = useDispatch();
24+
const { session } = useAuth();
25+
const { transfer, loading: transferLoading, error } = useTransfer();
26+
const [createTip] = useCreateTipMutation();
27+
1528
const [selectedTip, setSelectedTip] = useState('10');
1629
const [customTip, setCustomTip] = useState('');
17-
const [successMessage, setSuccessMessage] = useState(false);
30+
31+
useEffect(() => {
32+
if (error) {
33+
notifyError(error as ERRORS);
34+
}
35+
}, [error]);
36+
37+
const amount = useMemo(() => {
38+
// Si hay custom, usa custom. Sino, usa el seleccionado.
39+
const raw = selectedTip ? selectedTip : customTip;
40+
const parsed = Number(raw);
41+
// Evita NaN o negativos
42+
return Number.isFinite(parsed) && parsed > 0 ? parsed : 0;
43+
}, [selectedTip, customTip]);
44+
45+
const recipient = post?.author?.address ?? '';
1846

1947
const handleTipChange = (value: string) => {
2048
setSelectedTip(value);
2149
setCustomTip('');
22-
setSuccessMessage(false);
2350
};
2451

2552
const handleCustomTipChange = (event: React.ChangeEvent<HTMLInputElement>) => {
2653
setSelectedTip('');
2754
setCustomTip(event.target.value);
28-
setSuccessMessage(false);
2955
};
3056

57+
const handleSendTip = async () => {
58+
if (!session?.authenticated) return dispatch(openLoginModal());
59+
if (!recipient || amount <= 0) return;
60+
61+
try {
62+
const pTransfer = transfer({ amount, recipient });
63+
64+
const pCreateTip = pTransfer.then(() =>
65+
createTip({
66+
variables: {
67+
input: {
68+
postId: post.id,
69+
creator: recipient,
70+
amount,
71+
txHash: null,
72+
message: 'tip',
73+
},
74+
},
75+
refetchQueries: [
76+
{ query: GetTipsByBakerForPostDocument, variables: { postId: post.id } },
77+
],
78+
awaitRefetchQueries: true,
79+
})
80+
);
81+
82+
await Promise.all([pTransfer, pCreateTip]);
83+
84+
notifySuccess(SUCCESS.TIP_CREATED_SUCCESSFULLY);
85+
} catch (e) {
86+
console.error('Transfer error:', e);
87+
}
88+
};
89+
90+
const isDisabled = transferLoading || amount <= 0 || !recipient;
91+
3192
return (
3293
<Card sx={{ width: '100%', maxWidth: { lg: 400 }, margin: 'auto', backgroundColor: '#2B2D31' }}>
3394
<CardContent>
@@ -37,6 +98,7 @@ export const LeaveTipCard: FC<{ post: Post }> = ({ post }) => {
3798
<Typography variant="body2" color="textSecondary" sx={{ mb: 3 }}>
3899
Choose an amount to leave a tip and support the content you love.
39100
</Typography>
101+
40102
<Stack spacing={2}>
41103
<Stack spacing={2} direction="row">
42104
{tipOptions.map((option) => (
@@ -56,56 +118,39 @@ export const LeaveTipCard: FC<{ post: Post }> = ({ post }) => {
56118
'&:hover': { opacity: 1 },
57119
}}
58120
>
59-
<Typography variant="body1" fontWeight="bold" align={'center'}>
121+
<Typography variant="body1" fontWeight="bold" align="center">
60122
{option.title}
61123
</Typography>
62-
<Typography
63-
variant="subtitle2"
64-
align={'center'}
65-
style={{
66-
color: 'text.secondary',
67-
fontSize: '0.7rem',
68-
}}
69-
>
124+
<Typography variant="subtitle2" align="center" sx={{ fontSize: '0.7rem' }}>
70125
MMC
71126
</Typography>
72127
</Paper>
73128
))}
74129
</Stack>
130+
75131
<Box>
76132
<TextField
77133
type="number"
78134
placeholder="Enter custom tip in MMC"
79135
fullWidth
80136
value={customTip}
81137
onChange={handleCustomTipChange}
82-
InputProps={{
83-
inputProps: { min: 1 },
84-
}}
138+
InputProps={{ inputProps: { min: 1 } }}
85139
/>
86140
</Box>
87141
</Stack>
88142

89143
<Stack direction="row" justifyContent="center" sx={{ mt: 4 }}>
90144
<LoadingButton
91145
variant="contained"
92-
disabled={true}
93146
sx={{ width: '100%', py: 1.5 }}
94-
loading={false}
147+
loading={transferLoading}
148+
disabled={isDisabled}
149+
onClick={handleSendTip}
95150
>
96151
Leave a Tip
97152
</LoadingButton>
98153
</Stack>
99-
100-
<Typography variant="body2" color="text.secondary" align="center" sx={{ mt: 2 }}>
101-
This feature is coming in the next release!
102-
</Typography>
103-
104-
{successMessage && (
105-
<Typography variant="body2" color="success.main" align="center" sx={{ mt: 2 }}>
106-
Tip sent successfully! Thank you for your support.
107-
</Typography>
108-
)}
109154
</CardContent>
110155
</Card>
111156
);

src/components/nav-section/vertical/nav-item.tsx

Lines changed: 89 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1+
import { useRef, useState, useCallback } from 'react';
12
// @mui
23
import Box from '@mui/material/Box';
34
import Link from '@mui/material/Link';
4-
import Tooltip from '@mui/material/Tooltip';
5+
import Popover from '@mui/material/Popover';
6+
import Stack from '@mui/material/Stack';
57
import ListItemText from '@mui/material/ListItemText';
68
// routes
79
import { RouterLink } from '@src/routes/components';
@@ -18,18 +20,26 @@ type Props = NavItemProps & {
1820
};
1921

2022
export default function NavItem({
21-
item,
22-
open,
23-
depth,
24-
active,
25-
config,
26-
externalLink,
27-
...other
28-
}: Props) {
23+
item,
24+
open,
25+
depth,
26+
active,
27+
config,
28+
externalLink,
29+
...other
30+
}: Props) {
2931
const { title, path, icon, info, children, disabled, caption, roles } = item;
3032

3133
const subItem = depth !== 1;
3234

35+
const anchorRef = useRef<HTMLSpanElement | null>(null);
36+
const [popoverOpen, setPopoverOpen] = useState(false);
37+
38+
const handleOpen = useCallback(() => setPopoverOpen(true), []);
39+
const handleClose = useCallback(() => setPopoverOpen(false), []);
40+
41+
const comingSoonText = caption || '✨ Coming soon';
42+
3343
const renderContent = (
3444
<StyledItem
3545
disableGutters
@@ -52,13 +62,7 @@ export default function NavItem({
5262
{!(config.hiddenLabel && !subItem) && (
5363
<ListItemText
5464
primary={title}
55-
secondary={
56-
caption ? (
57-
<Tooltip title={caption} placement="top-start">
58-
<span>{caption}</span>
59-
</Tooltip>
60-
) : null
61-
}
65+
secondary={disabled ? null : undefined}
6266
primaryTypographyProps={{
6367
noWrap: true,
6468
typography: 'body2',
@@ -95,44 +99,81 @@ export default function NavItem({
9599
return null;
96100
}
97101

98-
// External link
99-
if (externalLink)
100-
return (
101-
<Link
102-
href={path}
103-
target="_blank"
104-
rel="noopener"
105-
underline="none"
106-
color="inherit"
107-
sx={{
108-
...(disabled && {
109-
cursor: 'default',
110-
}),
111-
}}
112-
>
102+
const commonLinkProps = {
103+
underline: 'none' as const,
104+
color: 'inherit',
105+
'aria-disabled': disabled ? 'true' : undefined,
106+
onClick: (e: React.MouseEvent) => {
107+
if (disabled) {
108+
e.preventDefault();
109+
e.stopPropagation();
110+
}
111+
},
112+
sx: {
113+
...(disabled && {
114+
cursor: 'not-allowed',
115+
pointerEvents: 'auto',
116+
}),
117+
},
118+
};
119+
120+
let clickableEl: React.ReactNode;
121+
122+
if (externalLink) {
123+
clickableEl = (
124+
<Link href={path} target="_blank" rel="noopener" {...commonLinkProps}>
125+
{renderContent}
126+
</Link>
127+
);
128+
} else if (children) {
129+
clickableEl = renderContent;
130+
} else {
131+
clickableEl = (
132+
<Link component={RouterLink} href={path ?? '/'} {...commonLinkProps}>
113133
{renderContent}
114134
</Link>
115135
);
136+
}
116137

117-
// Has child
118-
if (children) {
119-
return renderContent;
138+
if (!disabled) {
139+
return <>{clickableEl}</>;
120140
}
121141

122-
// Default
123142
return (
124-
<Link
125-
component={RouterLink}
126-
href={path ?? '/'}
127-
underline="none"
128-
color="inherit"
129-
sx={{
130-
...(disabled && {
131-
cursor: 'default',
132-
}),
133-
}}
134-
>
135-
{renderContent}
136-
</Link>
143+
<>
144+
<Box
145+
component="span"
146+
ref={anchorRef}
147+
onMouseEnter={handleOpen}
148+
onMouseLeave={handleClose}
149+
sx={{ display: 'inline-flex', width: '100%' }}
150+
>
151+
{clickableEl}
152+
</Box>
153+
154+
<Popover
155+
open={popoverOpen}
156+
anchorEl={anchorRef.current}
157+
anchorOrigin={{ vertical: 'center', horizontal: 'right' }}
158+
transformOrigin={{ vertical: 'center', horizontal: 'left' }}
159+
slotProps={{
160+
paper: {
161+
onMouseEnter: handleOpen,
162+
onMouseLeave: handleClose,
163+
sx: {
164+
backgroundColor: 'rgba(0,0,0,0.6)',
165+
padding: '8px 8px',
166+
borderRadius: 1,
167+
...(popoverOpen && { pointerEvents: 'auto' }),
168+
},
169+
},
170+
}}
171+
sx={{ pointerEvents: 'none' }}
172+
>
173+
<Stack spacing={1} direction="row" alignItems="center">
174+
<small>{comingSoonText}</small>
175+
</Stack>
176+
</Popover>
177+
</>
137178
);
138179
}

0 commit comments

Comments
 (0)