Merge branch 'main' into bounty

This commit is contained in:
Austin Chen 2022-08-15 16:37:35 -07:00
commit 97c0a5cfa8
17 changed files with 485 additions and 1028 deletions

View File

@ -1,32 +1,33 @@
<!DOCTYPE html> <!DOCTYPE html>
<html <html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml"
xmlns="http://www.w3.org/1999/xhtml" xmlns:o="urn:schemas-microsoft-com:office:office">
xmlns:v="urn:schemas-microsoft-com:vml"
xmlns:o="urn:schemas-microsoft-com:office:office"
>
<head> <head>
<title>Welcome to Manifold Markets</title> <title>Welcome to Manifold Markets!</title>
<!--[if !mso]><!--> <!--[if !mso]><!-->
<meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta http-equiv="X-UA-Compatible" content="IE=edge">
<!--<![endif]--> <!--<![endif]-->
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1" /> <meta name="viewport" content="width=device-width,initial-scale=1">
<style type="text/css"> <style type="text/css">
#outlook a { #outlook a {
padding: 0; padding: 0;
} }
body { body {
margin: 0; margin: 0;
padding: 0; padding: 0;
-webkit-text-size-adjust: 100%; -webkit-text-size-adjust: 100%;
-ms-text-size-adjust: 100%; -ms-text-size-adjust: 100%;
} }
table, table,
td { td {
border-collapse: collapse; border-collapse: collapse;
mso-table-lspace: 0pt; mso-table-lspace: 0pt;
mso-table-rspace: 0pt; mso-table-rspace: 0pt;
} }
img { img {
border: 0; border: 0;
height: auto; height: auto;
@ -35,6 +36,7 @@
text-decoration: none; text-decoration: none;
-ms-interpolation-mode: bicubic; -ms-interpolation-mode: bicubic;
} }
p { p {
display: block; display: block;
margin: 13px 0; margin: 13px 0;
@ -52,9 +54,7 @@
<![endif]--> <![endif]-->
<!--[if lte mso 11]> <!--[if lte mso 11]>
<style type="text/css"> <style type="text/css">
.mj-outlook-group-fix { .mj-outlook-group-fix { width:100% !important; }
width: 100% !important;
}
</style> </style>
<![endif]--> <![endif]-->
<style type="text/css"> <style type="text/css">
@ -63,10 +63,6 @@
width: 100% !important; width: 100% !important;
max-width: 100%; max-width: 100%;
} }
.mj-column-per-50 {
width: 50% !important;
max-width: 50%;
}
} }
</style> </style>
<style media="screen and (min-width:480px)"> <style media="screen and (min-width:480px)">
@ -74,615 +70,139 @@
width: 100% !important; width: 100% !important;
max-width: 100%; max-width: 100%;
} }
.moz-text-html .mj-column-per-50 {
width: 50% !important;
max-width: 50%;
}
</style> </style>
<style type="text/css"> <style type="text/css">
[owa] .mj-column-per-100 { [owa] .mj-column-per-100 {
width: 100% !important; width: 100% !important;
max-width: 100%; max-width: 100%;
} }
[owa] .mj-column-per-50 {
width: 50% !important;
max-width: 50%;
}
</style> </style>
<style type="text/css"> <style type="text/css">
@media only screen and (max-width:480px) { @media only screen and (max-width:480px) {
table.mj-full-width-mobile { table.mj-full-width-mobile {
width: 100% !important; width: 100% !important;
} }
td.mj-full-width-mobile { td.mj-full-width-mobile {
width: auto !important; width: auto !important;
} }
} }
</style> </style>
</head> </head>
<body style="word-spacing: normal; background-color: #f4f4f4">
<div style="background-color: #f4f4f4"> <body style="word-spacing:normal;background-color:#F4F4F4;">
<div style="background-color:#F4F4F4;">
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:600px;" width="600" bgcolor="#ffffff" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]--> <!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:600px;" width="600" bgcolor="#ffffff" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
<div <div style="background:#ffffff;background-color:#ffffff;margin:0px auto;max-width:600px;">
style=" <table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
background: #ffffff; style="background:#ffffff;background-color:#ffffff;width:100%;">
background-color: #ffffff;
margin: 0px auto;
max-width: 600px;
"
>
<table
align="center"
border="0"
cellpadding="0"
cellspacing="0"
role="presentation"
style="background: #ffffff; background-color: #ffffff; width: 100%"
>
<tbody> <tbody>
<tr> <tr>
<td <td
style=" style="direction:ltr;font-size:0px;padding:0px 0px 0px 0px;padding-bottom:0px;padding-left:0px;padding-right:0px;padding-top:0px;text-align:center;">
direction: ltr;
font-size: 0px;
padding: 0px 0px 0px 0px;
padding-bottom: 0px;
padding-left: 0px;
padding-right: 0px;
padding-top: 0px;
text-align: center;
"
>
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:600px;" ><![endif]--> <!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:600px;" ><![endif]-->
<div class="mj-column-per-100 mj-outlook-group-fix"
style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;"
width="100%">
<tbody>
<tr>
<td align="center"
style="font-size:0px;padding:0px 25px 0px 25px;padding-top:0px;padding-right:25px;padding-bottom:0px;padding-left:25px;word-break:break-word;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
style="border-collapse:collapse;border-spacing:0px;">
<tbody>
<tr>
<td style="width:550px;"><a href="https://manifold.markets/home" target="_blank"><img
alt="" height="auto" src="https://03jlj.mjt.lu/img/03jlj/b/urx/sjtz.gif"
style="border:none;display:block;outline:none;text-decoration:none;height:auto;width:100%;font-size:13px;"
width="550"></a></td>
</tr>
</tbody>
</table>
</td>
</tr>
<tr>
<td align="left"
style="font-size:0px;padding:10px 25px;padding-top:0px;padding-bottom:0px;word-break:break-word;">
<div <div
class="mj-column-per-100 mj-outlook-group-fix" style="font-family:Arial, sans-serif;font-size:18px;letter-spacing:normal;line-height:1;text-align:left;color:#000000;">
style=" <p class="text-build-content"
font-size: 0px; style="line-height: 24px; margin: 10px 0; margin-top: 10px; margin-bottom: 10px;"
text-align: left; data-testid="4XoHRGw1Y"><span
direction: ltr; style="color:#000000;font-family:Arial, Helvetica, sans-serif;font-size:18px;">
display: inline-block; Hi {{name}},</span></p>
vertical-align: top;
width: 100%;
"
>
<table
border="0"
cellpadding="0"
cellspacing="0"
role="presentation"
style="vertical-align: top"
width="100%"
>
<tbody>
<tr>
<td
align="center"
style="
font-size: 0px;
padding: 0px 25px 0px 25px;
padding-top: 0px;
padding-right: 25px;
padding-bottom: 0px;
padding-left: 25px;
word-break: break-word;
"
>
<table
border="0"
cellpadding="0"
cellspacing="0"
role="presentation"
style="
border-collapse: collapse;
border-spacing: 0px;
"
>
<tbody>
<tr>
<td style="width: 550px">
<a
href="https://manifold.markets/home"
target="_blank"
><img
alt=""
height="auto"
src="https://03jlj.mjt.lu/img/03jlj/b/urx/sjtz.gif"
style="
border: none;
display: block;
outline: none;
text-decoration: none;
height: auto;
width: 100%;
font-size: 13px;
"
width="550"
/></a>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</div> </div>
<!--[if mso | IE]></td></tr></table><![endif]-->
</td> </td>
</tr> </tr>
</tbody> <tr>
</table> <td align="left"
style="font-size:0px;padding:10px 25px;padding-top:0px;padding-bottom:0px;word-break:break-word;">
<div
style="font-family:Arial, sans-serif;font-size:18px;letter-spacing:normal;line-height:1;text-align:left;color:#000000;">
<p class="text-build-content"
style="line-height: 24px; margin: 10px 0; margin-top: 10px; margin-bottom: 10px;"
data-testid="4XoHRGw1Y"><span
style="color:#000000;font-family:Arial, Helvetica, sans-serif;font-size:18px;">
Welcome! Manifold Markets is a play-money prediction market platform where you can bet on
anything, from elections to Elon Musk to scientific papers to the NBA. </span></p>
</div> </div>
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:600px;" width="600" bgcolor="#ffffff" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]--> </td>
<div </tr>
style="
background: #ffffff;
background-color: #ffffff;
margin: 0px auto;
max-width: 600px;
"
>
<table
align="center"
border="0"
cellpadding="0"
cellspacing="0"
role="presentation"
style="background: #ffffff; background-color: #ffffff; width: 100%"
>
<tbody>
<tr> <tr>
<td <td align="left"
style=" style="font-size:0px;padding:10px 25px;padding-top:0px;padding-bottom:0px;word-break:break-word;">
direction: ltr;
font-size: 0px;
padding: 0px 0px 0px 0px;
padding-bottom: 0px;
padding-left: 0px;
padding-right: 0px;
padding-top: 0px;
text-align: center;
"
>
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:300px;" ><![endif]-->
<div <div
class="mj-column-per-50 mj-outlook-group-fix" style="font-family:Arial, sans-serif;font-size:18px;letter-spacing:normal;line-height:1;text-align:left;color:#000000;">
style=" <p class="text-build-content"
font-size: 0px; style="line-height: 24px; margin: 10px 0; margin-top: 10px; margin-bottom: 10px;"
text-align: left; data-testid="4XoHRGw1Y"><span
direction: ltr; style="color:#000000;font-family:Arial, Helvetica, sans-serif;font-size:18px;">
display: inline-block;
vertical-align: top;
width: 100%;
"
>
<table
border="0"
cellpadding="0"
cellspacing="0"
role="presentation"
style="vertical-align: top"
width="100%"
>
<tbody>
<tr>
<td
align="left"
style="
background: #ffffff;
font-size: 0px;
padding: 0px 15px 0px 50px;
padding-top: 0px;
padding-right: 15px;
padding-bottom: 0px;
padding-left: 50px;
word-break: break-word;
"
>
<div
style="
font-family: Arial, sans-serif;
font-size: 18px;
letter-spacing: normal;
line-height: 1;
text-align: left;
color: #000000;
"
>
<p
class="text-build-content"
data-testid="e885nY30eE"
style="margin: 10px 0; margin-top: 10px"
>
<span
style="
font-family: Arial, sans-serif;
font-size: 18px;
"
><b
>Hi {{name}}, thanks for joining Manifold
Markets!</b
></span
><br /><br /><span
style="
font-family: Arial, sans-serif;
font-size: 18px;
"
>We can&#39;t wait to see what questions you
will ask!</span
>
</p>
<p
class="text-build-content"
style="line-height: 23px; margin: 10px 0"
data-testid="e885nY30eE"
>
<span
style="
font-family: Arial, sans-serif;
font-size: 18px;
"
>As a gift M$1000 has been credited to your
account - the equivalent of 10 USD.
</span> </span>
</p> </p>
<p
class="text-build-content"
style="
line-height: 23px;
margin: 10px 0;
margin-bottom: 10px;
"
data-testid="e885nY30eE"
>
<span
style="
font-family: Arial, sans-serif;
font-size: 18px;
"
>Click the buttons to see what you can do with
it!</span
>
</p>
</div> </div>
</td> </td>
</tr> </tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td><td class="" style="vertical-align:top;width:300px;" ><![endif]-->
<div
class="mj-column-per-50 mj-outlook-group-fix"
style="
font-size: 0px;
text-align: left;
direction: ltr;
display: inline-block;
vertical-align: top;
width: 100%;
"
>
<table
border="0"
cellpadding="0"
cellspacing="0"
role="presentation"
style="vertical-align: top"
width="100%"
>
<tbody>
<tr> <tr>
<td <td>
align="center" <p></p>
style=" </td>
font-size: 0px; </tr>
padding: 0px 15px 0px 0px;
padding-top: 0px;
padding-right: 15px;
padding-bottom: 0px;
padding-left: 0px;
word-break: break-word;
"
>
<table
border="0"
cellpadding="0"
cellspacing="0"
role="presentation"
style="
border-collapse: collapse;
border-spacing: 0px;
"
>
<tbody>
<tr> <tr>
<td style="width: 285px"> <td align="center">
<a <table cellspacing="0" cellpadding="0">
href="https://manifold.markets/home" <tr>
target="_blank" <td>
> <table cellspacing="0" cellpadding="0">
<img <tr>
alt="" <td style="border-radius: 2px;" bgcolor="#4337c9">
height="auto" <a href="https://manifold.markets" target="_blank"
src="https://03jlj.mjt.lu/img/03jlj/b/urx/1rsm.png" style="padding: 12px 16px; border: 1px solid #4337c9;border-radius: 16px;font-family: Helvetica, Arial, sans-serif;font-size: 24px; color: #ffffff;text-decoration: none;font-weight:bold;display: inline-block;">
style=" Explore markets
border: none;
display: block;
outline: none;
text-decoration: none;
height: auto;
width: 100%;
font-size: 13px;
"
width="285"
/>
</a> </a>
</td> </td>
</tr> </tr>
</tbody> </table>
</td>
</tr>
</table> </table>
</td> </td>
</tr> </tr>
<tr> <tr>
<td <td align="left"
align="center" style="font-size:0px;padding:15px 25px 0px 25px;padding-top:15px;padding-right:25px;padding-bottom:0px;padding-left:25px;word-break:break-word;">
style="
font-size: 0px;
padding: 0px 15px 0px 0px;
padding-top: 0px;
padding-right: 15px;
padding-bottom: 0px;
padding-left: 0px;
word-break: break-word;
"
>
<table
border="0"
cellpadding="0"
cellspacing="0"
role="presentation"
style="
border-collapse: collapse;
border-spacing: 0px;
"
>
<tbody>
<tr>
<td style="width: 285px">
<a
href="https://manifold.markets/create"
target="_blank"
>
<img
alt=""
height="auto"
src="https://03jlj.mjt.lu/img/03jlj/b/urx/1rs2.png"
style="
border: none;
display: block;
outline: none;
text-decoration: none;
height: auto;
width: 100%;
font-size: 13px;
"
width="285"
/>
</a>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
<tr>
<td
align="center"
style="
font-size: 0px;
padding: 0px 15px 0px 0px;
padding-top: 0px;
padding-right: 15px;
padding-bottom: 0px;
padding-left: 0px;
word-break: break-word;
"
>
<table
border="0"
cellpadding="0"
cellspacing="0"
role="presentation"
style="
border-collapse: collapse;
border-spacing: 0px;
"
>
<tbody>
<tr>
<td style="width: 285px">
<a
href="https://manifold.markets/charity"
target="_blank"
>
<img
alt=""
height="auto"
src="https://03jlj.mjt.lu/img/03jlj/b/urx/1rsp.png"
style="
border: none;
display: block;
outline: none;
text-decoration: none;
height: auto;
width: 100%;
font-size: 13px;
"
width="285"
/>
</a>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:600px;" width="600" bgcolor="#ffffff" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
<div <div
style=" style="font-family:Arial, sans-serif;font-size:18px;letter-spacing:normal;line-height:1;text-align:left;color:#000000;">
background: #ffffff; <p class="text-build-content" style="line-height: 23px; margin: 10px 0; margin-top: 10px;"
background-color: #ffffff; data-testid="3Q8BP69fq"></a></li>
margin: 0px auto; </ul>
max-width: 600px; <p class="text-build-content" data-testid="3Q8BP69fq" style="margin: 10px 0;">&nbsp;</p>
" <p class="text-build-content" data-testid="3Q8BP69fq" style="margin: 10px 0;"><span
> style="color:#000000;font-family:Arial;font-size:18px;">Cheers,</span></p>
<table <p class="text-build-content" data-testid="3Q8BP69fq" style="margin: 10px 0;"><span
align="center" style="color:#000000;font-family:Arial;font-size:18px;">David from Manifold</span></p>
border="0" <p class="text-build-content" data-testid="3Q8BP69fq"
cellpadding="0" style="margin: 10px 0; margin-bottom: 10px;">&nbsp;</p>
cellspacing="0"
role="presentation"
style="background: #ffffff; background-color: #ffffff; width: 100%"
>
<tbody>
<tr>
<td
style="
direction: ltr;
font-size: 0px;
padding: 20px 0px 0px 0px;
padding-bottom: 0px;
padding-left: 0px;
padding-right: 0px;
padding-top: 20px;
text-align: center;
"
>
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:600px;" ><![endif]-->
<div
class="mj-column-per-100 mj-outlook-group-fix"
style="
font-size: 0px;
text-align: left;
direction: ltr;
display: inline-block;
vertical-align: top;
width: 100%;
"
>
<table
border="0"
cellpadding="0"
cellspacing="0"
role="presentation"
style="vertical-align: top"
width="100%"
>
<tbody>
<tr>
<td
align="left"
style="
font-size: 0px;
padding: 10px 25px;
padding-top: 0px;
padding-bottom: 0px;
word-break: break-word;
"
>
<div
style="
font-family: Arial, sans-serif;
font-size: 18px;
letter-spacing: normal;
line-height: 1;
text-align: left;
color: #000000;
"
>
<p
class="text-build-content"
data-testid="3Q8BP69fq"
style="margin: 10px 0; margin-top: 10px"
>
<span
style="
color: #55575d;
font-family: Arial, Helvetica, sans-serif;
font-size: 18px;
"
>I</span
><span
style="
color: #000000;
font-family: Arial, Helvetica, sans-serif;
font-size: 18px;
"
>f you have any questions or feedback we&#39;d
love to hear from you in our </span
><a
class="link-build-content"
style="color: inherit; text-decoration: none"
target="_blank"
href="https://discord.gg/VARzUpyCSa"
><span
style="
color: #1435b8;
font-family: Arial;
font-size: 18px;
"
><u>Discord server!</u></span
></a
>
</p>
<p
class="text-build-content"
data-testid="3Q8BP69fq"
style="margin: 10px 0"
></p>
<p
class="text-build-content"
data-testid="3Q8BP69fq"
style="margin: 10px 0"
>
<span
style="
color: #000000;
font-family: Arial;
font-size: 18px;
"
>Looking forward to seeing you,</span
>
</p>
<p
class="text-build-content"
data-testid="3Q8BP69fq"
style="margin: 10px 0"
>
<span
style="
color: #000000;
font-family: Arial;
font-size: 18px;
"
>David from Manifold</span
>
</p>
<br />
<p
class="text-build-content"
data-testid="3Q8BP69fq"
style="margin: 10px 0; margin-bottom: 10px"
></p>
</div> </div>
</td> </td>
</tr> </tr>
@ -696,112 +216,54 @@
</table> </table>
</div> </div>
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:600px;" width="600" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]--> <!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:600px;" width="600" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
<div style="margin: 0px auto; max-width: 600px"> <div style="margin:0px auto;max-width:600px;">
<table <table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
align="center"
border="0"
cellpadding="0"
cellspacing="0"
role="presentation"
style="width: 100%"
>
<tbody> <tbody>
<tr> <tr>
<td <td style="direction:ltr;font-size:0px;padding:0 0 20px 0;text-align:center;">
style=" <!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:600px;" ><![endif]-->
direction: ltr; <div class="mj-column-per-100 mj-outlook-group-fix"
font-size: 0px; style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
padding: 0 0 20px 0; <table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;"
text-align: center; width="100%">
" <tbody>
></td> <tr>
</tr> <td align="center" style="font-size:0px;padding:0px;word-break:break-word;">
</tbody> <table border="0" cellpadding="0" cellspacing="0" role="presentation"
</table> style="border-collapse:collapse;border-spacing:0px;">
</div> </div>
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:600px;" width="600" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]--> <!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:600px;" width="600" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
<div style="margin: 0px auto; max-width: 600px"> <div style="margin:0px auto;max-width:600px;">
<table <table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
align="center" style="width:100%;">
border="0"
cellpadding="0"
cellspacing="0"
role="presentation"
style="width: 100%"
>
<tbody> <tbody>
<tr> <tr>
<td <td style="direction:ltr;font-size:0px;padding:20px 0px 20px 0px;text-align:center;">
style="
direction: ltr;
font-size: 0px;
padding: 20px 0px 20px 0px;
text-align: center;
"
>
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:600px;" ><![endif]--> <!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:600px;" ><![endif]-->
<div <div class="mj-column-per-100 mj-outlook-group-fix"
class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
style=" <table border="0" cellpadding="0" cellspacing="0" role="presentation" width="100%">
font-size: 0px;
text-align: left;
direction: ltr;
display: inline-block;
vertical-align: top;
width: 100%;
"
>
<table
border="0"
cellpadding="0"
cellspacing="0"
role="presentation"
width="100%"
>
<tbody> <tbody>
<tr> <tr>
<td style="vertical-align: top; padding: 0"> <td style="vertical-align:top;padding:0;">
<table <table border="0" cellpadding="0" cellspacing="0" role="presentation" width="100%">
border="0"
cellpadding="0"
cellspacing="0"
role="presentation"
width="100%"
>
<tbody> <tbody>
<tr> <tr>
<td <td align="center"
align="center" style="font-size:0px;padding:10px 25px;padding-top:0px;padding-bottom:0px;word-break:break-word;">
style="
font-size: 0px;
padding: 10px 25px;
padding-top: 0px;
padding-bottom: 0px;
word-break: break-word;
"
>
<div <div
style=" style="font-family:Arial, sans-serif;font-size:11px;letter-spacing:normal;line-height:22px;text-align:center;color:#000000;">
font-family: Arial, sans-serif; <p style="margin: 10px 0;">This e-mail has been sent to {{name}}, <a
font-size: 11px; href="{{unsubscribeLink}}” style=" color:inherit;text-decoration:none;"
letter-spacing: normal; target="_blank">click here to unsubscribe</a>.</p>
line-height: 22px; </div>
text-align: center; </td>
color: #000000; </tr>
" <tr>
> <td align="center"
<p style="margin: 10px 0"> style="font-size:0px;padding:10px 25px;padding-top:0px;padding-bottom:0px;word-break:break-word;">
This e-mail has been sent to {{name}}, <div
<a style="font-family:Arial, sans-serif;font-size:11px;letter-spacing:normal;line-height:22px;text-align:center;color:#000000;">
href="{{unsubscribeLink}}"
style="
color: inherit;
text-decoration: none;
"
target="_blank"
>click here to unsubscribe</a
>.
</p>
</div> </div>
</td> </td>
</tr> </tr>
@ -821,4 +283,5 @@
<!--[if mso | IE]></td></tr></table><![endif]--> <!--[if mso | IE]></td></tr></table><![endif]-->
</div> </div>
</body> </body>
</html> </html>

View File

@ -30,7 +30,15 @@ const bodySchema = z.object({
const binarySchema = z.object({ const binarySchema = z.object({
outcome: z.enum(['YES', 'NO']), outcome: z.enum(['YES', 'NO']),
limitProb: z.number().gte(0.001).lte(0.999).optional(), limitProb: z
.number()
.gte(0.001)
.lte(0.999)
.refine(
(p) => Math.round(p * 100) === p * 100,
'limitProb must be in increments of 0.01 (i.e. whole percentage points)'
)
.optional(),
}) })
const freeResponseSchema = z.object({ const freeResponseSchema = z.object({

View File

@ -1,6 +1,6 @@
import Router from 'next/router' import Router from 'next/router'
import clsx from 'clsx' import clsx from 'clsx'
import { MouseEvent } from 'react' import { MouseEvent, useState } from 'react'
import { UserCircleIcon, UserIcon, UsersIcon } from '@heroicons/react/solid' import { UserCircleIcon, UserIcon, UsersIcon } from '@heroicons/react/solid'
export function Avatar(props: { export function Avatar(props: {
@ -10,7 +10,8 @@ export function Avatar(props: {
size?: number | 'xs' | 'sm' size?: number | 'xs' | 'sm'
className?: string className?: string
}) { }) {
const { username, avatarUrl, noLink, size, className } = props const { username, noLink, size, className } = props
const [avatarUrl, setAvatarUrl] = useState(props.avatarUrl)
const s = size == 'xs' ? 6 : size === 'sm' ? 8 : size || 10 const s = size == 'xs' ? 6 : size === 'sm' ? 8 : size || 10
const onClick = const onClick =
@ -35,6 +36,11 @@ export function Avatar(props: {
src={avatarUrl} src={avatarUrl}
onClick={onClick} onClick={onClick}
alt={username} alt={username}
onError={() => {
// If the image doesn't load, clear the avatarUrl to show the default
// Mostly for localhost, when getting a 403 from googleusercontent
setAvatarUrl('')
}}
/> />
) : ( ) : (
<UserCircleIcon <UserCircleIcon

View File

@ -4,11 +4,7 @@ import { SearchOptions } from '@algolia/client-search'
import { Contract } from 'common/contract' import { Contract } from 'common/contract'
import { User } from 'common/user' import { User } from 'common/user'
import { import { Sort, useQuery, useSort } from '../hooks/use-sort-and-query-params'
QuerySortOptions,
Sort,
useQueryAndSortParams,
} from '../hooks/use-sort-and-query-params'
import { import {
ContractHighlightOptions, ContractHighlightOptions,
ContractsGrid, ContractsGrid,
@ -24,11 +20,25 @@ import ContractSearchFirestore from 'web/pages/contract-search-firestore'
import { useMemberGroups } from 'web/hooks/use-group' import { useMemberGroups } from 'web/hooks/use-group'
import { NEW_USER_GROUP_SLUGS } from 'common/group' import { NEW_USER_GROUP_SLUGS } from 'common/group'
import { PillButton } from './buttons/pill-button' import { PillButton } from './buttons/pill-button'
import { sortBy } from 'lodash' import { debounce, sortBy } from 'lodash'
import { DEFAULT_CATEGORY_GROUPS } from 'common/categories' import { DEFAULT_CATEGORY_GROUPS } from 'common/categories'
import { Col } from './layout/col' import { Col } from './layout/col'
import { safeLocalStorage } from 'web/lib/util/local'
import clsx from 'clsx' import clsx from 'clsx'
// TODO: this obviously doesn't work with SSR, common sense would suggest
// that we should save things like this in cookies so the server has them
const MARKETS_SORT = 'markets_sort'
function setSavedSort(s: Sort) {
safeLocalStorage()?.setItem(MARKETS_SORT, s)
}
function getSavedSort() {
return safeLocalStorage()?.getItem(MARKETS_SORT) as Sort | null | undefined
}
const searchClient = algoliasearch( const searchClient = algoliasearch(
'GJQPAYENIF', 'GJQPAYENIF',
'75c28fc084a80e1129d427d470cf41a3' '75c28fc084a80e1129d427d470cf41a3'
@ -47,7 +57,6 @@ const sortOptions = [
{ label: 'Close date', value: 'close-date' }, { label: 'Close date', value: 'close-date' },
{ label: 'Resolve date', value: 'resolve-date' }, { label: 'Resolve date', value: 'resolve-date' },
] ]
export const DEFAULT_SORT = 'score'
type filter = 'personal' | 'open' | 'closed' | 'resolved' | 'all' type filter = 'personal' | 'open' | 'closed' | 'resolved' | 'all'
@ -68,7 +77,8 @@ type AdditionalFilter = {
export function ContractSearch(props: { export function ContractSearch(props: {
user?: User | null user?: User | null
querySortOptions?: { defaultFilter?: filter } & QuerySortOptions defaultSort?: Sort
defaultFilter?: filter
additionalFilter?: AdditionalFilter additionalFilter?: AdditionalFilter
highlightOptions?: ContractHighlightOptions highlightOptions?: ContractHighlightOptions
onContractClick?: (contract: Contract) => void onContractClick?: (contract: Contract) => void
@ -79,10 +89,13 @@ export function ContractSearch(props: {
hideQuickBet?: boolean hideQuickBet?: boolean
} }
headerClassName?: string headerClassName?: string
useQuerySortLocalStorage?: boolean
useQuerySortUrlParams?: boolean
}) { }) {
const { const {
user, user,
querySortOptions, defaultSort,
defaultFilter,
additionalFilter, additionalFilter,
onContractClick, onContractClick,
overrideGridClassName, overrideGridClassName,
@ -90,6 +103,8 @@ export function ContractSearch(props: {
cardHideOptions, cardHideOptions,
highlightOptions, highlightOptions,
headerClassName, headerClassName,
useQuerySortLocalStorage,
useQuerySortUrlParams,
} = props } = props
const [numPages, setNumPages] = useState(1) const [numPages, setNumPages] = useState(1)
@ -124,6 +139,7 @@ export function ContractSearch(props: {
setNumPages(results.nbPages) setNumPages(results.nbPages)
if (freshQuery) { if (freshQuery) {
setPages([newPage]) setPages([newPage])
window.scrollTo(0, 0)
} else { } else {
setPages((pages) => [...pages, newPage]) setPages((pages) => [...pages, newPage])
} }
@ -132,31 +148,33 @@ export function ContractSearch(props: {
} }
} }
const onSearchParametersChanged = useRef(
debounce((params) => {
searchParameters.current = params
performQuery(true)
}, 100)
).current
const contracts = pages const contracts = pages
.flat() .flat()
.filter((c) => !additionalFilter?.excludeContractIds?.includes(c.id)) .filter((c) => !additionalFilter?.excludeContractIds?.includes(c.id))
if (IS_PRIVATE_MANIFOLD || process.env.NEXT_PUBLIC_FIREBASE_EMULATE) { if (IS_PRIVATE_MANIFOLD || process.env.NEXT_PUBLIC_FIREBASE_EMULATE) {
return ( return <ContractSearchFirestore additionalFilter={additionalFilter} />
<ContractSearchFirestore
querySortOptions={querySortOptions}
additionalFilter={additionalFilter}
/>
)
} }
return ( return (
<Col className="h-full"> <Col className="h-full">
<ContractSearchControls <ContractSearchControls
className={headerClassName} className={headerClassName}
defaultSort={defaultSort}
defaultFilter={defaultFilter}
additionalFilter={additionalFilter} additionalFilter={additionalFilter}
hideOrderSelector={hideOrderSelector} hideOrderSelector={hideOrderSelector}
querySortOptions={querySortOptions} useQuerySortLocalStorage={useQuerySortLocalStorage}
useQuerySortUrlParams={useQuerySortUrlParams}
user={user} user={user}
onSearchParametersChanged={(params) => { onSearchParametersChanged={onSearchParametersChanged}
searchParameters.current = params
performQuery(true)
}}
/> />
<ContractsGrid <ContractsGrid
contracts={pages.length === 0 ? undefined : contracts} contracts={pages.length === 0 ? undefined : contracts}
@ -173,23 +191,40 @@ export function ContractSearch(props: {
function ContractSearchControls(props: { function ContractSearchControls(props: {
className?: string className?: string
defaultSort?: Sort
defaultFilter?: filter
additionalFilter?: AdditionalFilter additionalFilter?: AdditionalFilter
hideOrderSelector?: boolean hideOrderSelector?: boolean
onSearchParametersChanged: (params: SearchParameters) => void onSearchParametersChanged: (params: SearchParameters) => void
querySortOptions?: { defaultFilter?: filter } & QuerySortOptions useQuerySortLocalStorage?: boolean
useQuerySortUrlParams?: boolean
user?: User | null user?: User | null
}) { }) {
const { const {
className, className,
defaultSort,
defaultFilter,
additionalFilter, additionalFilter,
hideOrderSelector, hideOrderSelector,
onSearchParametersChanged, onSearchParametersChanged,
querySortOptions, useQuerySortLocalStorage,
useQuerySortUrlParams,
user, user,
} = props } = props
const { query, setQuery, sort, setSort } = const savedSort = useQuerySortLocalStorage ? getSavedSort() : null
useQueryAndSortParams(querySortOptions) const initialSort = savedSort ?? defaultSort ?? 'score'
const querySortOpts = { useUrl: !!useQuerySortUrlParams }
const [sort, setSort] = useSort(initialSort, querySortOpts)
const [query, setQuery] = useQuery('', querySortOpts)
const [filter, setFilter] = useState<filter>(defaultFilter ?? 'open')
const [pillFilter, setPillFilter] = useState<string | undefined>(undefined)
useEffect(() => {
if (useQuerySortLocalStorage) {
setSavedSort(sort)
}
}, [sort])
const follows = useFollows(user?.id) const follows = useFollows(user?.id)
const memberGroups = (useMemberGroups(user?.id) ?? []).filter( const memberGroups = (useMemberGroups(user?.id) ?? []).filter(
@ -208,12 +243,6 @@ function ContractSearchControls(props: {
const pillGroups: { name: string; slug: string }[] = const pillGroups: { name: string; slug: string }[] =
memberPillGroups.length > 0 ? memberPillGroups : DEFAULT_CATEGORY_GROUPS memberPillGroups.length > 0 ? memberPillGroups : DEFAULT_CATEGORY_GROUPS
const [filter, setFilter] = useState<filter>(
querySortOptions?.defaultFilter ?? 'open'
)
const [pillFilter, setPillFilter] = useState<string | undefined>(undefined)
const additionalFilters = [ const additionalFilters = [
additionalFilter?.creatorId additionalFilter?.creatorId
? `creatorId:${additionalFilter.creatorId}` ? `creatorId:${additionalFilter.creatorId}`

View File

@ -78,7 +78,7 @@ export function ContractCard(props: {
className className
)} )}
> >
<Col className="group relative flex-1 gap-3 py-4 pl-6"> <Col className="group relative flex-1 gap-3 py-4 pb-12 pl-6">
{onClick ? ( {onClick ? (
<a <a
className="absolute top-0 left-0 right-0 bottom-0" className="absolute top-0 left-0 right-0 bottom-0"
@ -106,7 +106,10 @@ export function ContractCard(props: {
/> />
</Link> </Link>
)} )}
<AvatarDetails contract={contract} /> <AvatarDetails
contract={contract}
className={'hidden md:inline-flex'}
/>
<p <p
className="break-words font-semibold text-indigo-700 group-hover:underline group-hover:decoration-indigo-400 group-hover:decoration-2" className="break-words font-semibold text-indigo-700 group-hover:underline group-hover:decoration-indigo-400 group-hover:decoration-2"
style={{ /* For iOS safari */ wordBreak: 'break-word' }} style={{ /* For iOS safari */ wordBreak: 'break-word' }}
@ -125,13 +128,19 @@ export function ContractCard(props: {
) : ( ) : (
<FreeResponseTopAnswer contract={contract} truncate="long" /> <FreeResponseTopAnswer contract={contract} truncate="long" />
))} ))}
<Row className={'absolute bottom-3 gap-2 md:gap-0'}>
<AvatarDetails
contract={contract}
short={true}
className={'block md:hidden'}
/>
<MiscDetails <MiscDetails
contract={contract} contract={contract}
showHotVolume={showHotVolume} showHotVolume={showHotVolume}
showTime={showTime} showTime={showTime}
hideGroupLink={hideGroupLink} hideGroupLink={hideGroupLink}
/> />
</Row>
</Col> </Col>
{showQuickBet ? ( {showQuickBet ? (
<QuickBet contract={contract} user={user} /> <QuickBet contract={contract} user={user} />

View File

@ -34,6 +34,7 @@ import { ContractGroupsList } from 'web/components/groups/contract-groups-list'
import { SiteLink } from 'web/components/site-link' import { SiteLink } from 'web/components/site-link'
import { groupPath } from 'web/lib/firebase/groups' import { groupPath } from 'web/lib/firebase/groups'
import { insertContent } from '../editor/utils' import { insertContent } from '../editor/utils'
import clsx from 'clsx'
export type ShowTime = 'resolve-date' | 'close-date' export type ShowTime = 'resolve-date' | 'close-date'
@ -83,9 +84,8 @@ export function MiscDetails(props: {
{!hideGroupLink && groupLinks && groupLinks.length > 0 && ( {!hideGroupLink && groupLinks && groupLinks.length > 0 && (
<SiteLink <SiteLink
href={groupPath(groupLinks[0].slug)} href={groupPath(groupLinks[0].slug)}
className="line-clamp-1 text-sm text-gray-400" className="truncate text-sm text-gray-400"
> >
<UserGroupIcon className="mx-1 mb-0.5 inline h-4 w-4 shrink-0" />
{groupLinks[0].name} {groupLinks[0].name}
</SiteLink> </SiteLink>
)} )}
@ -93,18 +93,24 @@ export function MiscDetails(props: {
) )
} }
export function AvatarDetails(props: { contract: Contract }) { export function AvatarDetails(props: {
const { contract } = props contract: Contract
className?: string
short?: boolean
}) {
const { contract, short, className } = props
const { creatorName, creatorUsername } = contract const { creatorName, creatorUsername } = contract
return ( return (
<Row className="items-center gap-2 text-sm text-gray-400"> <Row
className={clsx('items-center gap-2 text-sm text-gray-400', className)}
>
<Avatar <Avatar
username={creatorUsername} username={creatorUsername}
avatarUrl={contract.creatorAvatarUrl} avatarUrl={contract.creatorAvatarUrl}
size={6} size={6}
/> />
<UserLink name={creatorName} username={creatorUsername} /> <UserLink name={creatorName} username={creatorUsername} short={short} />
</Row> </Row>
) )
} }

View File

@ -106,11 +106,8 @@ export function CreatorContractsList(props: {
return ( return (
<ContractSearch <ContractSearch
user={user} user={user}
querySortOptions={{ defaultSort="newest"
defaultSort: 'newest', defaultFilter="all"
defaultFilter: 'all',
shouldLoadFromStorage: false,
}}
additionalFilter={{ additionalFilter={{
creatorId: creator.id, creatorId: creator.id,
}} }}

View File

@ -51,17 +51,10 @@ export function ShareModal(props: {
color="gradient" color="gradient"
className={'mb-2 flex max-w-xs self-center'} className={'mb-2 flex max-w-xs self-center'}
onClick={() => { onClick={() => {
if (window.navigator.share) {
window.navigator.share({
url: shareUrl,
title: contract.question,
})
} else {
copyToClipboard(shareUrl) copyToClipboard(shareUrl)
toast.success('Link copied!', { toast.success('Link copied!', {
icon: linkIcon, icon: linkIcon,
}) })
}
track('copy share link') track('copy share link')
}} }}
> >
@ -73,7 +66,7 @@ export function ShareModal(props: {
className="self-start" className="self-start"
tweetText={getTweetText(contract, shareUrl)} tweetText={getTweetText(contract, shareUrl)}
/> />
<ShareEmbedButton contract={contract} toastClassName={'-left-20'} /> <ShareEmbedButton contract={contract} />
<DuplicateContractButton contract={contract} /> <DuplicateContractButton contract={contract} />
</Row> </Row>
</Col> </Col>

View File

@ -69,7 +69,6 @@ export function MarketModal(props: {
'flex grid grid-cols-1 sm:grid-cols-2 flex-col gap-3 p-1' 'flex grid grid-cols-1 sm:grid-cols-2 flex-col gap-3 p-1'
} }
cardHideOptions={{ hideGroupLink: true, hideQuickBet: true }} cardHideOptions={{ hideGroupLink: true, hideQuickBet: true }}
querySortOptions={{ disableQueryString: true }}
highlightOptions={{ highlightOptions={{
contractIds: contracts.map((c) => c.id), contractIds: contracts.map((c) => c.id),
highlightClassName: highlightClassName:

View File

@ -1,12 +1,12 @@
import React, { Fragment } from 'react' import React from 'react'
import { CodeIcon } from '@heroicons/react/outline' import { CodeIcon } from '@heroicons/react/outline'
import { Menu, Transition } from '@headlessui/react' import { Menu } from '@headlessui/react'
import toast from 'react-hot-toast'
import { Contract } from 'common/contract' import { Contract } from 'common/contract'
import { contractPath } from 'web/lib/firebase/contracts' import { contractPath } from 'web/lib/firebase/contracts'
import { DOMAIN } from 'common/envs/constants' import { DOMAIN } from 'common/envs/constants'
import { copyToClipboard } from 'web/lib/util/copy' import { copyToClipboard } from 'web/lib/util/copy'
import { ToastClipboard } from 'web/components/toast-clipboard'
import { track } from 'web/lib/service/analytics' import { track } from 'web/lib/service/analytics'
export function embedCode(contract: Contract) { export function embedCode(contract: Contract) {
@ -16,11 +16,10 @@ export function embedCode(contract: Contract) {
return `<iframe width="560" height="405" src="${src}" title="${title}" frameborder="0"></iframe>` return `<iframe width="560" height="405" src="${src}" title="${title}" frameborder="0"></iframe>`
} }
export function ShareEmbedButton(props: { export function ShareEmbedButton(props: { contract: Contract }) {
contract: Contract const { contract } = props
toastClassName?: string
}) { const codeIcon = <CodeIcon className="mr-1.5 h-4 w-4" aria-hidden="true" />
const { contract, toastClassName } = props
return ( return (
<Menu <Menu
@ -28,6 +27,9 @@ export function ShareEmbedButton(props: {
className="relative z-10 flex-shrink-0" className="relative z-10 flex-shrink-0"
onMouseUp={() => { onMouseUp={() => {
copyToClipboard(embedCode(contract)) copyToClipboard(embedCode(contract))
toast.success('Embed code copied!', {
icon: codeIcon,
})
track('copy embed code') track('copy embed code')
}} }}
> >
@ -39,25 +41,9 @@ export function ShareEmbedButton(props: {
color: '#9ca3af', // text-gray-400 color: '#9ca3af', // text-gray-400
}} }}
> >
<CodeIcon className="mr-1.5 h-4 w-4" aria-hidden="true" /> {codeIcon}
Embed Embed
</Menu.Button> </Menu.Button>
<Transition
as={Fragment}
enter="transition ease-out duration-100"
enterFrom="transform opacity-0 scale-95"
enterTo="transform opacity-100 scale-100"
leave="transition ease-in duration-75"
leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95"
>
<Menu.Items>
<Menu.Item>
<ToastClipboard className={toastClassName} />
</Menu.Item>
</Menu.Items>
</Transition>
</Menu> </Menu>
) )
} }

View File

@ -34,16 +34,25 @@ export function UserLink(props: {
username: string username: string
showUsername?: boolean showUsername?: boolean
className?: string className?: string
justFirstName?: boolean short?: boolean
}) { }) {
const { name, username, showUsername, className, justFirstName } = props const { name, username, showUsername, className, short } = props
const firstName = name.split(' ')[0]
const maxLength = 10
const shortName =
firstName.length >= 3
? firstName.length < maxLength
? firstName
: firstName.substring(0, maxLength - 3) + '...'
: name.length > maxLength
? name.substring(0, maxLength) + '...'
: name
return ( return (
<SiteLink <SiteLink
href={`/${username}`} href={`/${username}`}
className={clsx('z-10 truncate', className)} className={clsx('z-10 truncate', className)}
> >
{justFirstName ? name.split(' ')[0] : name} {short ? shortName : name}
{showUsername && ` (@${username})`} {showUsername && ` (@${username})`}
</SiteLink> </SiteLink>
) )

View File

@ -1,9 +1,5 @@
import { debounce } from 'lodash' import { useState } from 'react'
import { useRouter } from 'next/router' import { NextRouter, useRouter } from 'next/router'
import { useEffect, useMemo, useState } from 'react'
import { DEFAULT_SORT } from 'web/components/contract-search'
const MARKETS_SORT = 'markets_sort'
export type Sort = export type Sort =
| 'newest' | 'newest'
@ -15,92 +11,55 @@ export type Sort =
| 'last-updated' | 'last-updated'
| 'score' | 'score'
export function getSavedSort() { type UpdatedQueryParams = { [k: string]: string }
// TODO: this obviously doesn't work with SSR, common sense would suggest type QuerySortOpts = { useUrl: boolean }
// that we should save things like this in cookies so the server has them
if (typeof window !== 'undefined') { function withURLParams(location: Location, params: UpdatedQueryParams) {
return localStorage.getItem(MARKETS_SORT) as Sort | null const newParams = new URLSearchParams(location.search)
for (const [k, v] of Object.entries(params)) {
if (!v) {
newParams.delete(k)
} else { } else {
return null newParams.set(k, v)
} }
} }
const newUrl = new URL(location.href)
newUrl.search = newParams.toString()
return newUrl
}
export interface QuerySortOptions { function updateURL(params: UpdatedQueryParams) {
defaultSort?: Sort // see relevant discussion here https://github.com/vercel/next.js/discussions/18072
shouldLoadFromStorage?: boolean const url = withURLParams(window.location, params).toString()
/** Use normal react state instead of url query string */ const updatedState = { ...window.history.state, as: url, url }
disableQueryString?: boolean window.history.replaceState(updatedState, '', url)
} }
export function useQueryAndSortParams({ function getStringURLParam(router: NextRouter, k: string) {
defaultSort = DEFAULT_SORT, const v = router.query[k]
shouldLoadFromStorage = true, return typeof v === 'string' ? v : null
disableQueryString, }
}: QuerySortOptions = {}) {
export function useQuery(defaultQuery: string, opts?: QuerySortOpts) {
const useUrl = opts?.useUrl ?? false
const router = useRouter() const router = useRouter()
const initialQuery = useUrl ? getStringURLParam(router, 'q') : null
const { s: sort, q: query } = router.query as { const [query, setQuery] = useState(initialQuery ?? defaultQuery)
q?: string if (!useUrl) {
s?: Sort return [query, setQuery] as const
} } else {
return [query, (q: string) => (setQuery(q), updateURL({ q }))] as const
const setSort = (sort: Sort | undefined) => {
router.replace({ query: { ...router.query, s: sort } }, undefined, {
shallow: true,
})
if (shouldLoadFromStorage) {
localStorage.setItem(MARKETS_SORT, sort || '')
} }
} }
const [queryState, setQueryState] = useState(query) export function useSort(defaultSort: Sort, opts?: QuerySortOpts) {
const useUrl = opts?.useUrl ?? false
useEffect(() => { const router = useRouter()
setQueryState(query) const initialSort = useUrl ? (getStringURLParam(router, 's') as Sort) : null
}, [query]) const [sort, setSort] = useState(initialSort ?? defaultSort)
if (!useUrl) {
// Debounce router query update. return [sort, setSort] as const
const pushQuery = useMemo( } else {
() => return [sort, (s: Sort) => (setSort(s), updateURL({ s }))] as const
debounce((query: string | undefined) => {
const queryObj = { ...router.query, q: query }
if (!query) delete queryObj.q
router.replace({ query: queryObj }, undefined, {
shallow: true,
})
}, 100),
[router]
)
const setQuery = (query: string | undefined) => {
setQueryState(query)
if (!disableQueryString) {
pushQuery(query)
}
}
useEffect(() => {
// If there's no sort option, then set the one from localstorage
if (router.isReady && !sort && shouldLoadFromStorage) {
const localSort = localStorage.getItem(MARKETS_SORT) as Sort
if (localSort && localSort !== defaultSort) {
// Use replace to not break navigating back.
router.replace(
{ query: { ...router.query, s: localSort } },
undefined,
{ shallow: true }
)
}
}
})
// use normal state if querydisableQueryString
const [sortState, setSortState] = useState(defaultSort)
return {
sort: disableQueryString ? sortState : sort ?? defaultSort,
query: queryState ?? '',
setSort: disableQueryString ? setSortState : setSort,
setQuery,
} }
} }

View File

@ -3,16 +3,11 @@ import { searchInAny } from 'common/util/parse'
import { sortBy } from 'lodash' import { sortBy } from 'lodash'
import { ContractsGrid } from 'web/components/contract/contracts-grid' import { ContractsGrid } from 'web/components/contract/contracts-grid'
import { useContracts } from 'web/hooks/use-contracts' import { useContracts } from 'web/hooks/use-contracts'
import { import { Sort, useQuery, useSort } from 'web/hooks/use-sort-and-query-params'
QuerySortOptions,
Sort,
useQueryAndSortParams,
} from 'web/hooks/use-sort-and-query-params'
const MAX_CONTRACTS_RENDERED = 100 const MAX_CONTRACTS_RENDERED = 100
export default function ContractSearchFirestore(props: { export default function ContractSearchFirestore(props: {
querySortOptions?: QuerySortOptions
additionalFilter?: { additionalFilter?: {
creatorId?: string creatorId?: string
tag?: string tag?: string
@ -21,10 +16,9 @@ export default function ContractSearchFirestore(props: {
} }
}) { }) {
const contracts = useContracts() const contracts = useContracts()
const { querySortOptions, additionalFilter } = props const { additionalFilter } = props
const [query, setQuery] = useQuery('', { useUrl: true })
const { query, setQuery, sort, setSort } = const [sort, setSort] = useSort('score', { useUrl: true })
useQueryAndSortParams(querySortOptions)
let matches = (contracts ?? []).filter((c) => let matches = (contracts ?? []).filter((c) =>
searchInAny( searchInAny(

View File

@ -31,7 +31,6 @@ import { CreateQuestionButton } from 'web/components/create-question-button'
import React, { useState } from 'react' import React, { useState } from 'react'
import { LoadingIndicator } from 'web/components/loading-indicator' import { LoadingIndicator } from 'web/components/loading-indicator'
import { Modal } from 'web/components/layout/modal' import { Modal } from 'web/components/layout/modal'
import { getSavedSort } from 'web/hooks/use-sort-and-query-params'
import { ChoicesToggleGroup } from 'web/components/choices-toggle-group' import { ChoicesToggleGroup } from 'web/components/choices-toggle-group'
import { toast } from 'react-hot-toast' import { toast } from 'react-hot-toast'
import { useCommentsOnGroup } from 'web/hooks/use-comments' import { useCommentsOnGroup } from 'web/hooks/use-comments'
@ -196,11 +195,8 @@ export default function GroupPage(props: {
const questionsTab = ( const questionsTab = (
<ContractSearch <ContractSearch
user={user} user={user}
querySortOptions={{ defaultSort={'newest'}
shouldLoadFromStorage: true, defaultFilter={'open'}
defaultSort: getSavedSort() ?? 'newest',
defaultFilter: 'open',
}}
additionalFilter={{ groupSlug: group.slug }} additionalFilter={{ groupSlug: group.slug }}
/> />
) )

View File

@ -4,8 +4,7 @@ import { PlusSmIcon } from '@heroicons/react/solid'
import { Page } from 'web/components/page' import { Page } from 'web/components/page'
import { Col } from 'web/components/layout/col' import { Col } from 'web/components/layout/col'
import { getSavedSort } from 'web/hooks/use-sort-and-query-params' import { ContractSearch } from 'web/components/contract-search'
import { ContractSearch, DEFAULT_SORT } from 'web/components/contract-search'
import { Contract } from 'common/contract' import { Contract } from 'common/contract'
import { User } from 'common/user' import { User } from 'common/user'
import { ContractPageContent } from './[username]/[contractSlug]' import { ContractPageContent } from './[username]/[contractSlug]'
@ -35,10 +34,8 @@ const Home = (props: { auth: { user: User } }) => {
<Col className="mx-auto w-full p-2"> <Col className="mx-auto w-full p-2">
<ContractSearch <ContractSearch
user={user} user={user}
querySortOptions={{ useQuerySortLocalStorage={true}
shouldLoadFromStorage: true, useQuerySortUrlParams={true}
defaultSort: getSavedSort() ?? DEFAULT_SORT,
}}
onContractClick={(c) => { onContractClick={(c) => {
// Show contract without navigating to contract page. // Show contract without navigating to contract page.
setContract(c) setContract(c)
@ -104,13 +101,19 @@ const useContractPage = () => {
window.history.pushState = function () { window.history.pushState = function () {
// eslint-disable-next-line prefer-rest-params // eslint-disable-next-line prefer-rest-params
pushState.apply(history, arguments as any) const args = [...(arguments as any)] as any
// Discard NextJS router state.
args[0] = null
pushState.apply(history, args)
updateContract() updateContract()
} }
window.history.replaceState = function () { window.history.replaceState = function () {
// eslint-disable-next-line prefer-rest-params // eslint-disable-next-line prefer-rest-params
replaceState.apply(history, arguments as any) const args = [...(arguments as any)] as any
// Discard NextJS router state.
args[0] = null
replaceState.apply(history, args)
updateContract() updateContract()
} }

View File

@ -40,6 +40,7 @@ import { safeLocalStorage } from 'web/lib/util/local'
import { redirectIfLoggedOut } from 'web/lib/firebase/server-auth' import { redirectIfLoggedOut } from 'web/lib/firebase/server-auth'
import { SiteLink } from 'web/components/site-link' import { SiteLink } from 'web/components/site-link'
import { NotificationSettings } from 'web/components/NotificationSettings' import { NotificationSettings } from 'web/components/NotificationSettings'
import { SEO } from 'web/components/SEO'
export const NOTIFICATIONS_PER_PAGE = 30 export const NOTIFICATIONS_PER_PAGE = 30
const MULTIPLE_USERS_KEY = 'multipleUsers' const MULTIPLE_USERS_KEY = 'multipleUsers'
@ -68,6 +69,8 @@ export default function Notifications(props: {
<Page> <Page>
<div className={'px-2 pt-4 sm:px-4 lg:pt-0'}> <div className={'px-2 pt-4 sm:px-4 lg:pt-0'}>
<Title text={'Notifications'} className={'hidden md:block'} /> <Title text={'Notifications'} className={'hidden md:block'} />
<SEO title="Notifications" description="Manifold user notifications" />
<div> <div>
<Tabs <Tabs
currentPageForAnalytics={'notifications'} currentPageForAnalytics={'notifications'}
@ -437,7 +440,7 @@ function IncomeNotificationItem(props: {
name={sourceUserName || ''} name={sourceUserName || ''}
username={sourceUserUsername || ''} username={sourceUserUsername || ''}
className={'mr-1 flex-shrink-0'} className={'mr-1 flex-shrink-0'}
justFirstName={true} short={true}
/> />
))} ))}
{getReasonForShowingIncomeNotification(false)} {' on'} {getReasonForShowingIncomeNotification(false)} {' on'}
@ -606,7 +609,7 @@ function NotificationItem(props: {
name={sourceUserName || ''} name={sourceUserName || ''}
username={sourceUserUsername || ''} username={sourceUserUsername || ''}
className={'mr-0 flex-shrink-0'} className={'mr-0 flex-shrink-0'}
justFirstName={true} short={true}
/> />
<div className={'inline-flex overflow-hidden text-ellipsis pl-1'}> <div className={'inline-flex overflow-hidden text-ellipsis pl-1'}>
<span className={'flex-shrink-0'}> <span className={'flex-shrink-0'}>
@ -678,7 +681,7 @@ function NotificationItem(props: {
name={sourceUserName || ''} name={sourceUserName || ''}
username={sourceUserUsername || ''} username={sourceUserUsername || ''}
className={'relative mr-1 flex-shrink-0'} className={'relative mr-1 flex-shrink-0'}
justFirstName={true} short={true}
/> />
)} )}
{getReasonForShowingNotification( {getReasonForShowingNotification(

View File

@ -15,11 +15,8 @@ export default function TagPage() {
<Title text={`#${tag}`} /> <Title text={`#${tag}`} />
<ContractSearch <ContractSearch
user={user} user={user}
querySortOptions={{ defaultSort="newest"
defaultSort: 'newest', defaultFilter="all"
defaultFilter: 'all',
shouldLoadFromStorage: true,
}}
additionalFilter={{ tag }} additionalFilter={{ tag }}
/> />
</Page> </Page>