safe api created
This commit is contained in:
parent
a78e63098e
commit
4bebc7fb10
2
.vscode/settings.json
vendored
Normal file
2
.vscode/settings.json
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
{
|
||||
}
|
||||
2116
package-lock.json
generated
2116
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -57,10 +57,11 @@
|
||||
"react-virtuoso": "^4.1.0",
|
||||
"recharts": "^2.15.0",
|
||||
"resend": "^3.2.0",
|
||||
"stripe": "^14.21.0",
|
||||
"stripe": "^18.5.0",
|
||||
"swr": "^2.1.5",
|
||||
"tinymce": "^6.4.0",
|
||||
"vanilla-calendar-pro": "^2.9.10"
|
||||
"vanilla-calendar-pro": "^2.9.10",
|
||||
"zod": "^3.25.76"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.17.5",
|
||||
|
||||
913
public/pics/payment-pic.svg
Normal file
913
public/pics/payment-pic.svg
Normal file
@ -0,0 +1,913 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 27.5.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 2512.222 2000" style="enable-background:new 0 0 2512.222 2000;" xml:space="preserve">
|
||||
<rect style="fill:#FFFFFF;" width="2512.222" height="2000"/>
|
||||
<g>
|
||||
<g style="opacity:0.4;">
|
||||
<g>
|
||||
<g>
|
||||
<g>
|
||||
<path style="fill:#FECC86;" d="M2238.844,1492.903c-40.214,90.9-95.976,162.932-162.665,219.435
|
||||
c-77.01,65.262-168.597,109.811-267.641,138.785c-322.243,94.289-723.361,23.711-957.925-34.843
|
||||
c-47.566-11.877-88.282-23.26-120.102-32.672c-53.708-15.887-104.613-40.148-150.487-72.282
|
||||
c-0.24-0.168-0.479-0.336-0.719-0.504c-314.349-220.496-447.861-535.639-343.34-834.127
|
||||
c1.624-4.657,309.799-952.591,1040.546-639.527c320.798,137.438,202.798,254.225,558.386,400.297
|
||||
c166.242,68.295,386.23,190.113,446.577,477.468C2303.009,1217.483,2295.757,1364.243,2238.844,1492.903z"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g style="opacity:0.3;">
|
||||
<g>
|
||||
<g>
|
||||
<g>
|
||||
<g>
|
||||
<path style="fill:#FECC86;" d="M1403.735,887.1c16.126,10.898,33.673,18.888,51.908,23.907l6.692,46.362l32.005,1.242
|
||||
l32.005,1.243l10.266-45.703c18.569-3.589,36.683-10.195,53.605-19.81l37.52,28.054l23.51-21.752l23.51-21.753l-25.061-39.582
|
||||
c10.898-16.126,18.888-33.673,23.907-51.908l46.362-6.692l1.242-32.005l1.242-32.005l-45.703-10.267
|
||||
c-3.589-18.569-10.195-36.682-19.81-53.604l28.055-37.521l-21.753-23.51l-21.752-23.51l-39.583,25.061
|
||||
c-16.126-10.898-33.673-18.888-51.907-23.906l-6.692-46.362l-32.005-1.243l-32.005-1.243l-10.267,45.704
|
||||
c-18.568,3.59-36.682,10.195-53.605,19.81l-37.52-28.055l-23.51,21.752l-23.51,21.752l25.061,39.582
|
||||
c-10.898,16.126-18.888,33.673-23.906,51.908l-46.362,6.692l-1.243,32.005l-1.242,32.005l45.703,10.267
|
||||
c3.589,18.569,10.195,36.682,19.81,53.605l-28.055,37.52l21.752,23.51l21.752,23.51L1403.735,887.1z M1421.213,652.024
|
||||
c48.711-45.07,124.736-42.119,169.806,6.593c45.07,48.712,42.119,124.737-6.593,169.807
|
||||
c-48.711,45.07-124.736,42.118-169.807-6.593C1369.55,773.12,1372.502,697.094,1421.213,652.024z"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g style="opacity:0.5;">
|
||||
<g>
|
||||
<g>
|
||||
<g>
|
||||
<g>
|
||||
<path style="fill:#FECC86;" d="M1149.549,1274.44c-16.026-8.936-33.151-15.026-50.682-18.269l-10.024-43.006l-30.161,1.414
|
||||
l-30.161,1.413l-5.958,43.755c-17.151,4.869-33.632,12.534-48.752,22.929l-37.503-23.325l-20.328,22.327l-20.328,22.327
|
||||
l26.73,35.157c-8.936,16.025-15.026,33.151-18.269,50.682l-43.006,10.024l1.414,30.161l1.413,30.161l43.755,5.958
|
||||
c4.868,17.151,12.533,33.632,22.929,48.751l-23.325,37.503l22.327,20.328l22.327,20.328l35.157-26.73
|
||||
c16.026,8.936,33.151,15.026,50.682,18.269l10.024,43.006l30.161-1.413l30.161-1.413l5.958-43.755
|
||||
c17.151-4.868,33.632-12.533,48.751-22.929l37.503,23.325l20.328-22.327l20.328-22.327l-26.731-35.157
|
||||
c8.936-16.025,15.026-33.151,18.269-50.682l43.006-10.024l-1.413-30.161l-1.413-30.161l-43.755-5.958
|
||||
c-4.868-17.152-12.533-33.632-22.929-48.752l23.325-37.503l-22.327-20.328l-22.327-20.328L1149.549,1274.44z
|
||||
M1152.088,1496.646c-42.118,46.26-113.763,49.618-160.024,7.5c-46.26-42.118-49.618-113.763-7.499-160.024
|
||||
c42.118-46.26,113.763-49.618,160.024-7.5S1194.207,1450.386,1152.088,1496.646z"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g style="opacity:0.5;">
|
||||
<g>
|
||||
<g>
|
||||
<g>
|
||||
<g>
|
||||
<path style="fill:#FECC86;" d="M1529.684,1560.188c-6.879-9.179-15.11-17.012-24.297-23.323l5.213-27.109l-17.584-6.864
|
||||
l-17.584-6.864l-14.531,23.472c-11.033-1.582-22.394-1.396-33.672,0.695l-15.485-22.859l-17.287,7.58l-17.287,7.58
|
||||
l6.323,26.876c-9.179,6.879-17.012,15.11-23.323,24.297l-27.109-5.213l-6.864,17.584l-6.864,17.584l23.472,14.531
|
||||
c-1.582,11.033-1.396,22.394,0.695,33.672l-22.859,15.485l7.58,17.287l7.58,17.287l26.876-6.323
|
||||
c6.879,9.179,15.11,17.012,24.297,23.323l-5.213,27.109l17.584,6.864l17.584,6.864l14.531-23.472
|
||||
c11.033,1.582,22.394,1.396,33.672-0.695l15.485,22.859l17.287-7.58l17.287-7.58l-6.324-26.876
|
||||
c9.179-6.879,17.012-15.11,23.323-24.297l27.109,5.213l6.864-17.584l6.864-17.584l-23.472-14.531
|
||||
c1.582-11.033,1.396-22.394-0.695-33.672l22.858-15.485l-7.58-17.287l-7.58-17.287L1529.684,1560.188z M1474.62,1687.73
|
||||
c-35.818,15.706-77.587-0.598-93.293-36.416c-15.706-35.818,0.598-77.587,36.416-93.293
|
||||
c35.818-15.706,77.587,0.598,93.293,36.416C1526.743,1630.256,1510.438,1672.024,1474.62,1687.73z"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<g>
|
||||
<g>
|
||||
<g>
|
||||
<g>
|
||||
<g>
|
||||
<path style="fill:#3E9896;" d="M656.314,775.385c0,0-51.98-85.939-176.683-63.654
|
||||
C479.631,711.731,563.582,836.232,656.314,775.385z"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<g>
|
||||
<g>
|
||||
<g>
|
||||
<g>
|
||||
<g>
|
||||
<path style="fill:#80C4A4;" d="M737.487,798.071c0,0-15.484-172.267-225.294-232.028
|
||||
C512.194,566.043,548.008,822.142,737.487,798.071z"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<path style="fill:#FE9F73;" d="M988.548,774.88c0,0,59.325-14.516,76.496,7.789c17.17,22.306-25.108,83.815-57.227,88.592
|
||||
C975.697,876.039,988.548,774.88,988.548,774.88z"/>
|
||||
</g>
|
||||
<g>
|
||||
<path style="fill:#ED7163;" d="M820.863,1596.799c0,0,150.967-73.31,178.643-176.863
|
||||
c27.676-103.553-150.016-116.787-147.547-116.775c2.469,0.012-180.469,48.474-192.961,78.037
|
||||
c-12.492,29.564-67.424,152.731-45.205,152.843C636.013,1534.152,820.863,1596.799,820.863,1596.799z"/>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<g>
|
||||
<path style="fill:#D0F6FC;" d="M954.423,666.885c-0.044-0.223-0.092-0.445-0.146-0.664c-1.701-7.446-8.715-12.449-16.31-11.641
|
||||
c-0.225,0.021-0.45,0.047-0.676,0.078c-0.075,0.011-0.148,0.013-0.223,0.026l-332.204,55.422
|
||||
c-0.075,0.012-0.145,0.035-0.219,0.048c-0.224,0.044-0.445,0.093-0.665,0.146c-7.446,1.701-12.456,8.71-11.648,16.305
|
||||
c0.021,0.224,0.047,0.449,0.078,0.675c0.011,0.075,0.013,0.149,0.026,0.223l115.235,690.732
|
||||
c1.199,7.185,7.995,12.032,15.173,10.834l336.027-56.059c7.178-1.198,12.033-7.988,10.834-15.173L954.471,667.105
|
||||
C954.459,667.03,954.437,666.959,954.423,666.885z"/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<path style="fill:#0D93B2;" d="M585.992,724.186l24.066-23.137l323.826-53.166l32.558,20.236c0,0,2.078,29.351,0.372,30.088
|
||||
c-1.706,0.737-376.699,67.786-377.648,62.003C588.216,754.427,585.992,724.186,585.992,724.186z"/>
|
||||
</g>
|
||||
<g style="opacity:0.5;">
|
||||
<g>
|
||||
<g>
|
||||
<g>
|
||||
<g>
|
||||
<path style="fill:#FFFFFF;" d="M623.266,733.739c0.339,2.029-0.578,3.977-2.187,5.047c-0.574,0.387-1.245,0.662-1.976,0.784
|
||||
c-0.725,0.121-1.441,0.077-2.117-0.101c-1.868-0.483-3.376-2.033-3.714-4.063c-0.461-2.761,1.41-5.371,4.163-5.831
|
||||
C620.196,729.115,622.805,730.978,623.266,733.739z"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<g>
|
||||
<g>
|
||||
<path style="fill:#FFFFFF;" d="M638.727,731.16c0.339,2.029-0.578,3.977-2.187,5.047c-0.574,0.386-1.244,0.662-1.976,0.784
|
||||
c-0.732,0.122-1.455,0.08-2.124-0.1c-1.869-0.49-3.369-2.035-3.707-4.064c-0.461-2.761,1.403-5.37,4.164-5.831
|
||||
C635.657,726.536,638.266,728.399,638.727,731.16z"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<g>
|
||||
<g>
|
||||
<path style="fill:#FFFFFF;" d="M654.187,728.58c0.339,2.029-0.578,3.977-2.187,5.047c-0.574,0.387-1.245,0.662-1.976,0.784
|
||||
c-0.732,0.122-1.455,0.079-2.124-0.1c-1.87-0.49-3.369-2.034-3.707-4.064c-0.461-2.761,1.403-5.37,4.163-5.831
|
||||
C651.117,723.956,653.727,725.819,654.187,728.58z"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<g>
|
||||
<g>
|
||||
<g>
|
||||
<path style="fill:#20111D;" d="M1068.513,1350.686c1.857,11.133-5.666,21.66-16.792,23.517l-321.719,53.672
|
||||
c-11.133,1.857-21.666-5.657-23.523-16.791L593.702,735.081c-2.073-12.424,6.319-24.167,18.735-26.238l317.06-52.895
|
||||
c12.417-2.071,24.166,6.312,26.238,18.735L1068.513,1350.686z M927.659,640.846l-318.386,53.116
|
||||
c-20.485,3.417-34.328,22.795-30.909,43.288l113.938,682.96c3.419,20.492,22.803,34.327,43.288,30.909l318.386-53.116
|
||||
c20.492-3.419,34.328-22.796,30.909-43.288l-113.938-682.96C967.528,651.263,948.151,637.427,927.659,640.846z"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<path style="fill:#FE9F73;" d="M851.809,1578.441l-0.009,0.91v0.286l-0.437,87.679l-0.749,148.963
|
||||
c-47.566-11.877-88.282-23.26-120.102-32.672c-53.708-15.887-104.613-40.148-150.487-72.282c-0.24-0.168-0.479-0.336-0.719-0.504
|
||||
l6.146-188.026c0,0-38.617-175.493-35.941-217.446c2.685-41.962,30.321-138.098,42.845-175.073
|
||||
c3.684-10.865,5.888-24.951,7.163-39.411c0.098-1.079,0.187-2.159,0.268-3.247c0-0.027,0.009-0.045,0.009-0.045
|
||||
c1.097-13.88,1.374-27.912,1.329-39.625c0-0.366,0-0.732-0.009-1.106c0-1.026-0.009-2.025-0.018-3.006
|
||||
c0-0.259-0.009-0.517-0.009-0.776c-0.169-14.139-0.767-23.844-0.767-23.844c0.241-2.4,0.58-4.648,0.99-6.762
|
||||
c0.419-2.212,0.937-4.264,1.525-6.173c0.446-1.454,0.946-2.819,1.481-4.095c3.613-8.635,9.152-13.479,15.691-15.281
|
||||
c4.844-1.338,10.241-1.008,15.816,0.678c19.741,5.977,41.748,29.009,49.553,55.512c14.567,49.455,1.508,192.575,1.508,192.575
|
||||
s33.764,34.103,54.959,102.086c5.62,18.019,10.357,38.42,13.345,61.203c1.133,8.662,1.704,16.788,1.757,24.415
|
||||
c0.089,8.813-0.5,16.949-1.606,24.451c-1.57,10.669-4.211,20.062-7.538,28.314c-17.725,44.014-54.995,55.833-54.995,55.833
|
||||
l2.462,0.723l60.775,17.761l92.568,27.047l3.176,0.927L851.809,1578.441z"/>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<path style="fill:#FE9F73;" d="M1045.097,1108.443l4.139,25.287c0,0-35.67,35.989-24.032,81.697
|
||||
c11.638,45.708,42.376,51.217,42.376,51.217s48.019-64.365,47.264-86.708
|
||||
C1114.089,1157.593,1113.904,1118.394,1045.097,1108.443z"/>
|
||||
</g>
|
||||
<g>
|
||||
<path style="fill:#F78A5E;" d="M1073.704,1258.174c0,0,19.254-19.522,37.136-62.129c-2.974-1.843-8.497-3.435-12.035-4.485
|
||||
c-10.859-3.224-17.766-0.992-23.642,6.629l-22.101,32.349c0,0-4.248,8.259,5.083,17.805
|
||||
C1061.733,1252.013,1073.704,1258.174,1073.704,1258.174z"/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<path d="M1068.222,1268.37l-1.007-0.249c-15.42-3.821-24.972-15.711-25.371-16.215c-1.004-1.177-25.345-30.218-20.486-64.015
|
||||
c4.835-33.638,33.043-61.034,34.241-62.184l2.107,2.195c-0.286,0.275-28.652,27.839-33.336,60.421
|
||||
c-4.66,32.416,19.581,61.364,19.826,61.652c0.122,0.153,8.941,11.099,22.753,14.93l27.399-38.671
|
||||
c28.671-41.39,15.454-73.785,15.316-74.107l2.798-1.195c0.149,0.348,3.645,8.683,3.468,22.41
|
||||
c-0.162,12.586-3.605,32.282-19.091,54.638L1068.222,1268.37z"/>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<path style="fill:#FE9F73;" d="M1072.952,1280.702l3.234,19.759c0,0-27.873,28.122-18.779,63.839s33.114,40.021,33.114,40.021
|
||||
s37.522-50.295,36.932-67.754C1126.863,1319.109,1126.718,1288.478,1072.952,1280.702z"/>
|
||||
</g>
|
||||
<g>
|
||||
<path style="fill:#F78A5E;" d="M1095.305,1397.704c0,0,15.045-15.255,29.019-48.548c-2.323-1.44-6.64-2.684-9.404-3.505
|
||||
c-8.485-2.519-13.883-0.775-18.474,5.18l-17.27,25.278c0,0-3.319,6.453,3.972,13.913
|
||||
C1085.951,1392.889,1095.305,1397.704,1095.305,1397.704z"/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<path d="M1091.022,1405.671l-0.787-0.195c-12.05-2.985-19.513-12.277-19.825-12.671c-0.784-0.92-19.804-23.612-16.008-50.022
|
||||
c3.779-26.285,25.82-47.692,26.757-48.591l1.646,1.715c-0.224,0.214-22.389,21.753-26.049,47.214
|
||||
c-3.641,25.33,15.301,47.95,15.493,48.175c0.096,0.119,6.986,8.674,17.779,11.667l21.41-30.218
|
||||
c22.404-32.343,12.076-57.656,11.969-57.907l2.187-0.934c0.46,1.075,10.994,26.711-12.208,60.206L1091.022,1405.671z"/>
|
||||
</g>
|
||||
<g>
|
||||
<path style="fill:#FE9F73;" d="M749.249,1472.067c0,0-24.062,62.223,6.75,78.368s-74.206,24.748-74.191,21.705
|
||||
c0.015-3.043-72.56-93.167-54.281-97.639c18.279-4.473,75.435-25.486,88.298-11.729
|
||||
C728.688,1476.528,749.249,1472.067,749.249,1472.067z"/>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<path style="fill:#FE9F73;" d="M1020.792,958.733l4.139,25.287c0,0-35.67,35.989-24.032,81.697
|
||||
c11.638,45.708,42.377,51.217,42.377,51.217s48.019-64.365,47.264-86.708C1089.784,1007.883,1089.599,968.684,1020.792,958.733z
|
||||
"/>
|
||||
</g>
|
||||
<g>
|
||||
<path style="fill:#F78A5E;" d="M1049.399,1108.464c0,0,19.254-19.522,37.136-62.129c-2.973-1.843-8.497-3.435-12.035-4.485
|
||||
c-10.859-3.224-17.766-0.992-23.642,6.629l-22.101,32.349c0,0-4.248,8.259,5.083,17.805
|
||||
C1037.428,1102.303,1049.399,1108.464,1049.399,1108.464z"/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<rect x="807.151" y="1621.354" transform="matrix(0.005 -1 1 0.005 -775.52 2466.3477)" width="88.878" height="3.043"/>
|
||||
</g>
|
||||
<g>
|
||||
<path d="M843.837,1583.046l-1.228-2.784c51.692-22.803,99.717-66.673,122.463-97.784c20.117-27.513,28.745-47.853,28.83-48.055
|
||||
l2.806,1.176c-0.086,0.205-8.833,20.846-29.18,48.675C944.55,1515.702,896.041,1560.016,843.837,1583.046z"/>
|
||||
</g>
|
||||
<g>
|
||||
<path style="fill:#F78A5E;" d="M634.36,1010.945c-3.03-8.576-24.938-9.722-31.376-8.573c-4.156,0.742,0.194,81.517-3.187,85.202
|
||||
c0,0-0.612,1.011,10.02,1.382c11.717,0.408,19.855-4.532,19.855-4.532S637.39,1019.52,634.36,1010.945z"/>
|
||||
</g>
|
||||
<g>
|
||||
<path d="M1043.923,1118.651l-0.999-0.237c-13.866-3.285-24.872-15.633-25.334-16.157c-1.055-1.239-25.396-30.28-20.538-64.076
|
||||
c4.836-33.638,33.043-61.034,34.242-62.184l2.106,2.195c-0.286,0.275-28.652,27.838-33.336,60.421
|
||||
c-4.66,32.415,19.581,61.363,19.826,61.652c0.087,0.099,10.284,11.521,22.748,14.936l27.403-38.677
|
||||
c28.671-41.39,15.454-73.785,15.317-74.107l2.798-1.195c0.149,0.349,3.644,8.684,3.468,22.41
|
||||
c-0.162,12.587-3.606,32.283-19.091,54.638L1043.923,1118.651z"/>
|
||||
</g>
|
||||
<g>
|
||||
<polygon points="1023.429,984.266 975.348,707.29 978.35,706.798 1026.432,983.774 "/>
|
||||
</g>
|
||||
<g>
|
||||
<path d="M1058.544,830.659l-2.48-1.763c0.212-0.299,21.067-30.112,5.765-47.536c-15.515-17.664-70.32-5.591-70.872-5.467
|
||||
l-0.667-2.969c2.324-0.522,57.132-12.581,73.825,6.428C1081.005,798.585,1059.469,829.359,1058.544,830.659z"/>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<g>
|
||||
<g>
|
||||
<g>
|
||||
<path style="fill:#000100;" d="M646.436,1498.991c-0.446,0.319-1.043,0.402-1.581,0.165
|
||||
c-18.096-7.958-35.025-34.515-35.737-35.642c-0.473-0.749-0.246-1.734,0.5-2.213c0.749-0.473,1.74-0.25,2.213,0.499
|
||||
c0.169,0.268,17.12,26.856,34.316,34.418c0.811,0.357,1.18,1.303,0.822,2.114
|
||||
C646.849,1498.606,646.662,1498.829,646.436,1498.991z"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<g>
|
||||
<g>
|
||||
<g>
|
||||
<path style="fill:#000100;" d="M668.339,1495.637c-0.349,0.25-0.795,0.36-1.248,0.27
|
||||
c-19.388-3.867-41.647-26.146-42.585-27.092c-0.624-0.63-0.62-1.645,0.01-2.269c0.629-0.628,1.645-0.619,2.269,0.01
|
||||
c0.223,0.225,22.51,22.53,40.933,26.204c0.869,0.174,1.433,1.019,1.259,1.887
|
||||
C668.895,1495.063,668.658,1495.409,668.339,1495.637z"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<path d="M693.271,1533.42l-0.921-2.9c0.189-0.06,19.076-6.238,35.973-25.422c15.591-17.7,32.646-50.447,25.36-105.934
|
||||
c-7.33-55.849-25.394-97.032-39.256-121.746c-15.024-26.783-28.489-40.535-28.623-40.671l-0.498-0.503l0.064-0.705
|
||||
c0.13-1.429,12.869-143.402-1.452-192.014c-8.574-29.118-35.936-55.772-57.406-55.921c-0.016,0-0.034,0-0.05,0
|
||||
c-10.614-0.053-18.279,6.56-22.172,19.13c-1.138,3.684-1.96,7.895-2.442,12.517c0.141,2.422,2.147,38.326-0.81,71.753
|
||||
c-1.453,16.497-3.889,29.875-7.24,39.761c-11.463,33.842-40.048,132.172-42.766,174.686
|
||||
c-2.638,41.262,35.523,215.263,35.909,217.016l-2.972,0.654c-1.579-7.173-38.639-176.175-35.974-217.863
|
||||
c2.736-42.799,31.42-141.514,42.921-175.469c3.273-9.657,5.658-22.796,7.091-39.052c3.026-34.207,0.819-71.057,0.797-71.425
|
||||
l-0.008-0.125l0.013-0.124c0.5-4.869,1.367-9.319,2.574-13.228c5.466-17.649,16.651-21.315,25.094-21.273
|
||||
c0.019,0,0.038,0,0.057,0c10.81,0.076,23.626,6.433,35.161,17.443c11.812,11.273,20.74,25.714,25.142,40.661
|
||||
c14.122,47.933,2.704,180.404,1.626,192.462c2.593,2.763,14.966,16.491,28.552,40.686
|
||||
c14.019,24.965,32.284,66.561,39.685,122.955c3.284,25.005,2.014,47.638-3.774,67.267c-4.646,15.755-12.193,29.617-22.432,41.203
|
||||
C713.036,1526.989,694.07,1533.167,693.271,1533.42z"/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<path style="fill:#FE9F73;" d="M1532.66,819.504c0,0-60.971-3.564-73.831,21.475c-12.86,25.04,39.831,77.903,72.285,76.802
|
||||
C1563.567,916.679,1532.66,819.504,1532.66,819.504z"/>
|
||||
</g>
|
||||
<g>
|
||||
<path style="fill:#ED7163;" d="M1846.011,1597.629c0,0-161.724-44.843-207.644-141.696
|
||||
c-45.921-96.853,126.46-141.957,124.034-141.499s186.255,15.087,203.881,41.909c17.625,26.822,93.896,138.045,72.062,142.167
|
||||
C2016.51,1502.632,1846.011,1597.629,1846.011,1597.629z"/>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<g>
|
||||
<path style="fill:#D0F6FC;" d="M1546.721,707.122c0.003-0.227,0.01-0.454,0.023-0.679c0.328-7.631,6.323-13.818,13.94-14.395
|
||||
c0.225-0.02,0.451-0.035,0.679-0.045c0.076-0.002,0.149-0.014,0.224-0.015l336.75-5.48c0.076-0.001,0.149,0.008,0.224,0.008
|
||||
c0.228,0.003,0.454,0.01,0.68,0.023c7.631,0.328,13.825,6.317,14.401,13.933c0.02,0.225,0.035,0.451,0.045,0.678
|
||||
c0.002,0.076,0.014,0.149,0.015,0.224l11.394,700.185c0.119,7.283-5.691,13.277-12.967,13.396l-340.626,5.543
|
||||
c-7.276,0.119-13.277-5.684-13.396-12.967l-11.394-700.185C1546.712,707.271,1546.721,707.198,1546.721,707.122z"/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<path style="fill:#0D93B2;" d="M1919.443,696.948l-27.849-18.411l-328.103,6.185l-28.369,25.782c0,0,3.256,29.244,5.068,29.661
|
||||
c1.811,0.417,382.747-1.354,382.636-7.213S1919.443,696.948,1919.443,696.948z"/>
|
||||
</g>
|
||||
<g style="opacity:0.5;">
|
||||
<g>
|
||||
<g>
|
||||
<g>
|
||||
<g>
|
||||
<path style="fill:#FFFFFF;" d="M1884.507,713.076c0.033,2.057,1.287,3.807,3.063,4.569c0.634,0.277,1.344,0.426,2.085,0.414
|
||||
c0.735-0.012,1.432-0.184,2.063-0.482c1.75-0.812,2.953-2.61,2.919-4.667c-0.046-2.799-2.356-5.028-5.148-4.983
|
||||
C1886.691,707.973,1884.461,710.277,1884.507,713.076z"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<g>
|
||||
<g>
|
||||
<path style="fill:#FFFFFF;" d="M1868.834,713.331c0.034,2.057,1.287,3.807,3.063,4.569c0.634,0.277,1.344,0.426,2.085,0.414
|
||||
c0.742-0.012,1.446-0.185,2.071-0.482c1.75-0.819,2.946-2.609,2.912-4.666c-0.046-2.799-2.349-5.029-5.148-4.983
|
||||
C1871.019,708.228,1868.789,710.532,1868.834,713.331z"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<g>
|
||||
<g>
|
||||
<path style="fill:#FFFFFF;" d="M1853.162,713.586c0.033,2.057,1.287,3.807,3.063,4.569c0.634,0.276,1.344,0.426,2.085,0.414
|
||||
c0.742-0.012,1.446-0.185,2.071-0.482c1.75-0.819,2.946-2.609,2.912-4.666c-0.046-2.799-2.349-5.029-5.148-4.983
|
||||
C1855.347,708.483,1853.117,710.787,1853.162,713.586z"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<g>
|
||||
<g>
|
||||
<g>
|
||||
<path style="fill:#20111D;" d="M1546.838,715.029c-0.205-12.594,9.837-22.961,22.424-23.165l321.4-5.23
|
||||
c12.587-0.205,22.961,9.83,23.166,22.424l11.151,685.254c0.184,11.286-8.819,20.579-20.104,20.763l-326.123,5.307
|
||||
c-11.278,0.183-20.578-8.812-20.762-20.098L1546.838,715.029z M1531.349,714.896l11.266,692.307
|
||||
c0.338,20.773,17.445,37.333,38.218,36.995l322.743-5.252c20.766-0.338,37.333-17.446,36.994-38.219l-11.266-692.307
|
||||
c-0.338-20.773-17.453-37.332-38.218-36.994l-322.743,5.252C1547.57,677.016,1531.011,694.124,1531.349,714.896z"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<path style="fill:#FE9F73;" d="M2076.179,1712.338c-77.01,65.262-168.597,109.811-267.641,138.785l0.161-132.933l0.053-45.717
|
||||
l3.72-86.145v-0.009l-0.054-0.259l-0.16-0.901l0.928-0.473l2.034-1.026l86.199-43.345c0.009,0,0.009,0,0.018,0l56.52-28.421
|
||||
l2.293-1.151c0,0-38.929-4.915-64.281-45.182c-0.027-0.036-0.045-0.08-0.071-0.116c-0.018-0.027-0.027-0.054-0.045-0.08
|
||||
c-4.683-7.431-8.885-16.066-12.301-26.092c-2.444-7.181-4.487-15.076-5.986-23.746l-0.009-0.009
|
||||
c-1.32-7.511-2.23-15.602-2.676-24.326c-0.678-13.407-0.633-26.119-0.027-38.108c0.437-8.537,1.16-16.699,2.105-24.496v-0.009
|
||||
c1.133-9.384,2.596-18.216,4.282-26.485c4.648-22.854,10.972-41.382,16.726-55.146c7.841-18.778,14.612-28.697,14.612-28.697
|
||||
s-8.189-29.304-16.503-65.931c-0.687-2.988-1.365-6.021-2.034-9.09c-9.054-41.15-17.475-88.901-14.755-114.664
|
||||
c2.881-27.484,20.366-54.103,38.706-63.541c5.174-2.676,10.428-3.97,15.432-3.533c6.762,0.598,13.087,4.353,18.198,12.203
|
||||
c0.758,1.16,1.49,2.417,2.194,3.755c0.91,1.757,1.784,3.666,2.587,5.736c0.009,0.009,0.009,0.018,0.018,0.036
|
||||
c0.794,2.016,1.526,4.175,2.203,6.494c0,0,1.258,10.366,3.818,25.12c0.125,0.705,0.25,1.418,0.375,2.15
|
||||
c0.125,0.678,0.241,1.356,0.357,2.025c2.07,11.338,4.817,24.728,8.314,37.841v0.009c0,0.018,0.009,0.027,0.009,0.036
|
||||
c0,0.009,0,0.009,0.009,0.018c0.268,1.044,0.562,2.096,0.848,3.131c3.862,13.996,8.581,27.439,14.157,37.457
|
||||
c19.001,34.112,63.541,123.682,73.764,164.475c10.214,40.776,3.907,220.345,3.907,220.345v0.009L2076.179,1712.338z"/>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<path style="fill:#FE9F73;" d="M1537.276,1157.795l0.496,25.619c0,0,41.582,28.956,38.39,76.014
|
||||
c-3.192,47.059-32.431,58.027-32.431,58.027s-58.852-54.635-62.145-76.747
|
||||
C1478.293,1218.595,1471.397,1180.007,1537.276,1157.795z"/>
|
||||
</g>
|
||||
<g>
|
||||
<path style="fill:#F78A5E;" d="M1536.178,1310.23c0,0-22.463-15.724-47.745-54.401c2.592-2.349,7.737-4.913,11.027-6.585
|
||||
c10.098-5.132,17.295-4.184,24.45,2.251l27.58,27.826c0,0,5.669,7.356-1.785,18.43
|
||||
C1546.84,1302.008,1536.178,1310.23,1536.178,1310.23z"/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<path d="M1543.411,1319.269l-35.428-34.547c-19.28-19.203-26.223-37.953-28.655-50.303c-2.652-13.469-0.719-22.299-0.636-22.668
|
||||
l2.968,0.672c-0.076,0.339-7.226,34.589,28.459,70.132l33.92,33.076c12.893-6.262,19.589-18.62,19.656-18.747
|
||||
c0.215-0.375,18.83-33.224,8.392-64.264c-10.491-31.201-43.368-53.189-43.699-53.407l1.675-2.54
|
||||
c1.387,0.915,34.077,22.766,44.908,54.978c10.882,32.364-7.815,65.322-8.616,66.709c-0.275,0.52-7.522,13.941-21.999,20.483
|
||||
L1543.411,1319.269z"/>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<path style="fill:#FE9F73;" d="M1540.986,1332.252l0.387,20.018c0,0,32.493,22.626,29.998,59.399
|
||||
c-2.494,36.772-25.342,45.343-25.342,45.343s-45.988-42.693-48.561-59.971S1489.507,1349.609,1540.986,1332.252z"/>
|
||||
</g>
|
||||
<g>
|
||||
<path style="fill:#F78A5E;" d="M1540.128,1451.367c0,0-17.552-12.287-37.309-42.51c2.025-1.836,6.046-3.839,8.616-5.145
|
||||
c7.891-4.01,13.515-3.269,19.106,1.759l21.551,21.744c0,0,4.43,5.748-1.394,14.402
|
||||
C1548.459,1444.942,1540.128,1451.367,1540.128,1451.367z"/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<path d="M1545.78,1458.43l-27.684-26.995c-28.877-28.763-23.147-55.88-22.889-57.019l2.319,0.524
|
||||
c-0.059,0.266-5.646,27.028,22.238,54.802l26.506,25.846c10.101-4.904,15.307-14.55,15.359-14.649
|
||||
c0.168-0.294,14.714-25.963,6.558-50.218c-8.198-24.381-33.888-41.563-34.147-41.734l1.309-1.985
|
||||
c1.083,0.714,26.628,17.79,35.091,42.96c8.503,25.289-6.106,51.044-6.733,52.127c-0.215,0.406-5.877,10.894-17.19,16.006
|
||||
L1545.78,1458.43z"/>
|
||||
</g>
|
||||
<g>
|
||||
<path style="fill:#FE9F73;" d="M1893.924,1462.016c0,0,34.903,56.854,7.513,78.298c-27.39,21.444,77.455,10.94,76.891,7.951
|
||||
s54.542-104.738,35.756-105.836c-18.786-1.098-78.798-11.444-88.965,4.409
|
||||
C1914.952,1462.691,1893.924,1462.016,1893.924,1462.016z"/>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<path style="fill:#FE9F73;" d="M1534.146,1006.157l0.496,25.619c0,0,41.582,28.956,38.39,76.014
|
||||
c-3.192,47.059-32.431,58.027-32.431,58.027s-58.853-54.635-62.145-76.748
|
||||
C1475.164,1066.958,1468.267,1028.37,1534.146,1006.157z"/>
|
||||
</g>
|
||||
<g>
|
||||
<path style="fill:#F78A5E;" d="M1533.048,1158.593c0,0-22.463-15.724-47.745-54.401c2.592-2.349,7.737-4.913,11.027-6.585
|
||||
c10.098-5.132,17.295-4.184,24.45,2.251l27.58,27.826c0,0,5.669,7.356-1.785,18.43
|
||||
C1543.71,1150.371,1533.048,1158.593,1533.048,1158.593z"/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
|
||||
<rect x="1743.938" y="1650.153" transform="matrix(0.0266 -0.9996 0.9996 0.0266 111.31 3417.6558)" width="133.08" height="3.043"/>
|
||||
</g>
|
||||
<g>
|
||||
<path d="M1820.932,1588.251c-55.506-13.224-111.219-48.05-139.494-74.812c-25.038-23.696-37.368-42.419-37.49-42.605l2.548-1.663
|
||||
c0.12,0.184,12.28,18.63,37.034,42.059c27.99,26.491,83.148,60.967,138.108,74.061L1820.932,1588.251z"/>
|
||||
</g>
|
||||
<g>
|
||||
<path style="fill:#F78A5E;" d="M1923.654,987.727c1.432-8.982,22.772-14.065,29.312-14.098
|
||||
c4.221-0.021,14.53,80.212,18.52,83.226c0,0,0.785,0.884-9.606,3.169c-11.45,2.518-20.347-0.872-20.347-0.872
|
||||
S1922.222,996.709,1923.654,987.727z"/>
|
||||
</g>
|
||||
<g>
|
||||
<path d="M1540.274,1167.623l-35.421-34.538c-19.28-19.203-26.223-37.954-28.655-50.303c-2.653-13.469-0.72-22.299-0.636-22.668
|
||||
l2.968,0.672c-0.076,0.34-7.226,34.588,28.459,70.132l33.926,33.081c11.643-5.61,19.61-18.687,19.691-18.822
|
||||
c0.175-0.305,18.79-33.154,8.353-64.195c-10.491-31.201-43.368-53.189-43.699-53.407l1.675-2.54
|
||||
c1.387,0.915,34.077,22.766,44.907,54.978c10.882,32.364-7.814,65.322-8.616,66.709c-0.372,0.621-8.969,14.755-22.013,20.49
|
||||
L1540.274,1167.623z"/>
|
||||
</g>
|
||||
<g>
|
||||
<polygon points="1534.172,1031.806 1530.727,750.877 1532.718,750.817 1536.163,1031.746 "/>
|
||||
</g>
|
||||
<g>
|
||||
<path d="M1473.887,887.006c-1.144-1.113-27.884-27.491-14.745-49.457c12.987-21.71,69.069-19.749,71.451-19.655l-0.12,3.041
|
||||
c-0.563-0.02-56.656-1.992-68.719,18.175c-11.904,19.9,13.991,45.457,14.254,45.713L1473.887,887.006z"/>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<g>
|
||||
<g>
|
||||
<g>
|
||||
<path style="fill:#000100;" d="M1999.909,1469.931c0.496,0.234,1.099,0.207,1.585-0.123
|
||||
c16.361-11.095,28.216-40.272,28.713-41.509c0.33-0.823-0.071-1.75-0.891-2.086c-0.822-0.33-1.756,0.068-2.087,0.89
|
||||
c-0.118,0.294-11.989,29.506-27.536,40.05c-0.734,0.497-0.925,1.495-0.427,2.228
|
||||
C1999.433,1469.627,1999.658,1469.812,1999.909,1469.931z"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<g>
|
||||
<g>
|
||||
<g>
|
||||
<path style="fill:#000100;" d="M1977.76,1470.588c0.388,0.182,0.847,0.211,1.276,0.04
|
||||
c18.371-7.305,36.241-33.237,36.992-34.337c0.5-0.732,0.312-1.73-0.42-2.229c-0.732-0.504-1.73-0.312-2.23,0.419
|
||||
c-0.178,0.261-18.072,26.224-35.528,33.165c-0.824,0.328-1.226,1.261-0.898,2.084
|
||||
C1977.11,1470.123,1977.405,1470.42,1977.76,1470.588z"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<path d="M1960.061,1512.252c-0.831-0.105-20.602-2.756-41.339-19.032c-12.163-9.546-22.089-21.817-29.503-36.475
|
||||
c-9.238-18.262-14.573-40.292-15.859-65.479c-2.904-56.804,7.549-101.015,16.829-128.101
|
||||
c8.994-26.251,18.684-41.987,20.736-45.173c-3.238-11.664-38.39-139.896-33.156-189.591c1.63-15.496,7.805-31.312,17.386-44.533
|
||||
c9.358-12.912,20.814-21.48,31.434-23.506c8.302-1.585,19.996-0.027,28.577,16.38c1.895,3.629,3.55,7.849,4.921,12.547
|
||||
l0.05,0.244c0.044,0.366,4.528,37.009,13.682,70.107c4.344,15.73,9.063,28.222,14.026,37.129
|
||||
c17.443,31.321,63.483,123.234,73.902,164.834c10.15,40.521,4.218,213.437,3.96,220.777l-3.041-0.106
|
||||
c0.063-1.794,6.175-179.826-3.87-219.932c-10.349-41.322-56.222-132.875-73.608-164.092c-5.08-9.119-9.892-21.836-14.301-37.8
|
||||
c-8.944-32.341-13.455-68.014-13.755-70.426c-1.308-4.458-2.876-8.452-4.662-11.872c-6.107-11.677-14.859-16.794-25.31-14.801
|
||||
c-21.09,4.025-43.189,35.181-46.364,65.368c-5.308,50.403,32.86,187.738,33.246,189.12l0.191,0.681l-0.399,0.586
|
||||
c-0.108,0.157-10.868,16.115-20.808,45.171c-9.172,26.811-19.502,70.578-16.626,126.833
|
||||
c2.859,55.987,25.615,85.128,44.203,99.716c20.081,15.761,39.646,18.382,39.842,18.407L1960.061,1512.252z"/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<path style="fill:#F7597F;" d="M1196.431,1126.25l-437.359,89.492c-8.109,1.659-16.027-3.569-17.687-11.678l-54.039-264.097
|
||||
c-1.659-8.109,3.569-16.028,11.678-17.687l437.359-89.492c8.109-1.659,16.027,3.569,17.687,11.678l54.039,264.097
|
||||
C1209.768,1116.672,1204.539,1124.591,1196.431,1126.25z"/>
|
||||
</g>
|
||||
<g style="opacity:0.1;">
|
||||
|
||||
<ellipse transform="matrix(0.7071 -0.7071 0.7071 0.7071 -446.6819 970.1445)" style="fill:#FFFFFF;" cx="947.727" cy="1024.265" rx="106.076" ry="106.076"/>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<path style="fill:#FFFFFF;" d="M770.674,1113.261c-0.12,1.523-0.476,2.819-1.069,3.892c-0.593,1.073-1.393,1.934-2.402,2.585
|
||||
c-1.009,0.65-2.207,1.117-3.595,1.402c-1.388,0.284-2.68,0.326-3.875,0.127c-1.196-0.199-2.276-0.676-3.242-1.43
|
||||
c-0.967-0.754-1.804-1.806-2.512-3.159c-0.708-1.352-1.269-3.038-1.682-5.06l-1.009-4.932c-0.414-2.021-0.561-3.792-0.441-5.315
|
||||
c0.12-1.521,0.476-2.819,1.069-3.891c0.592-1.073,1.399-1.935,2.42-2.588c1.021-0.652,2.225-1.121,3.613-1.405
|
||||
c1.389-0.284,2.674-0.325,3.857-0.123c1.184,0.202,2.258,0.68,3.224,1.433c0.966,0.754,1.803,1.807,2.511,3.159
|
||||
c0.708,1.353,1.269,3.039,1.682,5.061l1.009,4.932C770.648,1109.968,770.794,1111.74,770.674,1113.261z M765.901,1103.695
|
||||
c-0.314-1.534-0.715-2.816-1.204-3.845c-0.489-1.029-1.046-1.835-1.67-2.418c-0.625-0.582-1.316-0.961-2.073-1.136
|
||||
c-0.756-0.175-1.56-0.176-2.41-0.002c-0.85,0.174-1.588,0.49-2.215,0.948c-0.627,0.459-1.113,1.078-1.459,1.859
|
||||
c-0.346,0.782-0.542,1.742-0.587,2.88c-0.045,1.138,0.089,2.474,0.403,4.009l1.009,4.932c0.314,1.535,0.715,2.817,1.204,3.845
|
||||
c0.489,1.029,1.046,1.835,1.671,2.418c0.625,0.583,1.316,0.961,2.072,1.136c0.757,0.176,1.56,0.176,2.41,0.002
|
||||
c0.85-0.174,1.588-0.49,2.215-0.948c0.627-0.458,1.113-1.077,1.46-1.859c0.346-0.781,0.541-1.741,0.586-2.88
|
||||
c0.045-1.138-0.089-2.475-0.403-4.009L765.901,1103.695z"/>
|
||||
<path style="fill:#FFFFFF;" d="M792.776,1108.739c-0.12,1.522-0.477,2.819-1.069,3.892c-0.593,1.073-1.393,1.934-2.402,2.585
|
||||
c-1.009,0.65-2.207,1.118-3.595,1.402c-1.388,0.284-2.68,0.326-3.875,0.127c-1.196-0.199-2.276-0.676-3.242-1.43
|
||||
c-0.967-0.754-1.803-1.806-2.511-3.159c-0.708-1.352-1.269-3.038-1.682-5.06l-1.009-4.932c-0.414-2.021-0.561-3.792-0.441-5.315
|
||||
c0.12-1.521,0.476-2.819,1.069-3.891c0.592-1.073,1.399-1.935,2.42-2.588c1.021-0.653,2.225-1.121,3.613-1.405
|
||||
c1.389-0.284,2.674-0.325,3.857-0.123c1.183,0.202,2.258,0.68,3.224,1.434c0.966,0.754,1.803,1.807,2.511,3.159
|
||||
c0.708,1.353,1.269,3.039,1.682,5.061l1.009,4.932C792.749,1105.446,792.896,1107.218,792.776,1108.739z M788.002,1099.173
|
||||
c-0.314-1.535-0.715-2.816-1.204-3.845c-0.489-1.029-1.045-1.835-1.67-2.418c-0.625-0.582-1.316-0.961-2.073-1.136
|
||||
c-0.756-0.175-1.56-0.176-2.41-0.002c-0.85,0.174-1.588,0.49-2.215,0.948c-0.627,0.459-1.114,1.078-1.459,1.859
|
||||
c-0.346,0.782-0.542,1.742-0.587,2.88c-0.045,1.139,0.089,2.474,0.403,4.009l1.009,4.932c0.314,1.535,0.715,2.817,1.204,3.845
|
||||
c0.489,1.029,1.045,1.835,1.671,2.418c0.625,0.583,1.316,0.961,2.072,1.136c0.757,0.175,1.56,0.176,2.41,0.002
|
||||
c0.85-0.174,1.588-0.49,2.215-0.948c0.627-0.458,1.113-1.077,1.46-1.859c0.346-0.781,0.541-1.741,0.586-2.879
|
||||
c0.045-1.138-0.089-2.475-0.403-4.009L788.002,1099.173z"/>
|
||||
<path style="fill:#FFFFFF;" d="M811.638,1089.313c0.294,1.437,0.183,2.76-0.333,3.969c-0.516,1.209-1.512,2.562-2.987,4.056
|
||||
l-3.031,3.094c-0.886,0.892-1.598,1.703-2.135,2.435c-0.537,0.732-0.933,1.441-1.187,2.127
|
||||
c-0.254,0.686-0.394,1.369-0.421,2.046c-0.027,0.678,0.042,1.419,0.207,2.222l0.157,0.767l12.311-2.519
|
||||
c0.195-0.04,0.376-0.02,0.545,0.06c0.169,0.08,0.273,0.217,0.313,0.412l0.329,1.608c0.04,0.195-0.001,0.368-0.122,0.52
|
||||
c-0.121,0.152-0.279,0.247-0.474,0.287l-14.476,2.962c-0.193,0.039-0.375,0.007-0.546-0.097
|
||||
c-0.172-0.104-0.277-0.254-0.317-0.448l-0.605-2.955c-0.388-1.896-0.311-3.723,0.233-5.482c0.544-1.758,1.68-3.511,3.409-5.258
|
||||
l3.193-3.237c0.652-0.665,1.183-1.242,1.592-1.732c0.409-0.489,0.715-0.951,0.919-1.385c0.204-0.434,0.318-0.87,0.344-1.306
|
||||
c0.025-0.436-0.016-0.921-0.126-1.457c-0.269-1.313-0.884-2.181-1.846-2.605c-0.962-0.424-2.43-0.434-4.402-0.031
|
||||
c-0.95,0.194-1.998,0.466-3.145,0.815c-1.146,0.349-2.216,0.701-3.209,1.056l-0.146,0.03c-0.39,0.08-0.63-0.1-0.719-0.54
|
||||
l-0.265-1.295c-0.04-0.198-0.004-0.359,0.11-0.485c0.114-0.125,0.263-0.233,0.448-0.323c0.875-0.436,1.878-0.835,3.01-1.195
|
||||
c1.132-0.36,2.246-0.652,3.341-0.876c2.971-0.608,5.27-0.532,6.897,0.225C810.129,1085.536,811.174,1087.047,811.638,1089.313z"
|
||||
/>
|
||||
<path style="fill:#FFFFFF;" d="M832.19,1106.611c-0.092,0.171-0.235,0.277-0.43,0.317l-2.009,0.411
|
||||
c-0.195,0.04-0.368-0.001-0.52-0.122c-0.152-0.121-0.247-0.279-0.287-0.474l-4.702-22.979l-4.294,3.01
|
||||
c-0.156,0.108-0.306,0.177-0.453,0.207c-0.219,0.045-0.356-0.066-0.411-0.335l-0.381-1.863
|
||||
c-0.025-0.122-0.013-0.219,0.035-0.293c0.048-0.073,0.126-0.159,0.233-0.257l4.978-3.759c0.262-0.206,0.503-0.332,0.722-0.376
|
||||
l1.498-0.306c0.195-0.04,0.373-0.006,0.534,0.1c0.161,0.107,0.262,0.257,0.302,0.452l5.263,25.719
|
||||
C832.308,1106.258,832.282,1106.44,832.19,1106.611z"/>
|
||||
</g>
|
||||
<g>
|
||||
<path style="fill:#FFFFFF;" d="M878.913,1091.113c-0.12,1.522-0.477,2.819-1.069,3.892c-0.593,1.073-1.393,1.934-2.402,2.585
|
||||
c-1.009,0.65-2.207,1.117-3.595,1.402c-1.388,0.284-2.68,0.326-3.875,0.127c-1.196-0.199-2.276-0.676-3.242-1.43
|
||||
c-0.967-0.754-1.803-1.806-2.512-3.159c-0.708-1.352-1.269-3.038-1.682-5.06l-1.009-4.932c-0.414-2.021-0.561-3.792-0.441-5.315
|
||||
c0.12-1.521,0.476-2.819,1.069-3.892c0.592-1.073,1.399-1.935,2.42-2.588c1.021-0.653,2.225-1.121,3.613-1.405
|
||||
c1.389-0.284,2.674-0.325,3.857-0.123c1.184,0.202,2.258,0.68,3.224,1.433c0.966,0.754,1.803,1.807,2.511,3.159
|
||||
c0.708,1.353,1.269,3.039,1.682,5.061l1.009,4.932C878.886,1087.821,879.033,1089.592,878.913,1091.113z M874.139,1081.547
|
||||
c-0.314-1.535-0.715-2.816-1.204-3.845c-0.489-1.029-1.045-1.835-1.67-2.418c-0.625-0.582-1.316-0.961-2.073-1.136
|
||||
c-0.756-0.175-1.56-0.176-2.41-0.002c-0.85,0.174-1.588,0.49-2.215,0.948c-0.627,0.458-1.114,1.078-1.459,1.859
|
||||
c-0.346,0.782-0.542,1.742-0.587,2.88c-0.045,1.139,0.089,2.475,0.403,4.009l1.009,4.932c0.314,1.534,0.715,2.817,1.204,3.845
|
||||
c0.489,1.029,1.046,1.835,1.671,2.418c0.625,0.583,1.316,0.961,2.072,1.136c0.757,0.176,1.56,0.176,2.41,0.002
|
||||
c0.85-0.174,1.588-0.49,2.215-0.948c0.627-0.458,1.113-1.077,1.46-1.859c0.346-0.781,0.541-1.741,0.586-2.88
|
||||
c0.045-1.138-0.089-2.475-0.403-4.009L874.139,1081.547z"/>
|
||||
<path style="fill:#FFFFFF;" d="M900.358,1088.685c-0.249,1.079-0.71,2.017-1.384,2.814c-0.674,0.798-1.534,1.462-2.58,1.993
|
||||
c-1.046,0.531-2.239,0.934-3.578,1.208c-1.169,0.239-2.287,0.391-3.352,0.455c-1.066,0.064-2.057,0.087-2.974,0.068
|
||||
c-0.206-0.009-0.386-0.056-0.543-0.139c-0.157-0.083-0.255-0.224-0.296-0.422l-0.257-1.258c-0.085-0.415,0.079-0.665,0.494-0.75
|
||||
l0.11-0.022c1.126-0.078,2.234-0.177,3.326-0.3c1.091-0.121,2.063-0.27,2.916-0.445c1.997-0.408,3.364-1.105,4.101-2.09
|
||||
c0.737-0.985,0.927-2.351,0.569-4.099l-0.156-0.764c-0.099-0.485-0.269-0.975-0.51-1.47c-0.241-0.494-0.57-0.925-0.989-1.296
|
||||
c-0.418-0.369-0.93-0.638-1.535-0.805c-0.605-0.167-1.322-0.166-2.15,0.003l-6.648,1.36c-0.463,0.095-0.739-0.077-0.829-0.516
|
||||
l-0.262-1.278c-0.095-0.463,0.089-0.742,0.552-0.836l6.648-1.36c0.877-0.179,1.568-0.459,2.074-0.841
|
||||
c0.505-0.381,0.867-0.816,1.085-1.303c0.217-0.487,0.335-0.997,0.352-1.532c0.018-0.534-0.023-1.044-0.123-1.529l-0.104-0.509
|
||||
c-0.308-1.504-0.965-2.513-1.971-3.028c-1.006-0.514-2.544-0.56-4.614-0.137c-1.072,0.219-2.053,0.477-2.944,0.774
|
||||
c-0.891,0.297-1.918,0.64-3.081,1.03l-0.11,0.022c-0.414,0.085-0.663-0.08-0.748-0.495l-0.265-1.296
|
||||
c-0.035-0.172,0.002-0.334,0.11-0.485c0.108-0.151,0.255-0.27,0.44-0.36c0.836-0.377,1.756-0.745,2.762-1.105
|
||||
c1.005-0.36,2.092-0.659,3.261-0.898c1.363-0.279,2.629-0.417,3.797-0.416c1.167,0.003,2.204,0.203,3.11,0.6
|
||||
c0.906,0.399,1.671,1.003,2.293,1.815c0.623,0.812,1.074,1.899,1.353,3.263l0.105,0.512c0.254,1.242,0.191,2.422-0.189,3.54
|
||||
c-0.38,1.119-1.085,2.074-2.115,2.869c0.722,0.182,1.348,0.447,1.876,0.796c0.528,0.349,0.978,0.751,1.351,1.208
|
||||
c0.373,0.457,0.674,0.941,0.906,1.451c0.231,0.511,0.402,1.035,0.512,1.57l0.157,0.767
|
||||
C900.592,1086.386,900.607,1087.607,900.358,1088.685z"/>
|
||||
<path style="fill:#FFFFFF;" d="M922.46,1084.163c-0.249,1.079-0.71,2.017-1.384,2.814c-0.674,0.798-1.534,1.462-2.58,1.993
|
||||
c-1.046,0.531-2.239,0.934-3.578,1.208c-1.169,0.239-2.287,0.391-3.352,0.455c-1.066,0.064-2.057,0.087-2.974,0.068
|
||||
c-0.205-0.009-0.386-0.056-0.542-0.139c-0.157-0.083-0.255-0.224-0.296-0.422l-0.257-1.258c-0.085-0.415,0.079-0.665,0.494-0.75
|
||||
l0.11-0.022c1.126-0.078,2.234-0.177,3.326-0.3c1.091-0.122,2.063-0.27,2.916-0.445c1.997-0.409,3.364-1.105,4.101-2.09
|
||||
c0.737-0.985,0.927-2.351,0.569-4.099l-0.156-0.764c-0.099-0.485-0.269-0.975-0.51-1.47c-0.241-0.494-0.57-0.926-0.988-1.296
|
||||
c-0.418-0.369-0.93-0.638-1.535-0.804c-0.605-0.167-1.322-0.166-2.15,0.003l-6.648,1.36c-0.463,0.095-0.739-0.077-0.829-0.516
|
||||
l-0.262-1.278c-0.095-0.463,0.089-0.742,0.552-0.836l6.648-1.36c0.877-0.179,1.568-0.459,2.073-0.841
|
||||
c0.505-0.381,0.867-0.816,1.085-1.303c0.217-0.487,0.335-0.997,0.353-1.532c0.017-0.534-0.023-1.044-0.123-1.529l-0.104-0.509
|
||||
c-0.308-1.504-0.965-2.513-1.971-3.028c-1.006-0.514-2.544-0.56-4.614-0.137c-1.072,0.219-2.053,0.477-2.944,0.774
|
||||
c-0.891,0.297-1.918,0.64-3.082,1.03l-0.11,0.022c-0.414,0.085-0.663-0.08-0.748-0.495l-0.265-1.296
|
||||
c-0.035-0.172,0.001-0.334,0.11-0.485c0.108-0.151,0.255-0.27,0.44-0.36c0.836-0.377,1.756-0.745,2.761-1.105
|
||||
c1.005-0.36,2.092-0.659,3.261-0.898c1.363-0.279,2.629-0.417,3.797-0.416c1.167,0.003,2.204,0.203,3.111,0.6
|
||||
c0.906,0.399,1.671,1.003,2.293,1.815c0.623,0.812,1.074,1.899,1.353,3.263l0.105,0.512c0.254,1.242,0.191,2.422-0.189,3.54
|
||||
c-0.38,1.119-1.085,2.074-2.115,2.869c0.722,0.182,1.348,0.447,1.876,0.796c0.528,0.349,0.978,0.751,1.351,1.208
|
||||
c0.373,0.457,0.674,0.941,0.906,1.451c0.231,0.511,0.402,1.035,0.511,1.57l0.157,0.767
|
||||
C922.694,1081.864,922.709,1083.084,922.46,1084.163z"/>
|
||||
<path style="fill:#FFFFFF;" d="M940.429,1084.464c-0.092,0.171-0.236,0.277-0.43,0.317l-2.009,0.411
|
||||
c-0.195,0.04-0.368-0.001-0.52-0.122c-0.152-0.121-0.247-0.279-0.287-0.474l-4.702-22.979l-4.294,3.01
|
||||
c-0.156,0.108-0.306,0.177-0.453,0.207c-0.219,0.045-0.356-0.066-0.411-0.335l-0.381-1.863
|
||||
c-0.025-0.122-0.013-0.219,0.035-0.293c0.048-0.073,0.126-0.159,0.233-0.257l4.978-3.759c0.262-0.206,0.503-0.331,0.722-0.376
|
||||
l1.498-0.306c0.195-0.04,0.373-0.006,0.534,0.1c0.161,0.107,0.262,0.257,0.302,0.452l5.262,25.719
|
||||
C940.547,1084.11,940.521,1084.293,940.429,1084.464z"/>
|
||||
</g>
|
||||
<g>
|
||||
<path style="fill:#FFFFFF;" d="M987.504,1067.524l-3.178,0.65l1.331,6.502c0.04,0.195,0.019,0.377-0.062,0.545
|
||||
c-0.081,0.169-0.22,0.274-0.417,0.314l-2.142,0.438c-0.197,0.04-0.366-0.001-0.507-0.125c-0.14-0.123-0.231-0.282-0.271-0.477
|
||||
l-1.331-6.502l-10.526,2.154c-0.558,0.114-0.968,0.09-1.229-0.072c-0.261-0.162-0.446-0.511-0.556-1.047l-0.284-1.388
|
||||
c-0.105-0.512-0.051-0.992,0.16-1.441l7.68-17.366c0.067-0.166,0.161-0.325,0.281-0.476c0.121-0.151,0.339-0.26,0.655-0.324
|
||||
l2.18-0.446c0.641-0.131,1.016,0.071,1.126,0.605l3.334,16.295l3.178-0.65c0.195-0.04,0.372-0.012,0.531,0.084
|
||||
c0.159,0.096,0.259,0.243,0.299,0.44l0.303,1.48C988.153,1067.159,987.967,1067.429,987.504,1067.524z M977.841,1053.591
|
||||
c-0.03-0.146-0.094-0.209-0.191-0.189l-0.036,0.007c-0.097,0.02-0.179,0.113-0.247,0.279l-5.958,13.667
|
||||
c-0.053,0.112-0.07,0.217-0.05,0.313c0.03,0.146,0.142,0.199,0.336,0.159l8.696-1.779L977.841,1053.591z"/>
|
||||
<path style="fill:#FFFFFF;" d="M1008.926,1064.796c-0.12,1.459-0.492,2.708-1.117,3.749c-0.625,1.041-1.493,1.885-2.603,2.531
|
||||
c-1.111,0.646-2.447,1.128-4.005,1.447c-1.461,0.299-2.697,0.488-3.709,0.567c-1.011,0.078-1.913,0.109-2.703,0.09
|
||||
c-0.254,0.001-0.476-0.031-0.666-0.095c-0.191-0.064-0.309-0.207-0.354-0.429l-0.295-1.444
|
||||
c-0.045-0.219-0.018-0.396,0.082-0.531c0.099-0.135,0.246-0.221,0.441-0.261c0.048-0.01,0.086-0.011,0.113-0.004
|
||||
c0.027,0.007,0.064,0.006,0.113-0.004c0.966,0.006,1.975-0.048,3.031-0.164c1.055-0.114,2.167-0.291,3.336-0.53
|
||||
c1.413-0.289,2.492-0.718,3.238-1.287c0.746-0.569,1.267-1.25,1.561-2.043c0.294-0.793,0.401-1.698,0.32-2.717
|
||||
c-0.082-1.019-0.244-2.123-0.487-3.312l-0.082-0.402c-0.745,0.33-1.545,0.665-2.401,1.005c-0.857,0.341-1.845,0.625-2.965,0.854
|
||||
c-1.194,0.244-2.357,0.355-3.49,0.334c-1.134-0.021-2.175-0.24-3.122-0.655c-0.948-0.415-1.766-1.066-2.455-1.953
|
||||
c-0.69-0.887-1.183-2.06-1.482-3.522l-0.045-0.219c-0.593-2.898-0.32-5.314,0.82-7.247c1.139-1.933,3.207-3.206,6.203-3.819
|
||||
c2.752-0.563,5.09-0.172,7.014,1.172c1.924,1.345,3.253,3.806,3.986,7.386l1.308,6.393
|
||||
C1008.908,1061.635,1009.047,1063.338,1008.926,1064.796z M1003.908,1053.941c-0.303-1.481-0.677-2.688-1.121-3.621
|
||||
c-0.445-0.933-0.964-1.643-1.559-2.128c-0.594-0.486-1.263-0.779-2.007-0.88c-0.744-0.1-1.554-0.061-2.431,0.118
|
||||
c-0.853,0.175-1.604,0.442-2.253,0.802c-0.649,0.361-1.172,0.847-1.566,1.459c-0.395,0.612-0.642,1.359-0.741,2.239
|
||||
c-0.1,0.88-0.022,1.939,0.231,3.178l0.045,0.218c0.199,0.971,0.523,1.752,0.974,2.343c0.45,0.591,0.982,1.02,1.595,1.286
|
||||
c0.613,0.267,1.27,0.411,1.972,0.431c0.702,0.021,1.418-0.044,2.149-0.193c0.438-0.09,0.91-0.205,1.413-0.346
|
||||
c0.504-0.141,0.999-0.293,1.486-0.456c0.487-0.162,0.947-0.333,1.38-0.51c0.433-0.177,0.8-0.334,1.102-0.472L1003.908,1053.941z
|
||||
"/>
|
||||
<path style="fill:#FFFFFF;" d="M1028.116,1045.017c0.294,1.437,0.183,2.76-0.333,3.969c-0.516,1.21-1.512,2.562-2.987,4.056
|
||||
l-3.031,3.094c-0.886,0.892-1.598,1.703-2.135,2.435c-0.537,0.732-0.933,1.441-1.187,2.127
|
||||
c-0.254,0.687-0.394,1.369-0.421,2.046c-0.027,0.678,0.042,1.419,0.207,2.223l0.157,0.767l12.311-2.519
|
||||
c0.195-0.04,0.376-0.02,0.545,0.06c0.169,0.08,0.273,0.217,0.313,0.412l0.329,1.607c0.04,0.195-0.001,0.368-0.122,0.52
|
||||
c-0.121,0.152-0.279,0.247-0.474,0.287l-14.476,2.962c-0.193,0.039-0.375,0.007-0.546-0.097
|
||||
c-0.172-0.104-0.277-0.254-0.317-0.448l-0.605-2.955c-0.388-1.896-0.311-3.724,0.233-5.482c0.544-1.758,1.68-3.511,3.409-5.258
|
||||
l3.193-3.237c0.652-0.665,1.183-1.242,1.592-1.732c0.409-0.489,0.715-0.951,0.919-1.385c0.204-0.434,0.318-0.87,0.344-1.306
|
||||
c0.025-0.435-0.017-0.921-0.126-1.456c-0.269-1.313-0.884-2.181-1.846-2.605c-0.962-0.424-2.43-0.434-4.402-0.031
|
||||
c-0.95,0.194-1.998,0.466-3.145,0.815c-1.146,0.349-2.216,0.701-3.209,1.056l-0.146,0.03c-0.39,0.08-0.63-0.1-0.72-0.54
|
||||
l-0.265-1.295c-0.041-0.198-0.004-0.359,0.11-0.485c0.113-0.125,0.262-0.233,0.448-0.323c0.875-0.436,1.878-0.835,3.009-1.195
|
||||
c1.132-0.36,2.246-0.652,3.342-0.876c2.971-0.608,5.27-0.532,6.897,0.225C1026.607,1041.24,1027.652,1042.752,1028.116,1045.017
|
||||
z"/>
|
||||
<path style="fill:#FFFFFF;" d="M1048.668,1062.316c-0.092,0.171-0.235,0.277-0.43,0.317l-2.009,0.411
|
||||
c-0.195,0.04-0.368-0.001-0.52-0.122c-0.152-0.121-0.247-0.279-0.287-0.474l-4.702-22.979l-4.294,3.01
|
||||
c-0.156,0.108-0.306,0.177-0.453,0.207c-0.219,0.045-0.356-0.066-0.411-0.335l-0.381-1.863
|
||||
c-0.025-0.122-0.013-0.219,0.035-0.293c0.048-0.073,0.126-0.159,0.233-0.257l4.978-3.759c0.262-0.206,0.503-0.331,0.722-0.376
|
||||
l1.498-0.307c0.195-0.04,0.373-0.006,0.534,0.1c0.161,0.107,0.262,0.257,0.302,0.452l5.263,25.719
|
||||
C1048.786,1061.962,1048.76,1062.145,1048.668,1062.316z"/>
|
||||
</g>
|
||||
<g>
|
||||
<path style="fill:#FFFFFF;" d="M1092.151,1031.914c0.294,1.437,0.183,2.76-0.333,3.969c-0.516,1.209-1.512,2.562-2.987,4.056
|
||||
l-3.031,3.094c-0.886,0.892-1.598,1.703-2.135,2.435c-0.537,0.732-0.933,1.441-1.187,2.127
|
||||
c-0.254,0.687-0.394,1.369-0.421,2.046c-0.027,0.678,0.042,1.419,0.207,2.223l0.157,0.767l12.311-2.519
|
||||
c0.195-0.04,0.376-0.02,0.545,0.06c0.169,0.08,0.273,0.217,0.313,0.412l0.329,1.608c0.04,0.195-0.001,0.368-0.122,0.52
|
||||
c-0.121,0.152-0.279,0.247-0.474,0.287l-14.476,2.962c-0.193,0.04-0.375,0.007-0.546-0.097
|
||||
c-0.172-0.104-0.277-0.254-0.317-0.448l-0.605-2.955c-0.388-1.896-0.311-3.724,0.233-5.482c0.544-1.758,1.68-3.51,3.409-5.258
|
||||
l3.193-3.237c0.652-0.665,1.183-1.242,1.592-1.732c0.409-0.489,0.715-0.951,0.919-1.385c0.204-0.434,0.318-0.87,0.344-1.306
|
||||
c0.025-0.435-0.016-0.921-0.126-1.456c-0.269-1.313-0.884-2.181-1.846-2.605c-0.962-0.424-2.43-0.434-4.402-0.031
|
||||
c-0.95,0.194-1.998,0.466-3.145,0.815c-1.146,0.349-2.216,0.701-3.209,1.056l-0.146,0.03c-0.39,0.08-0.63-0.1-0.719-0.54
|
||||
l-0.265-1.295c-0.041-0.198-0.004-0.359,0.11-0.485c0.113-0.125,0.262-0.233,0.448-0.323c0.875-0.436,1.878-0.835,3.01-1.195
|
||||
s2.246-0.652,3.341-0.876c2.971-0.608,5.27-0.532,6.897,0.225C1090.642,1028.137,1091.688,1029.649,1092.151,1031.914z"/>
|
||||
<path style="fill:#FFFFFF;" d="M1117.493,1042.295c-0.12,1.522-0.476,2.819-1.069,3.892c-0.593,1.073-1.393,1.934-2.402,2.585
|
||||
c-1.009,0.65-2.207,1.118-3.595,1.402c-1.388,0.284-2.68,0.326-3.875,0.127c-1.196-0.199-2.276-0.676-3.242-1.43
|
||||
c-0.967-0.754-1.803-1.806-2.512-3.159c-0.708-1.352-1.269-3.038-1.682-5.06l-1.009-4.932c-0.414-2.021-0.561-3.792-0.441-5.315
|
||||
c0.12-1.521,0.476-2.819,1.069-3.891c0.592-1.073,1.399-1.935,2.42-2.588c1.021-0.653,2.225-1.121,3.613-1.405
|
||||
c1.389-0.284,2.674-0.325,3.857-0.123c1.183,0.202,2.258,0.68,3.224,1.433c0.966,0.754,1.803,1.807,2.511,3.159
|
||||
c0.708,1.353,1.269,3.039,1.682,5.061l1.009,4.932C1117.466,1039.003,1117.613,1040.774,1117.493,1042.295z M1112.719,1032.729
|
||||
c-0.314-1.534-0.715-2.816-1.204-3.845c-0.489-1.029-1.045-1.835-1.67-2.418c-0.625-0.582-1.316-0.961-2.073-1.136
|
||||
c-0.756-0.175-1.56-0.176-2.41-0.002c-0.85,0.174-1.588,0.49-2.215,0.948c-0.627,0.459-1.114,1.078-1.459,1.859
|
||||
c-0.346,0.782-0.542,1.742-0.587,2.88c-0.045,1.139,0.089,2.474,0.403,4.009l1.009,4.932c0.314,1.534,0.715,2.816,1.204,3.845
|
||||
c0.489,1.029,1.046,1.835,1.671,2.418c0.625,0.583,1.316,0.961,2.072,1.136c0.757,0.175,1.56,0.176,2.41,0.002
|
||||
c0.85-0.174,1.588-0.49,2.215-0.948c0.627-0.458,1.113-1.078,1.46-1.859c0.346-0.781,0.541-1.741,0.586-2.88
|
||||
c0.045-1.138-0.089-2.475-0.403-4.009L1112.719,1032.729z"/>
|
||||
<path style="fill:#FFFFFF;" d="M1136.354,1022.869c0.294,1.437,0.183,2.76-0.333,3.969c-0.516,1.209-1.512,2.562-2.987,4.056
|
||||
l-3.031,3.094c-0.886,0.892-1.598,1.703-2.135,2.435c-0.537,0.732-0.933,1.441-1.187,2.127
|
||||
c-0.254,0.687-0.394,1.369-0.421,2.046c-0.027,0.678,0.042,1.419,0.207,2.223l0.157,0.767l12.311-2.519
|
||||
c0.195-0.04,0.376-0.02,0.545,0.06c0.169,0.08,0.273,0.217,0.313,0.412l0.329,1.608c0.04,0.195-0.001,0.368-0.122,0.52
|
||||
c-0.121,0.152-0.279,0.247-0.474,0.287l-14.476,2.962c-0.193,0.04-0.375,0.007-0.546-0.097
|
||||
c-0.172-0.104-0.277-0.254-0.317-0.448l-0.605-2.955c-0.388-1.896-0.311-3.724,0.233-5.482c0.544-1.758,1.68-3.51,3.409-5.258
|
||||
l3.193-3.237c0.652-0.665,1.183-1.242,1.592-1.732c0.409-0.489,0.715-0.951,0.919-1.385c0.204-0.434,0.318-0.87,0.344-1.306
|
||||
c0.025-0.435-0.016-0.921-0.126-1.456c-0.269-1.313-0.884-2.181-1.847-2.605c-0.962-0.424-2.43-0.434-4.402-0.031
|
||||
c-0.95,0.194-1.998,0.466-3.145,0.815c-1.146,0.349-2.216,0.701-3.209,1.056l-0.146,0.03c-0.39,0.08-0.63-0.1-0.72-0.54
|
||||
l-0.265-1.295c-0.04-0.198-0.004-0.359,0.11-0.485c0.113-0.126,0.263-0.233,0.448-0.323c0.875-0.436,1.878-0.835,3.01-1.195
|
||||
c1.132-0.36,2.246-0.652,3.341-0.876c2.971-0.608,5.27-0.532,6.897,0.225
|
||||
C1134.845,1019.093,1135.891,1020.604,1136.354,1022.869z"/>
|
||||
<path style="fill:#FFFFFF;" d="M1156.907,1040.168c-0.092,0.171-0.235,0.277-0.43,0.317l-2.009,0.411
|
||||
c-0.196,0.04-0.368-0.001-0.52-0.122c-0.152-0.121-0.247-0.279-0.287-0.474l-4.702-22.978l-4.294,3.01
|
||||
c-0.156,0.108-0.306,0.177-0.453,0.207c-0.219,0.045-0.356-0.066-0.411-0.335l-0.381-1.863
|
||||
c-0.025-0.122-0.013-0.219,0.035-0.293c0.048-0.073,0.126-0.159,0.233-0.257l4.978-3.759c0.262-0.206,0.503-0.331,0.722-0.376
|
||||
l1.498-0.306c0.195-0.04,0.373-0.006,0.534,0.1c0.161,0.107,0.262,0.257,0.302,0.452l5.262,25.719
|
||||
C1157.025,1039.814,1156.999,1039.997,1156.907,1040.168z"/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
|
||||
<rect x="761.376" y="1134.017" transform="matrix(0.9797 -0.2005 0.2005 0.9797 -213.289 183.5005)" style="fill:#FFFFFF;" width="76.121" height="21.806"/>
|
||||
</g>
|
||||
<g>
|
||||
|
||||
<rect x="1053.412" y="856.182" transform="matrix(0.9797 -0.2005 0.2005 0.9797 -150.7902 236.315)" style="fill:#FFFFFF;" width="76.121" height="13.083"/>
|
||||
</g>
|
||||
<g>
|
||||
|
||||
<rect x="998.969" y="885.065" transform="matrix(0.9797 -0.2005 0.2005 0.9797 -157.0789 231.9744)" style="fill:#FFFFFF;" width="135.855" height="13.083"/>
|
||||
</g>
|
||||
<g>
|
||||
|
||||
<rect x="765.123" y="1139.3" transform="matrix(0.9797 -0.2005 0.2005 0.9797 -211.6603 210.1493)" style="fill:#FFFFFF;" width="333.427" height="21.806"/>
|
||||
</g>
|
||||
<g>
|
||||
<path style="fill:#FFFFFF;" d="M1148.897,1105.788c1.207,5.899-2.597,11.66-8.496,12.867c-5.899,1.207-11.66-2.597-12.867-8.496
|
||||
c-1.207-5.899,2.597-11.66,8.496-12.867C1141.929,1096.085,1147.69,1099.888,1148.897,1105.788z"/>
|
||||
</g>
|
||||
<g>
|
||||
<path style="fill:#FFFFFF;" d="M1178.805,1099.668c1.207,5.899-2.597,11.66-8.496,12.867c-5.899,1.207-11.66-2.597-12.867-8.496
|
||||
c-1.207-5.899,2.597-11.66,8.496-12.867C1171.837,1089.965,1177.598,1093.769,1178.805,1099.668z"/>
|
||||
</g>
|
||||
<g>
|
||||
<path style="fill:#FECC86;" d="M783.056,935.007l7.604,37.16c0.386,1.888-0.828,3.739-2.725,4.127l-63.57,13.008
|
||||
c-1.896,0.388-3.749-0.835-4.136-2.723l-7.604-37.16c-0.388-1.896,0.835-3.749,2.732-4.137l63.57-13.008
|
||||
C780.824,931.885,782.668,933.11,783.056,935.007z"/>
|
||||
</g>
|
||||
<g>
|
||||
<polygon style="fill:#EFA348;" points="749.265,959.219 716.022,966.021 715.665,964.273 747.159,957.829 744.874,946.662
|
||||
746.622,946.304 "/>
|
||||
</g>
|
||||
<g>
|
||||
<polygon style="fill:#EFA348;" points="759.836,982.043 758.088,982.4 754.552,965.118 787.824,958.31 788.182,960.058
|
||||
756.658,966.508 "/>
|
||||
</g>
|
||||
<g>
|
||||
|
||||
<rect x="760.182" y="936.122" transform="matrix(0.9797 -0.2005 0.2005 0.9797 -174.125 171.7873)" style="fill:#EFA348;" width="1.784" height="18.9"/>
|
||||
</g>
|
||||
<g>
|
||||
|
||||
<rect x="761.06" y="943.41" transform="matrix(0.9797 -0.2005 0.2005 0.9797 -173.613 174.1081)" style="fill:#EFA348;" width="23.653" height="1.784"/>
|
||||
</g>
|
||||
<g>
|
||||
<polygon style="fill:#EFA348;" points="738.729,978.271 736.982,978.628 735.756,972.64 718.115,976.25 717.758,974.502
|
||||
737.147,970.534 "/>
|
||||
</g>
|
||||
<g>
|
||||
<path style="fill:#1D1D1B;" d="M1198.284,1127.236l-440.529,90.141c-7.945,1.626-15.731-3.515-17.356-11.46L685.71,938.65
|
||||
c-1.626-7.945,3.515-15.731,11.46-17.356l440.53-90.141c7.945-1.626,15.73,3.515,17.356,11.46l54.688,267.268
|
||||
C1211.37,1117.825,1206.229,1125.611,1198.284,1127.236z M697.707,923.915c-6.499,1.33-10.704,7.699-9.375,14.198l54.688,267.268
|
||||
c1.33,6.499,7.699,10.704,14.198,9.374l440.529-90.141c6.499-1.33,10.704-7.699,9.375-14.198l-54.688-267.268
|
||||
c-1.33-6.499-7.699-10.704-14.198-9.375L697.707,923.915z"/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
|
||||
<ellipse transform="matrix(0.7071 -0.7071 0.7071 0.7071 -234.9784 1533.5594)" style="fill:#FFFFFF;" cx="1733.681" cy="1050.424" rx="130.755" ry="130.755"/>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<path style="fill:#5AB3BF;" d="M1709.386,1117.035l-49.314-68.58c-2.335-3.246-1.595-7.77,1.651-10.105
|
||||
c3.246-2.334,7.771-1.595,10.104,1.651l38.089,52.969l95.221-120.798c2.475-3.141,7.028-3.679,10.167-1.204
|
||||
c3.141,2.476,3.68,7.027,1.204,10.168L1709.386,1117.035z"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<polygon style="fill:#FFFFFF;" points="1778.273,1157.795 1832.192,1211.071 1808.574,1126.452 "/>
|
||||
<rect x="1626.583" y="1281.68" style="fill:#FFFFFF;" width="223.607" height="11.112"/>
|
||||
<rect x="1626.583" y="1327.67" style="fill:#FFFFFF;" width="223.607" height="11.112"/>
|
||||
<g>
|
||||
<g>
|
||||
<path style="fill:#6095AB;" d="M1155.167,674.34c-11.273-0.925-22.207-3.989-32.498-9.106l2.71-5.453
|
||||
c9.597,4.771,19.787,7.628,30.286,8.49L1155.167,674.34z M1188.727,671.734l-1.462-5.911c10.375-2.567,20.075-6.893,28.827-12.86
|
||||
l3.43,5.032C1210.17,664.371,1199.808,668.993,1188.727,671.734z M1095.434,645.481c-7.564-7.471-14.542-16.309-20.741-26.271
|
||||
l5.17-3.217c5.947,9.557,12.625,18.02,19.85,25.155L1095.434,645.481z M1243.671,634.498l-4.916-3.593
|
||||
c6.146-8.408,11.054-17.911,14.588-28.244l5.761,1.971C1255.371,615.546,1250.178,625.594,1243.671,634.498z M769.482,622.276
|
||||
l-6.076-0.402c0.726-10.963,2.112-22.04,4.12-32.924l5.988,1.105C771.549,600.707,770.192,611.547,769.482,622.276z
|
||||
M1059.793,589.381c-3.989-10.088-7.289-20.78-9.809-31.781l5.936-1.359c2.452,10.701,5.66,21.098,9.536,30.901L1059.793,589.381
|
||||
z M1265.608,571.736l-6.075-0.407c0.193-2.891,0.291-5.851,0.291-8.798c0-7.642-0.658-15.45-1.955-23.208l6.006-1.004
|
||||
c1.352,8.09,2.038,16.236,2.038,24.213C1265.914,565.613,1265.812,568.71,1265.608,571.736z M781.227,558.504l-5.823-1.779
|
||||
c3.213-10.513,7.068-20.986,11.458-31.127l5.588,2.419C788.15,537.951,784.374,548.208,781.227,558.504z M1044.792,524.826
|
||||
c-1.176-11.123-2.011-22.201-2.483-32.925l6.083-0.268c0.466,10.6,1.292,21.553,2.455,32.553L1044.792,524.826z
|
||||
M1249.276,508.349c-3.809-9.78-8.661-19.478-14.424-28.824l5.183-3.196c5.955,9.658,10.973,19.688,14.915,29.81
|
||||
L1249.276,508.349z M806.994,498.958l-5.286-3.022c5.475-9.577,11.54-18.945,18.028-27.843l4.92,3.587
|
||||
C818.3,480.398,812.358,489.576,806.994,498.958z M1800.472,498.815l-5.804-1.842c3.331-10.5,5.929-21.084,7.717-31.459
|
||||
l6.001,1.035C1806.55,477.195,1803.887,488.051,1800.472,498.815z M1048.068,459.025l-6.087-0.155
|
||||
c0.285-11.183,0.994-22.274,2.109-32.963l6.057,0.632C1049.049,437.069,1048.35,448,1048.068,459.025z M1215.729,453.478
|
||||
c-6.843-7.957-14.507-15.703-22.778-23.024l4.035-4.56c8.478,7.503,16.337,15.448,23.36,23.613L1215.729,453.478z
|
||||
M845.237,446.537l-4.487-4.116c7.497-8.173,15.502-15.955,23.794-23.129l3.984,4.605
|
||||
C860.412,430.918,852.576,438.535,845.237,446.537z M1811.558,433.441l-6.089-0.112c0.023-1.214,0.033-2.424,0.033-3.632
|
||||
c0.001-9.565-0.692-19.174-2.06-28.559l6.025-0.878c1.411,9.676,2.125,19.58,2.124,29.438
|
||||
C1811.591,430.942,1811.581,432.19,1811.558,433.441z M1167.405,410.439c-7.551-5.254-15.619-10.344-23.978-15.128
|
||||
c-1.206-0.69-2.412-1.369-3.62-2.036l2.945-5.329c1.233,0.681,2.466,1.375,3.699,2.079c8.515,4.874,16.735,10.06,24.432,15.416
|
||||
L1167.405,410.439z M894.301,404.162l-3.403-5.05c9.238-6.224,18.876-11.872,28.646-16.787l2.737,5.44
|
||||
C912.74,392.564,903.327,398.081,894.301,404.162z M1054.917,394.37l-5.977-1.163c2.134-10.965,4.785-21.776,7.881-32.131
|
||||
l5.834,1.743C1059.616,372.984,1057.012,383.599,1054.917,394.37z M1110.513,379.355c-10.127-4.048-20.496-7.407-30.817-9.982
|
||||
l1.474-5.908c10.589,2.642,21.222,6.086,31.604,10.237L1110.513,379.355z M952.117,375.153l-1.987-5.756
|
||||
c10.515-3.629,21.302-6.529,32.064-8.618l1.161,5.977C972.872,368.792,962.362,371.617,952.117,375.153z M1796.191,369.708
|
||||
c-3.211-10.101-7.303-20.166-12.164-29.917l5.449-2.716c5,10.031,9.212,20.39,12.518,30.789L1796.191,369.708z M1047.813,363.795
|
||||
c-10.717-1.084-21.608-1.384-32.342-0.887l-0.281-6.083c11.031-0.511,22.222-0.203,33.235,0.912L1047.813,363.795z
|
||||
M1073.588,332.263l-5.612-2.362c4.308-10.24,9.211-20.251,14.572-29.754l5.304,2.991
|
||||
C1082.604,312.439,1077.805,322.237,1073.588,332.263z M1767.563,311.927c-5.968-8.682-12.703-17.264-20.016-25.507l4.555-4.041
|
||||
c7.479,8.43,14.369,17.211,20.479,26.1L1767.563,311.927z M1105.434,275.904l-4.907-3.605
|
||||
c6.515-8.87,13.651-17.444,21.209-25.486l4.437,4.17C1118.783,258.847,1111.805,267.231,1105.434,275.904z M1724.681,263.348
|
||||
c-7.83-7.11-16.28-14.068-25.115-20.681l3.649-4.875c8.989,6.728,17.588,13.809,25.56,21.047L1724.681,263.348z
|
||||
M1149.746,228.707l-3.916-4.663c8.345-7.008,17.273-13.684,26.538-19.843l3.37,5.071
|
||||
C1166.662,215.306,1157.917,221.844,1149.746,228.707z M1672.677,224.281c-9.034-5.645-18.552-11.097-28.292-16.203l2.828-5.393
|
||||
c9.875,5.177,19.528,10.706,28.692,16.432L1672.677,224.281z M1614.975,193.952c-9.823-4.312-20.019-8.395-30.304-12.136
|
||||
l2.081-5.722c10.407,3.786,20.727,7.918,30.671,12.283L1614.975,193.952z M1203.708,192.737l-2.826-5.394
|
||||
c9.597-5.027,19.703-9.709,30.038-13.918l2.297,5.64C1223.061,183.201,1213.133,187.801,1203.708,192.737z M1553.651,171.607
|
||||
c-10.358-3.062-20.991-5.863-31.602-8.323l1.375-5.932c10.73,2.489,21.48,5.32,31.954,8.416L1553.651,171.607z M1263.884,168.143
|
||||
l-1.793-5.819c10.342-3.187,21.099-6.028,31.975-8.444l1.32,5.944C1284.669,162.204,1274.07,165.003,1263.884,168.143z
|
||||
M1490.016,156.838c-10.711-1.83-21.595-3.364-32.35-4.559l0.673-6.052c10.873,1.208,21.875,2.758,32.702,4.608L1490.016,156.838
|
||||
z M1327.458,153.952l-0.878-6.026c10.731-1.562,21.782-2.779,32.843-3.617l0.462,6.071
|
||||
C1348.961,151.208,1338.051,152.41,1327.458,153.952z M1425.118,149.64c-10.9-0.555-21.841-0.778-32.628-0.66l-0.064-6.089
|
||||
c10.891-0.118,21.977,0.107,33.002,0.668L1425.118,149.64z"/>
|
||||
</g>
|
||||
<g>
|
||||
<path style="fill:#6095AB;" d="M1780.973,545.356l-5.402-2.809c2.521-4.851,4.922-9.775,7.136-14.635l5.542,2.524
|
||||
C1785.992,535.392,1783.543,540.412,1780.973,545.356z"/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<g>
|
||||
<g>
|
||||
<g>
|
||||
<g>
|
||||
<g>
|
||||
<g>
|
||||
<g>
|
||||
<path style="fill:#CC154A;" d="M1728.18,576.527l-33.819,58.29c0,0,48.944-39.19,50.122-40.13
|
||||
c1.17-0.934,44.693-64.258,44.693-64.258l19.348-5.538l-30.713-8.23L1728.18,576.527z"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<g>
|
||||
<g>
|
||||
<g>
|
||||
<g>
|
||||
<g>
|
||||
<g>
|
||||
<polygon style="fill:#F7597F;" points="1777.812,516.66 1755.992,495.87 1694.361,634.817 "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<g>
|
||||
<g>
|
||||
<g>
|
||||
<g>
|
||||
<g>
|
||||
<g>
|
||||
<polygon style="fill:#F7597F;" points="1818.064,557.857 1789.176,530.428 1694.361,634.817 "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 56 KiB |
@ -2623,3 +2623,9 @@ export const WineGlassCrackSolid: React.FC<IconProps> = ({ className, onClick })
|
||||
</svg>;
|
||||
return Icon;
|
||||
}
|
||||
export const ClockRotateLeftSolid: React.FC<IconProps> = ({ className, onClick }) => {
|
||||
const Icon = <svg onClick={(e) => onClick && onClick(e)} className={`${className}`} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
|
||||
<path d="M75 75L41 41C25.9 25.9 0 36.6 0 57.9V168c0 13.3 10.7 24 24 24H134.1c21.4 0 32.1-25.9 17-41l-30.8-30.8C155 85.5 203 64 256 64c106 0 192 86 192 192s-86 192-192 192c-40.8 0-78.6-12.7-109.7-34.4c-14.5-10.1-34.4-6.6-44.6 7.9s-6.6 34.4 7.9 44.6C151.2 495 201.7 512 256 512c141.4 0 256-114.6 256-256S397.4 0 256 0C185.3 0 121.3 28.7 75 75zm181 53c-13.3 0-24 10.7-24 24V256c0 6.4 2.5 12.5 7 17l72 72c9.4 9.4 24.6 9.4 33.9 0s9.4-24.6 0-33.9l-65-65V152c0-13.3-10.7-24-24-24z" />
|
||||
</svg>;
|
||||
return Icon;
|
||||
}
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
import { useState, useEffect } from "react"
|
||||
|
||||
interface RadioButton {
|
||||
forId: string,
|
||||
@ -15,25 +14,14 @@ interface RadioButton {
|
||||
|
||||
const RadioButton: React.FunctionComponent<RadioButton> = ({ children, title, forId, value, name, checked = false, labelClass, inputClass, titleClass, onChange,}) => {
|
||||
|
||||
const [isChecked, setIsChecked] = useState(checked);
|
||||
|
||||
const toggleCheckbox = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setIsChecked(event.target.checked);
|
||||
onChange(event.target.checked);
|
||||
onChange(!checked);
|
||||
}
|
||||
|
||||
// useEffects
|
||||
useEffect(() => {
|
||||
if (isChecked !== checked) {
|
||||
setIsChecked(checked);
|
||||
// onChange(checked);
|
||||
}
|
||||
}, [checked]);
|
||||
|
||||
|
||||
return (
|
||||
<label htmlFor={forId} className={`${isChecked ? "checked" : ""} select-none relative lg:cursor-pointer ${labelClass}`}>
|
||||
<input checked={isChecked} type="checkbox" autoComplete="off" id={forId} name={name} value={value} className={`absolute w-0 h-0 opacity-0 lg:cursor-pointer [&~span.checkmark]:checked:after:block`} onChange={toggleCheckbox}></input>
|
||||
<label htmlFor={forId} className={`${checked ? "checked" : ""} select-none relative lg:cursor-pointer ${labelClass}`}>
|
||||
<input checked={checked} type="checkbox" autoComplete="off" id={forId} name={name} value={value} className={`absolute w-0 h-0 opacity-0 lg:cursor-pointer [&~span.checkmark]:checked:after:block`} onChange={toggleCheckbox}></input>
|
||||
<span className={`checkmark absolute top-1/2 right-0 -translate-y-1/2 after:content-[""] after:absolute after:hidden after:top-1/2 after:left-1/2 after:-translate-x-1/2 after:-translate-y-1/2 after:rotate-45 ${inputClass}`}></span>
|
||||
<span className={`${titleClass}`}>{title}</span>
|
||||
</label>
|
||||
|
||||
@ -118,11 +118,6 @@ export const billboardLayoutDataQuery = (billboardId ) => `
|
||||
english_name
|
||||
types
|
||||
}
|
||||
ownership {
|
||||
directus_users_id {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
billboard_ratings_aggregated(filter: { status: { _neq: "archived" }, billboard_id: { id: { _eq: "${billboardId}" } } }) {
|
||||
avg {
|
||||
|
||||
22
src/common/services/directus/endpoint-finder.ts
Normal file
22
src/common/services/directus/endpoint-finder.ts
Normal file
@ -0,0 +1,22 @@
|
||||
export const getDirectusEndPoint = ({ collection, type, id }: { collection: string, type: "create" | "edit" | "delete", id?: string }) => {
|
||||
let endPointAddress = "";
|
||||
switch (collection) {
|
||||
case "directus_files":
|
||||
if (type === "create") endPointAddress = `/files`;
|
||||
if (type === "edit") endPointAddress = `/files/${id}`;
|
||||
if (type === "delete") endPointAddress = `/files/${id}`;
|
||||
break;
|
||||
case "directus_users":
|
||||
if (type === "create") endPointAddress = `/users`;
|
||||
if (type === "edit") endPointAddress = `/users/${id}`;
|
||||
if (type === "delete") endPointAddress = `/users/${id}`;
|
||||
break;
|
||||
|
||||
default:
|
||||
if (type === "create") endPointAddress = `/items/${collection}`;
|
||||
if (type === "edit") endPointAddress = `/items/${collection}/${id}`;
|
||||
if (type === "delete") endPointAddress = `/items/${collection}/${id}`;
|
||||
break;
|
||||
}
|
||||
return endPointAddress;
|
||||
}
|
||||
11
src/common/services/directus/general.ts
Normal file
11
src/common/services/directus/general.ts
Normal file
@ -0,0 +1,11 @@
|
||||
export const getCurrencyId = ({ abbr }: { abbr: string }) => {
|
||||
let currenciesList = [
|
||||
{ id: 1 , abbr: "usd" },
|
||||
{ id: 2 , abbr: "eur" },
|
||||
{ id: 3 , abbr: "cad" },
|
||||
{ id: 4 , abbr: "irr" },
|
||||
{ id: 5 , abbr: "jpy" },
|
||||
{ id: 6 , abbr: "tm" },
|
||||
];
|
||||
return currenciesList.find(x => x.abbr === abbr)?.id ?? currenciesList[0].id;
|
||||
}
|
||||
@ -116,7 +116,7 @@ export const websiteName = { fa: "فلایرلند", en: "Flierland" };
|
||||
// }
|
||||
|
||||
// Admin access token
|
||||
export const adminAccessToken = process.env.NODE_ENV === 'production' ? process.env.NEXT_PUBLIC_ADMIN_LIVE_STATIC_TOKEN : process.env.NEXT_PUBLIC_LOCAL_ADMIN_STATIC_TOKEN;
|
||||
export const adminAccessToken = process.env.NODE_ENV === 'production' ? process.env.ADMIN_LIVE_STATIC_TOKEN : process.env.LOCAL_ADMIN_STATIC_TOKEN;
|
||||
|
||||
// Strips html tags from text
|
||||
export const stripHtml = (text) => {
|
||||
@ -691,7 +691,6 @@ export const fileSubmitter = async (body, cb) => {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
// authorization: `Bearer ${token}`,
|
||||
},
|
||||
body: body,
|
||||
credentials: "include", // This ensures cookies are included with the request
|
||||
|
||||
@ -10,29 +10,31 @@ export const getStripeSecretKey = () => {
|
||||
const secretKey = process.env.NODE_ENV === 'production' ? (process.env.STRIPE_SECRET_KEY as any) : (process.env.STRIPE_TEST_SECRET_KEY as any);
|
||||
return secretKey;
|
||||
}
|
||||
// get stripe webhook secret key based on environment
|
||||
export const getWebhookSecretKey = () => {
|
||||
const secretKey = process.env.NODE_ENV === 'production' ? (process.env.STRIPE_WEBHOOK_SECRET as any) : (process.env.STRIPE_TEST_WEBHOOK_SECRET as any);
|
||||
return secretKey;
|
||||
}
|
||||
|
||||
// get stripe wallet top-up product id
|
||||
export const getStripeWalletProductId = (amount: number) => {
|
||||
const priceIds = [
|
||||
{ amount: 10, price_id: "price_1P0jWMF1S9Vi166enI8ALC13", test_price_id: "price_1P1PXhF1S9Vi166ebIz6k0pm" },
|
||||
{ amount: 20, price_id: "price_1P0jWiF1S9Vi166e9nK2P7Tu", test_price_id: "price_1P1PXhF1S9Vi166ebIz6k0pm" },
|
||||
{ amount: 50, price_id: "price_1P0jXAF1S9Vi166e3XtjMW6R", test_price_id: "price_1P1PXhF1S9Vi166ebIz6k0pm" },
|
||||
{ amount: 100, price_id: "price_1P0jXPF1S9Vi166e6ddQilu8", test_price_id: "price_1P1PXhF1S9Vi166ebIz6k0pm" },
|
||||
{ amount: 200, price_id: "price_1P0jXeF1S9Vi166eZNORDZfC", test_price_id: "price_1P1PXhF1S9Vi166ebIz6k0pm" }
|
||||
{ amount: 10, price_id: "price_1P0jWMF1S9Vi166enI8ALC13", test_price_id: "price_1S4HxWFaTaYTZ9ZYw9DKWW80" },
|
||||
{ amount: 20, price_id: "price_1P0jWiF1S9Vi166e9nK2P7Tu", test_price_id: "price_1S4HxWFaTaYTZ9ZYw9DKWW80" },
|
||||
{ amount: 50, price_id: "price_1P0jXAF1S9Vi166e3XtjMW6R", test_price_id: "price_1S4HxWFaTaYTZ9ZYw9DKWW80" },
|
||||
{ amount: 100, price_id: "price_1P0jXPF1S9Vi166e6ddQilu8", test_price_id: "price_1S4HxWFaTaYTZ9ZYw9DKWW80" },
|
||||
{ amount: 200, price_id: "price_1P0jXeF1S9Vi166eZNORDZfC", test_price_id: "price_1S4HxWFaTaYTZ9ZYw9DKWW80" }
|
||||
]
|
||||
const productId = priceIds.filter(x => x.amount === amount)[0][process.env.NODE_ENV === 'production' ? "price_id" : "test_price_id"];
|
||||
return productId;
|
||||
}
|
||||
|
||||
// get stripe ads product id
|
||||
export const getStripeAdsProductId = (name: string) => {
|
||||
const priceIds = [
|
||||
{ name: "one-month", cost: 10, price_id: "price_1P4glVF1S9Vi166eMmz9I8zM", test_price_id: "price_1P4gtIF1S9Vi166eT12UZlhd" },
|
||||
{ name: "three-months", cost: 25, price_id: "price_1OwU0IF1S9Vi166e8dn2pEpF", test_price_id: "price_1OwlnxF1S9Vi166eA2ZlEV5W" },
|
||||
{ name: "six-months", cost: 50, price_id: "price_1OwU2oF1S9Vi166edFUgabk2", test_price_id: "price_1OwlnxF1S9Vi166eA2ZlEV5W" },
|
||||
{ name: "twelve-months", cost: 100, price_id: "price_1OwU5MF1S9Vi166e6tKQY2UU", test_price_id: "price_1OwlnxF1S9Vi166eA2ZlEV5W" },
|
||||
{ name: "ad-cost", cost: 20, price_id: "price_1RAmVKF1S9Vi166eaDGdNjJl", test_price_id: "price_1RAtSMFaTaYTZ9ZYp0q5jwQ0" },
|
||||
]
|
||||
const productId = priceIds.filter(x => x.name === name)[0][process.env.NODE_ENV === 'production' ? "price_id" : "test_price_id"];
|
||||
return productId;
|
||||
export const getStripeAdsProductData = () => {
|
||||
const productData = {
|
||||
name: "ad-cost",
|
||||
cost: 20,
|
||||
price_id: process.env.NODE_ENV === 'production' ? "price_1RAmVKF1S9Vi166eaDGdNjJl" : "price_1RAtSMFaTaYTZ9ZYp0q5jwQ0"
|
||||
}
|
||||
return productData;
|
||||
}
|
||||
@ -9,7 +9,7 @@ interface messageQueryProps {
|
||||
}
|
||||
|
||||
// new payment
|
||||
export const newPaymentQuery = (data: PaymentData) => {
|
||||
export const newPaymentQuery = (data: any) => {
|
||||
return `{
|
||||
status: "published",
|
||||
payment_status: "${data.payment_status}",
|
||||
@ -18,7 +18,7 @@ export const newPaymentQuery = (data: PaymentData) => {
|
||||
owner_user_id: "${data.owner_user_id}",
|
||||
order_amount: "${data.order_amount}",
|
||||
payed_amount: "${data.payed_amount}",
|
||||
currency: "${data.currency}",
|
||||
currency: ${data.currency},
|
||||
payment_method: "${data.payment_method}",
|
||||
discount_used: "${data.discount_used}",
|
||||
discount_code: "${hasValue(data.discount_code) ? data.discount_code : null}",
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
import { gql } from "@apollo/client";
|
||||
import { UUID } from "crypto";
|
||||
import { print } from "graphql";
|
||||
import { safeJson } from "lib/general/safe-response-json";
|
||||
import { basePath } from "services/general/general";
|
||||
|
||||
|
||||
@ -853,7 +852,6 @@ export const fetchDataRequest = async ({ apiRoute, data, system = false, variabl
|
||||
const response = await fetch(`${basePath()}/api/${apiRoute}`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Access-Control-Allow-Origin": "*",
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
@ -884,7 +882,6 @@ export const mutationItemsRequest = async ({ apiRoute, mutation, system = false,
|
||||
const response = await fetch(`${basePath()}/api/${apiRoute}`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Access-Control-Allow-Origin": "*",
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
@ -913,70 +910,89 @@ export const mutationItemsRequest = async ({ apiRoute, mutation, system = false,
|
||||
|
||||
// ---------- Create/update/delete handlers ---------- //
|
||||
// ---- create items ---- //
|
||||
export const restBulkCreateRequest = async ({ collection, items, isAdmin }: { collection: string; items: Record<string, any>[]; isAdmin?: boolean; }) => {
|
||||
try {
|
||||
const response = await fetch(`${basePath()}/api/directus/bulk-create`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ collection, items, isAdmin }),
|
||||
});
|
||||
export const restBulkCreateRequest = async ({ collection, items, systemSecret }: { collection: string; items: Record<string, any>[]; systemSecret?: string; }) => {
|
||||
const isSystemCall = !!systemSecret;
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
throw new Error(`Bulk create failed: ${errorText}`);
|
||||
}
|
||||
const url = isSystemCall ? `${basePath()}/api/system/system-bulk-create` : `${basePath()}/api/directus/bulk-create`;
|
||||
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error('REST bulk create error:', error);
|
||||
throw error;
|
||||
const headers: Record<string, string> = {
|
||||
"Content-Type": "application/json",
|
||||
};
|
||||
|
||||
// Only attach x-system-secret for system calls
|
||||
if (isSystemCall) {
|
||||
headers["x-system-secret"] = systemSecret!;
|
||||
}
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: "POST",
|
||||
headers,
|
||||
body: JSON.stringify({ collection, items }),
|
||||
});
|
||||
|
||||
const { data, error } = await safeJson(response);
|
||||
|
||||
if (!response.ok) {
|
||||
return { data: undefined, error: error || response.statusText };
|
||||
}
|
||||
return { data, error: undefined }
|
||||
};
|
||||
|
||||
// ---- update items ---- //
|
||||
export const restBulkUpdateRequest = async ({ collection, updates, isAdmin }: { collection: string; updates: { id: string; data: Record<string, any> }[]; isAdmin?: boolean; }) => {
|
||||
try {
|
||||
const response = await fetch(`${basePath()}/api/directus/bulk-update`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ collection, updates, isAdmin }),
|
||||
});
|
||||
export const restBulkUpdateRequest = async ({ collection, updates, systemSecret }: { collection: string; updates: { id: string; data: Record<string, any> }[]; systemSecret?: string; }) => {
|
||||
const isSystemCall = !!systemSecret;
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Bulk update failed: ${response.statusText}`);
|
||||
}
|
||||
const url = isSystemCall ? `${basePath()}/api/system/system-bulk-update` : `${basePath()}/api/directus/bulk-update`;
|
||||
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error('REST bulk update error:', error);
|
||||
throw error;
|
||||
const headers: Record<string, string> = {
|
||||
"Content-Type": "application/json",
|
||||
};
|
||||
|
||||
// Only attach x-system-secret for system calls
|
||||
if (isSystemCall) {
|
||||
headers["x-system-secret"] = systemSecret!;
|
||||
}
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: "POST",
|
||||
headers,
|
||||
body: JSON.stringify({ collection, updates }),
|
||||
});
|
||||
|
||||
const { data, error } = await safeJson(response);
|
||||
|
||||
if (!response.ok) {
|
||||
return { data: undefined, error: error || response.statusText };
|
||||
}
|
||||
return { data, error: undefined }
|
||||
};
|
||||
|
||||
// ---- delete items ---- //
|
||||
export const restBulkDeleteRequest = async ({ collection, ids, isAdmin }: { collection: string; ids: string[]; isAdmin?: boolean; }) => {
|
||||
try {
|
||||
const response = await fetch(`${basePath()}/api/directus/bulk-delete`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ collection, ids, isAdmin }),
|
||||
});
|
||||
export const restBulkDeleteRequest = async ({ collection, ids, systemSecret }: { collection: string; ids: any[]; systemSecret?: string; }) => {
|
||||
const isSystemCall = !!systemSecret;
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json();
|
||||
throw new Error(`Bulk delete failed: ${error.detail || error.error}`);
|
||||
}
|
||||
const url = isSystemCall ? `${basePath()}/api/system/system-bulk-delete` : `${basePath()}/api/directus/bulk-delete`;
|
||||
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error('REST bulk delete error:', error);
|
||||
throw error;
|
||||
const headers: Record<string, string> = {
|
||||
"Content-Type": "application/json",
|
||||
};
|
||||
|
||||
// Only attach x-system-secret for system calls
|
||||
if (isSystemCall) {
|
||||
headers["x-system-secret"] = systemSecret!;
|
||||
}
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: "POST",
|
||||
headers,
|
||||
body: JSON.stringify({ collection, ids }),
|
||||
});
|
||||
|
||||
const { data, error } = await safeJson(response);
|
||||
|
||||
if (!response.ok) {
|
||||
return { data: undefined, error: error || response.statusText };
|
||||
}
|
||||
return { data, error: undefined }
|
||||
};
|
||||
|
||||
|
||||
@ -39,19 +39,6 @@ export const updateBillboardViews = (visits: string[], id: string | string[] | u
|
||||
}
|
||||
`
|
||||
|
||||
// update billboard news seen_by
|
||||
export const updateBillboardNewsViews = (views_count: number, id: string | string[] | undefined) => gql`
|
||||
mutation {
|
||||
update_billboard_news_item (
|
||||
id: ${id},
|
||||
data: { seen_by: ${views_count + 1} }
|
||||
)
|
||||
{
|
||||
id
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
// update wiki views
|
||||
export const updateWikiViews = (views_count: number, id: string | string[] | undefined) => gql`
|
||||
mutation {
|
||||
|
||||
46
src/common/services/user/get-user-from-request.ts
Normal file
46
src/common/services/user/get-user-from-request.ts
Normal file
@ -0,0 +1,46 @@
|
||||
import { UserProps } from "common/types/user";
|
||||
import { NextApiRequest } from "next";
|
||||
import { cmsAddress } from "services/general/general";
|
||||
|
||||
interface UserData {
|
||||
id: string;
|
||||
role: string;
|
||||
email: string;
|
||||
wallet_balance: string;
|
||||
raw: UserProps;
|
||||
token: string;
|
||||
}
|
||||
|
||||
export async function getUserFromRequest(req: NextApiRequest) {
|
||||
try {
|
||||
// 1. Extract the session token from cookies
|
||||
const token = req.cookies["directus_session_token"];
|
||||
if (!token) return null;
|
||||
|
||||
// 2. Ask Directus who this token belongs to
|
||||
const response = await fetch(`${cmsAddress()}/users/me`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
return null; // token invalid or expired
|
||||
}
|
||||
|
||||
const user = await response.json();
|
||||
|
||||
// 3. Return useful info
|
||||
return {
|
||||
id: user.data.id,
|
||||
role: user.data.role,
|
||||
email: user.data.email,
|
||||
wallet_balance: user.data.wallet_balance,
|
||||
raw: user.data, // optional: full directus user object
|
||||
token, // optional: so you can reuse for direct calls
|
||||
} as UserData;
|
||||
} catch (err) {
|
||||
console.error("❌ Failed to get user from Directus:", err);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@ -4,8 +4,8 @@ import { useRouter } from "next/dist/client/router";
|
||||
import Input from "components/input/text";
|
||||
import Label from "components/label/label";
|
||||
import Button from "components/button/button";
|
||||
import { deepClone, emailValidationRegex, hasValue } from "services/general/general";
|
||||
import { ArrowTurnDownLeftSolid, KeySolid, ShieldKeyholeSolid } from "components/icons";
|
||||
import { deepClone, hasValue } from "services/general/general";
|
||||
import { ArrowTurnDownLeftSolid, ShieldKeyholeSolid } from "components/icons";
|
||||
import { gql, useMutation } from "@apollo/client";
|
||||
import { publicDataFetch } from "common/data/apollo-client";
|
||||
|
||||
|
||||
@ -4,11 +4,10 @@ import { useRouter } from "next/dist/client/router";
|
||||
import Input from "components/input/text";
|
||||
import Label from "components/label/label";
|
||||
import Button from "components/button/button";
|
||||
import { adminAccessToken, emailValidationRegex, hasValue } from "services/general/general";
|
||||
import { gql, useMutation } from "@apollo/client";
|
||||
import { adminDataFetch } from "common/data/apollo-client";
|
||||
import { basePath, emailValidationRegex, hasValue } from "services/general/general";
|
||||
import DataList from "components/select/data-list";
|
||||
import { XmarkSolid } from "components/icons";
|
||||
import { safeJson } from "lib/general/safe-response-json";
|
||||
|
||||
interface SignUp {
|
||||
cities: {
|
||||
@ -53,8 +52,10 @@ const SignUp: React.FunctionComponent<SignUp> = ({ cities }) => {
|
||||
const [cityError, setCityError] = useState(false);
|
||||
const [city, setCity] = useState<string[]>([]);
|
||||
const [cityOpen, setCityOpen] = useState(false);
|
||||
const [signingUp, setSigningUp] = useState(false);
|
||||
const [emailError, setEmailError] = useState(false);
|
||||
const [passwordError, setPasswordError] = useState(false);
|
||||
const [existingEmail, setExistingEmail] = useState(false);
|
||||
const [signUpSuccess, setSignUpSuccess] = useState(false);
|
||||
const [showSuccessMessage, setShowSuccessMessage] = useState(false);
|
||||
|
||||
@ -90,35 +91,35 @@ const SignUp: React.FunctionComponent<SignUp> = ({ cities }) => {
|
||||
return 0
|
||||
});
|
||||
|
||||
const AddNewUser = () => gql`
|
||||
mutation CreateNewUser {
|
||||
create_users_item (
|
||||
data: {
|
||||
first_name: ${JSON.stringify(userInfo.first_name)},
|
||||
last_name: ${JSON.stringify(userInfo.last_name)},
|
||||
city: {
|
||||
id: ${userInfo.city.ids[0]}
|
||||
},
|
||||
communication_language: {
|
||||
code: ${JSON.stringify(router.locale === "fa" ? "fa-IR" : "en-US")}
|
||||
},
|
||||
email: ${JSON.stringify(userInfo.email)},
|
||||
password: ${JSON.stringify(userInfo.password)},
|
||||
role: "5892560c-92ad-4d2b-9446-b5bdf0399e99"
|
||||
}
|
||||
)
|
||||
{
|
||||
email
|
||||
id
|
||||
const submitForm = async () => {
|
||||
try {
|
||||
const response = await fetch(`${basePath()}/api/auth/create-user`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
first_name: userInfo.first_name,
|
||||
last_name: userInfo.last_name,
|
||||
city_id: userInfo.city.ids[0],
|
||||
communication_language: router.locale === "fa" ? "fa-IR" : "en-US",
|
||||
email: userInfo.email,
|
||||
password: userInfo.password,
|
||||
}),
|
||||
})
|
||||
const { data, error } = await safeJson(response);
|
||||
if (error === `Value for field "email" in collection "directus_users" has to be unique.`) {
|
||||
setExistingEmail(true);
|
||||
setShowSuccessMessage(false);
|
||||
}
|
||||
if (data) {
|
||||
setSignUpSuccess(true);
|
||||
setShowSuccessMessage(true);
|
||||
}
|
||||
} catch (err) {
|
||||
console.log(`There was something wrong with your registration: ${err}`);
|
||||
}
|
||||
`
|
||||
const [submitForm, { data: mutationData, error: mutationErrors, loading: signingUp }] = useMutation(AddNewUser(), {
|
||||
client: adminDataFetch("system", adminAccessToken),
|
||||
onError: (err) => {
|
||||
console.log("There was a problem with your request, please try again.");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// methods
|
||||
const Terms = () => {
|
||||
@ -126,11 +127,14 @@ const SignUp: React.FunctionComponent<SignUp> = ({ cities }) => {
|
||||
}
|
||||
|
||||
const handleInputUpdate = (data: string, type: string) => {
|
||||
type === "first_name" && setUserInfo({...userInfo, first_name: data});
|
||||
type === "last_name" && setUserInfo({...userInfo, last_name: data});
|
||||
type === "city" && setUserInfo({...userInfo, city: data});
|
||||
type === "email" && setUserInfo({...userInfo, email: data});
|
||||
type === "password" && setUserInfo({...userInfo, password: data});
|
||||
type === "first_name" && setUserInfo({ ...userInfo, first_name: data });
|
||||
type === "last_name" && setUserInfo({ ...userInfo, last_name: data });
|
||||
type === "city" && setUserInfo({ ...userInfo, city: data });
|
||||
if (type === "email") {
|
||||
setUserInfo({ ...userInfo, email: data });
|
||||
setExistingEmail(false);
|
||||
}
|
||||
type === "password" && setUserInfo({ ...userInfo, password: data });
|
||||
}
|
||||
|
||||
const validate = () => {
|
||||
@ -146,8 +150,7 @@ const SignUp: React.FunctionComponent<SignUp> = ({ cities }) => {
|
||||
}
|
||||
|
||||
const handleSignUp = async () => {
|
||||
!mutationData && submitForm();
|
||||
!mutationErrors && setShowSuccessMessage(true);
|
||||
submitForm();
|
||||
}
|
||||
const resetSignUp = async () => {
|
||||
setUserInfo({
|
||||
@ -185,14 +188,6 @@ const SignUp: React.FunctionComponent<SignUp> = ({ cities }) => {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (mutationErrors) {
|
||||
// console.log(mutationErrors.message);
|
||||
}
|
||||
if (mutationData && !signUpSuccess) {
|
||||
resetSignUp();
|
||||
setSignUpSuccess(true);
|
||||
}
|
||||
|
||||
// useEffects
|
||||
useEffect(() => {
|
||||
@ -205,15 +200,14 @@ const SignUp: React.FunctionComponent<SignUp> = ({ cities }) => {
|
||||
// return
|
||||
return (
|
||||
<form className="block pt-1">
|
||||
{!mutationErrors && showSuccessMessage &&
|
||||
{showSuccessMessage &&
|
||||
<p className="flex items-center space-x-3 rtl:space-x-reverse w-full bg-success-bg text-success-text text-base/7 px-gi py-2 rounded-md mt-4" >
|
||||
<XmarkSolid className="size-5 fill-current ml-4 lg:cursor-pointer shrink-0" onClick={() => setShowSuccessMessage(false)} />
|
||||
{translate("signup-success-message")}
|
||||
</p>
|
||||
}
|
||||
{mutationErrors && mutationErrors.message === `Value for field "email" in collection "directus_users" has to be unique.` &&
|
||||
{existingEmail &&
|
||||
<p className="flex items-center space-x-3 rtl:space-x-reverse w-full bg-error-bg text-error-text text-base/7 px-gi py-2 rounded-md mt-4" >
|
||||
{/* <XmarkSolid className="size-5 fill-current ml-4 lg:cursor-pointer shrink-0" onClick={() => setShowSuccessMessage(false)} /> */}
|
||||
{translate("signup-email-exist-message")}
|
||||
</p>
|
||||
}
|
||||
|
||||
@ -1,8 +1,6 @@
|
||||
import React, { useEffect, useState } from "react"
|
||||
import useTranslate from "services/translation/translation"
|
||||
import DataList from "components/select/data-list";
|
||||
import { getLocaleTr, useGetRouter } from "services/general/general";
|
||||
import BillboardMultiFilter from "./billboard-multi-filter";
|
||||
import Modal from "components/modal/modal";
|
||||
import CatSelector from "./cat-selector";
|
||||
import { BillboardCategory } from "common/types/billboard";
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
import React, { useEffect, useState } from "react"
|
||||
import useTranslate from "services/translation/translation"
|
||||
import { useRouter } from 'next/router'
|
||||
import Link from "components/link/link"
|
||||
import { getLocaleTr, url, useGetRouter } from "services/general/general";
|
||||
import { ChevronDownSolid } from "components/icons";
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import React, { useEffect, useState } from "react"
|
||||
import Link from "components/link/link"
|
||||
import useTranslate from "services/translation/translation"
|
||||
import { addVisitedPage, adminAccessToken, adminFetchPrivateData, basePath, getLocaleTr, getSmartTime, getVisitedPages, hasValue, oneDay, useGetRouter } from "services/general/general";
|
||||
import { addVisitedPage, basePath, fetchPrivateData, getLocaleTr, getSmartTime, getVisitedPages, hasValue, oneDay, safeClone, useGetRouter } from "services/general/general";
|
||||
import { CalendarDaysSolid, CopySolid, EllipsisSolid, EmptyHeart, EyeSolid, FilledHeart, ImageSharpSolid, ReplySolid, ShareNodesSolid, SplitSolid, Telegram, Whatsapp, XMark, XTwitter } from "components/icons";
|
||||
import Gallery from "components/gallery/gallery";
|
||||
import Image from "components/image/image";
|
||||
@ -13,12 +13,9 @@ import ClickOutside from "components/click-outside/click-outside";
|
||||
import Popover from "components/popover/popover";
|
||||
import { copyPageUrl, shareMore } from "services/social/social";
|
||||
import ReactPlayer from "react-player";
|
||||
import { useMutation } from "@apollo/client";
|
||||
import { adminDataFetch } from "common/data/apollo-client";
|
||||
import { updateBillboardNewsViews } from "services/queries/directus/item-viewed";
|
||||
import { useAuth } from "services/user/AuthContext";
|
||||
import { getUserFullDataQuery, likedBillboardNews } from "services/queries/directus/user";
|
||||
import LoginNeeded from "components/general/login-needed";
|
||||
import { restBulkUpdateRequest } from "services/queries/directus/billboard";
|
||||
|
||||
interface NewsDetailsProps {
|
||||
billboardId: string;
|
||||
@ -147,24 +144,32 @@ const NewsDetails: React.FunctionComponent<NewsDetailsProps> = ({ billboardId, b
|
||||
const { locale, query, asPath, router } = useGetRouter();
|
||||
const { isAuthenticated, userData: uData } = useAuth();
|
||||
|
||||
const { data: userLikes, error: userLikesError } = useSWR(isAuthenticated ? ['system', adminAccessToken, getUserFullDataQuery(uData?.data.id)] : null, ([route, token, query]) => adminFetchPrivateData(route, token, query));
|
||||
const getUserLikedNewsQuery = () => `
|
||||
users_me {
|
||||
id
|
||||
liked_news
|
||||
}
|
||||
`
|
||||
const { data: userLikes, error: userLikesError } = useSWR(isAuthenticated ? ['system', getUserLikedNewsQuery()] : null, ([route, query]) => fetchPrivateData(route, query));
|
||||
|
||||
userLikesError && console.log(userLikesError);
|
||||
userLikesError && console.log(userLikesError.message);
|
||||
|
||||
const newsData: BillboardNewsItem = news;
|
||||
const userLikesList: string[] = userLikes?.data.users_by_id.liked_news;
|
||||
const userLikesList: string[] = userLikes?.data.users_me.liked_news;
|
||||
console.log(userLikesList);
|
||||
|
||||
const pageUrl = encodeURI(basePath() + (locale === "en" ? "/en" : "") + asPath);
|
||||
const shareData = {
|
||||
title: newsData ? getLocaleTr(newsData, locale).title : billboardTitle,
|
||||
url: pageUrl
|
||||
}
|
||||
const picWidth = (x:any) => x.gallery[0]?.directus_files_id.width;
|
||||
const picHeight = (x:any) => x.gallery[0]?.directus_files_id.height;
|
||||
const picWidth = (x: any) => x.gallery[0]?.directus_files_id.width;
|
||||
const picHeight = (x: any) => x.gallery[0]?.directus_files_id.height;
|
||||
|
||||
const userLikesArrayLocal = () => {
|
||||
let userLikesLocal = hasValue(userLikesList) ? userLikesList.map(x => x) : [];
|
||||
let userLikesLocal = hasValue(userLikesList) ? safeClone(userLikesList) : [];
|
||||
|
||||
if (userLikesLocal !== undefined) {
|
||||
if (userLikesLocal) {
|
||||
if (userLikesLocal === null || userLikesLocal.length === 0) {
|
||||
userLikesLocal = [newsData?.id];
|
||||
} else {
|
||||
@ -179,22 +184,53 @@ const NewsDetails: React.FunctionComponent<NewsDetailsProps> = ({ billboardId, b
|
||||
}
|
||||
return userLikesLocal;
|
||||
};
|
||||
|
||||
// update news seen_by
|
||||
const [updateViews, { data: BillboardNewsViewsMutationData, error: BillboardNewsViewsMutationErrors, loading: mutating }] = useMutation(updateBillboardNewsViews(newsData?.seen_by, newsData?.id), {
|
||||
client: adminDataFetch("", adminAccessToken),
|
||||
onError: (err) => {
|
||||
console.log(err.message);
|
||||
}
|
||||
});
|
||||
// update user liked news
|
||||
const [updateLikedNews, { data: BillboardLikedNewsData, error: BillboardLikedNewsDataErrors, loading: billboardNewsMutating }] = useMutation(likedBillboardNews(uData?.data.id, userLikesArrayLocal()), {
|
||||
client: adminDataFetch("system", adminAccessToken),
|
||||
onError: (err) => {
|
||||
console.log(err.message);
|
||||
}
|
||||
});
|
||||
|
||||
// update news seen_by
|
||||
const updateViews = async () => {
|
||||
try {
|
||||
const { data, error } = await restBulkUpdateRequest({
|
||||
collection: 'billboard_news',
|
||||
updates: [
|
||||
{
|
||||
id: newsData?.id,
|
||||
data: {
|
||||
seen_by: newsData?.seen_by + 1
|
||||
},
|
||||
},
|
||||
]
|
||||
});
|
||||
|
||||
if (error) {
|
||||
console.error("News view update failed:", error);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("News view update request failed:", err);
|
||||
}
|
||||
}
|
||||
|
||||
// update user liked news
|
||||
const updateLikedNews = async () => {
|
||||
try {
|
||||
const { data, error } = await restBulkUpdateRequest({
|
||||
collection: 'directus_users',
|
||||
updates: [
|
||||
{
|
||||
id: uData?.data.id,
|
||||
data: {
|
||||
liked_news: userLikesArrayLocal()
|
||||
},
|
||||
},
|
||||
]
|
||||
});
|
||||
|
||||
if (error) {
|
||||
console.error("News like failed:", error);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("News like request failed:", err);
|
||||
}
|
||||
}
|
||||
|
||||
// methods
|
||||
const onBack = () => {
|
||||
router.back();
|
||||
@ -257,12 +293,12 @@ const NewsDetails: React.FunctionComponent<NewsDetailsProps> = ({ billboardId, b
|
||||
<ReplySolid className="reactive-button inline-block size-9 lg:size-11 ltr:rotate-180 fill-current p-2 lg:p-3 rounded-full bg-[var(--light-brand-color)] text-[var(--brand-color)]" onClick={onBack} />
|
||||
</div>
|
||||
|
||||
{newsData.video_link ?
|
||||
{newsData.video_link ?
|
||||
<div
|
||||
className="flex items-center justify-center w-full mx-auto bg-white border-b border-gray-200/75 mb-1 max-lg:px-4 py-4 mt-1 sm:py-6 rounded-lg">
|
||||
<ReactPlayer controls url={newsData.video_link} />
|
||||
</div>
|
||||
:
|
||||
:
|
||||
<>
|
||||
{/* gallery */}
|
||||
<Gallery
|
||||
@ -330,7 +366,7 @@ const NewsDetails: React.FunctionComponent<NewsDetailsProps> = ({ billboardId, b
|
||||
{newsData.features?.includes("celebrate") && <Label value={translate("billboard-news-label-celebrate")} className="bg-[var(--brand-color)]" />}
|
||||
{newsData.features?.includes("hot") && <Label value={translate("billboard-news-label-hot")} className="bg-primary" />}
|
||||
{newsData.features?.includes("limited") && <Label value={translate("billboard-news-label-limited")} className="bg-violet-600" />}
|
||||
|
||||
|
||||
{/* header */}
|
||||
<header className="block w-full pb-3 lg:py-4 lg:mb-2">
|
||||
<h2 className="block text-xl/9 lg:text-2xl text-center text-secondary-light pt-2">{getLocaleTr(newsData, locale).title}</h2>
|
||||
@ -356,8 +392,8 @@ const NewsDetails: React.FunctionComponent<NewsDetailsProps> = ({ billboardId, b
|
||||
|
||||
{/* content */}
|
||||
<Parser limit limitLength={800} text={getLocaleTr(newsData, locale).content} className={`[&_*]:!text-sm/7 [&_*]:lg:!text-sm/7 !min-h-0 [&_*]:!text-start`} />
|
||||
{getLocaleTr(newsData, locale).cta_link &&
|
||||
<Link
|
||||
{getLocaleTr(newsData, locale).cta_link &&
|
||||
<Link
|
||||
href={`${getLocaleTr(newsData, locale).cta_link}`}
|
||||
className="reactive-button block mx-auto w-max py-3 px-6 rounded-xl bg-[var(--brand-color)] text-white text-sm lg:text-base font-semibold mt-6 mb-4 select-none uppercase"
|
||||
>
|
||||
@ -396,13 +432,13 @@ const NewsDetails: React.FunctionComponent<NewsDetailsProps> = ({ billboardId, b
|
||||
</ClickOutside>
|
||||
<div className="flex items-center relative space-x-2 rtl:space-x-reverse mx-auto">
|
||||
<LoginNeeded wrapperClass="liked-news" handleClass="liked-news" isVisible={showLoginNeeded} onClose={() => setShowLoginNeeded(false)} />
|
||||
{isAuthenticated ?
|
||||
{isAuthenticated ?
|
||||
(userLikesList && newsData) &&
|
||||
isLiked ?
|
||||
<FilledHeart className="login-needed-handle-liked-news inline-block p-2 size-9 sm:size-9 bg-market-input fill-primary rounded-lg md:rounded-lg lg:cursor-pointer hover:fill-[var(--brand-color)]" onClick={handleNewsLike} />
|
||||
:
|
||||
<EmptyHeart className="login-needed-handle-liked-news inline-block p-2 size-9 sm:size-9 bg-market-input fill-secondary-light rounded-lg md:rounded-lg lg:cursor-pointer hover:fill-[var(--brand-color)]" onClick={handleNewsLike} />
|
||||
:
|
||||
:
|
||||
<EmptyHeart className="login-needed-handle-liked-news inline-block p-2 size-9 sm:size-9 bg-market-input fill-secondary-light rounded-lg md:rounded-lg lg:cursor-pointer hover:fill-[var(--brand-color)]" onClick={handleNewsLike} />
|
||||
}
|
||||
<ShareNodesSolid className="news-sharing-handle inline-block rounded-lg md:rounded-lg bg-market-input hover:bg-[var(--light-brand-color)] p-2 size-9 sm:size-9 fill-current text-secondary-light hover:text-[var(--brand-color)] lg:cursor-pointer" onClick={() => setShareOpen(true)} />
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
import React, { useEffect, useRef, useState } from "react"
|
||||
import useTranslate from "services/translation/translation"
|
||||
import { adminAccessToken, adminFetchPrivateData, fetchPrivateData, fetchPublicData, getLocaleTr, useGetRouter } from 'services/general/general'
|
||||
import { getLocaleTr, useGetRouter } from 'services/general/general'
|
||||
import { CaretLeftSolid, CircleExclamationSolid, PhoneSolid } from "components/icons"
|
||||
import { Billboard, BillboardProduct, BillboardProductsCategory, BillboardServiceTypes, ProductRatings, ProductVariantGalleries, ProductVariations } from "common/types/billboard"
|
||||
import { Billboard, BillboardProduct, BillboardProductsCategory, BillboardServiceTypes, ProductVariantGalleries, ProductVariations } from "common/types/billboard"
|
||||
import Parser from "components/parser/parser"
|
||||
import useSWR from "swr"
|
||||
import RelatedProducts from "./related"
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
import React, { useEffect, useState } from "react"
|
||||
import Link from "components/link/link"
|
||||
import useTranslate from "services/translation/translation"
|
||||
import { useGetRouter } from "services/general/general";
|
||||
import { ImageSharpSolid } from "components/icons";
|
||||
|
||||
@ -1,10 +1,7 @@
|
||||
import React, { useEffect, useState } from "react"
|
||||
import Link from "components/link/link"
|
||||
import React from "react"
|
||||
import useTranslate from "services/translation/translation"
|
||||
import { hasValue, useGetRouter } from "services/general/general";
|
||||
import { ImageSharpSolid, PercentageSolid } from "components/icons";
|
||||
import Gallery from "components/gallery/gallery";
|
||||
import Image from "components/image/image";
|
||||
import { PercentageSolid } from "components/icons";
|
||||
import StarRating from "components/rating/star-rating";
|
||||
import InnerLoading from "components/loading/inner-loading";
|
||||
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import React, { useState } from "react"
|
||||
import useTranslate from "services/translation/translation"
|
||||
import { getEngTr, getLocaleTr, getPerTr, hasValue, useGetRouter } from "services/general/general";
|
||||
import { ArrowLeft, ArrowLeftRegular, ArrowLeftSolid, ArrowRotateLeftLight, ArrowsRotateSolid, ArrowTurnDownLeftSolid, BasketShoppingSolid, BoxTapedSolid, CartXmarkSolid, CircleExclamationSolid, CreditCardSolid, ImageSharpSolid, MinusSolid, PlugCircleXmarkSolid, PlusSolid, XmarkSolid } from "components/icons";
|
||||
import { BasketShoppingSolid, BoxTapedSolid, CartXmarkSolid, CircleExclamationSolid, CreditCardSolid, ImageSharpSolid, MinusSolid, PlusSolid } from "components/icons";
|
||||
import Image from "components/image/image";
|
||||
import Button from "components/button/button";
|
||||
import Select from "components/select/select";
|
||||
|
||||
@ -1,14 +1,12 @@
|
||||
import React, { useEffect, useMemo, useRef, useState } from "react"
|
||||
import Link from "components/link/link"
|
||||
import React, { useEffect, useMemo, useState } from "react"
|
||||
import useTranslate from "services/translation/translation"
|
||||
import { adminAccessToken, adminFetchPrivateData, fetchPublicData, getDatesBetween, getLocaleTr, getMonth, getPriceUnit, hasValue, mtd, stripHtml, useGetRouter } from "services/general/general";
|
||||
import { Calendar, CalendarCheckSolid, CalendarDaysSolid, CalendarSolid, CalendarStarLight, CalendarStarSolid, CalendarXmarkSolid, CaretLeftSolid, CircleCheckSolid, CircleDollarSolid, CircleDotSolid, CircleExclamationSolid, CircleQuestionLight, CircleQuestionSolid, CircleSolid, ClockSolid, CreditCardSolid, HourGlassRegular, HourGlassSolid, ImageSharpSolid, MinusSolid, PlusSolid, SeatAirlineSolid, TicketSolid, UserVneckHairSolid, XMark, XmarkSolid } from "components/icons";
|
||||
import { adminAccessToken, adminFetchPrivateData, fetchPublicData, getLocaleTr, getMonth, getPriceUnit, hasValue, mtd, stripHtml, useGetRouter } from "services/general/general";
|
||||
import { CalendarCheckSolid, CalendarSolid, CalendarXmarkSolid, CaretLeftSolid, CircleCheckSolid, CircleDollarSolid, CircleDotSolid, CircleExclamationSolid, CircleQuestionSolid, CircleSolid, ClockSolid, CreditCardSolid, HourGlassSolid, ImageSharpSolid, MinusSolid, PlusSolid, SeatAirlineSolid, TicketSolid, UserVneckHairSolid, XMark, XmarkSolid } from "components/icons";
|
||||
import VanillaCalendar from "components/calendars/vanilla-calendar";
|
||||
import Slider from "components/slider/slider";
|
||||
import Input from "components/input/text";
|
||||
import Button from "components/button/button";
|
||||
import Image from "components/image/image";
|
||||
import { getRealTimeReservationData, getReservationData, getServiceProvidersDates, providerSharedData, withProviderDuration, withProviderPrice, withProviderShared, withProviderSharedProps } from "../products/product-services";
|
||||
import { getRealTimeReservationData, getReservationData, getServiceProvidersDates, providerSharedData, withProviderDuration, withProviderPrice } from "../products/product-services";
|
||||
import useSWR from "swr";
|
||||
import InnerLoading from "components/loading/inner-loading";
|
||||
import { BillboardReservation, BillboardServiceTypes } from "common/types/billboard";
|
||||
|
||||
@ -18,13 +18,12 @@ interface ProductItemProps {
|
||||
|
||||
|
||||
const ProductItem: React.FunctionComponent<ProductItemProps> = ({ item, priceData, billboardId, billboardTitle, rating = 0 }) => {
|
||||
|
||||
const translate = useTranslate();
|
||||
const { locale } = useGetRouter();
|
||||
const pic = item.ad_item ? item.ad_item.gallery[0]?.directus_files_id : (!item.variations || !item.variations.some(x => x.pics.length > 0) ? null : item.variations.filter(x => x.main_variant)[0].pics[0].variant_galleries_id.gallery[0]?.directus_files_id);
|
||||
const itemTitle = item.translations.filter((x: any) => x.languages_code.code.slice(0, 2) === locale)[0].title;
|
||||
const price = priceData;
|
||||
|
||||
|
||||
return (
|
||||
<Link
|
||||
href={item.ad_item ? `/market/${item.ad_item.id}/${url(getLocaleTr(item.ad_item, locale).title)}` : `/billboard/${billboardId}/${url(billboardTitle)}/products/${item.product_id}`}
|
||||
|
||||
@ -1,8 +1,6 @@
|
||||
import React, { useEffect, useState } from "react"
|
||||
import Link from "components/link/link"
|
||||
import useTranslate from "services/translation/translation"
|
||||
import React from "react"
|
||||
import { useGetRouter } from "services/general/general";
|
||||
import { ChevronLeftSolid, ChevronRightSolid, StarSolid } from "components/icons";
|
||||
import { StarSolid } from "components/icons";
|
||||
|
||||
interface ProductPlaceHolderProps {
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import React, { useEffect, useState } from "react"
|
||||
import useTranslate from "services/translation/translation"
|
||||
import { adminAccessToken, adminFetchPrivateData, fetchPublicData, getLocaleTr, hasValue, url, useGetRouter } from 'services/general/general'
|
||||
import { basePath, fetchPublicData, getLocaleTr, hasValue, url, useGetRouter } from 'services/general/general'
|
||||
import { Billboard, BillboardProduct, BillboardProductsCategory, BillboardServiceTypes } from "common/types/billboard"
|
||||
import useSWR from "swr"
|
||||
import InnerLoading from "components/loading/inner-loading"
|
||||
@ -8,17 +8,15 @@ import { ArrowLeftSolid, BoxOpenSolid, MagnifyingGlass, XMark } from "components
|
||||
import Pagination from "../../../../components/general/pagination"
|
||||
import Input from "components/input/text"
|
||||
import ProductItem from "./cards/product-item"
|
||||
import { fetchDataRequest } from "services/queries/directus/billboard"
|
||||
import { PriceDataProps } from "pages/api/billboard/product-price-data"
|
||||
import ProductCatsFilter from "./cats-filter"
|
||||
import ProductsPlaceHolder from "./products-placeholder"
|
||||
import { useUpdateQueryParams } from "services/general/update-query"
|
||||
import Button from "components/button/button"
|
||||
import { safeJson } from "lib/general/safe-response-json"
|
||||
|
||||
interface BillboardProductsProps {
|
||||
billboard: Billboard;
|
||||
products: BillboardProduct[];
|
||||
totalProductsCount: number;
|
||||
}
|
||||
interface ProductsContainerProps {
|
||||
items: {
|
||||
@ -54,10 +52,10 @@ const ProductsContainer: React.FunctionComponent<ProductsContainerProps> = ({ to
|
||||
return (
|
||||
<div className="flex flex-col space-y-2">
|
||||
{/* shop products */}
|
||||
{currentProductType === "shop" &&
|
||||
{(currentProductType === "shop") &&
|
||||
<div className={`grid grid-cols-2 md:grid-cols-3 xl:grid-cols-4 gap-4 w-full py-4`}>
|
||||
{items.map(x => (
|
||||
<ProductItem key={x.item.id} item={x.item} priceData={priceData.filter(y => y.id === x.item.id)[0]} billboardId={billboardId} billboardTitle={billboardTitle} rating={x.rating} />
|
||||
<ProductItem key={x.item.id} item={x.item} priceData={priceData.filter(y => String(y.id) === String(x.item.id))[0]} billboardId={billboardId} billboardTitle={billboardTitle} rating={x.rating} />
|
||||
))}
|
||||
</div>
|
||||
}
|
||||
@ -75,13 +73,14 @@ const ProductsContainer: React.FunctionComponent<ProductsContainerProps> = ({ to
|
||||
)
|
||||
}
|
||||
|
||||
const BillboardProducts: React.FunctionComponent<BillboardProductsProps> = ({ billboard, products, totalProductsCount }) => {
|
||||
const BillboardProducts: React.FunctionComponent<BillboardProductsProps> = ({ billboard }) => {
|
||||
|
||||
// states
|
||||
const [activeCats, setActiveCats] = useState<number[]>([0]);
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [searchValue, setSearchValue] = useState("");
|
||||
const [searchVisible, setSearchVisible] = useState(false);
|
||||
const [priceData, setPriceData] = useState<PriceDataProps[]>([]);
|
||||
|
||||
// variables
|
||||
const translate = useTranslate();
|
||||
@ -145,11 +144,11 @@ const BillboardProducts: React.FunctionComponent<BillboardProductsProps> = ({ bi
|
||||
languages_code: { code: { _eq: "${locale === 'fa' ? 'fa-IR' : 'en-US'}" }},
|
||||
${hasValue(searchValue) ? `title: { _icontains: "${searchValue}" }` : ""}
|
||||
},
|
||||
${activeCats[0] !== 0 ?
|
||||
`category: {
|
||||
${activeCats[0] !== 0 ?
|
||||
`category: {
|
||||
local_id: { _in: ${JSON.stringify(activeCats)} }
|
||||
},`
|
||||
: ""},
|
||||
: ""},
|
||||
status: { _eq: "published" }
|
||||
},
|
||||
sort: ["sort", "-date_created"],
|
||||
@ -167,11 +166,11 @@ const BillboardProducts: React.FunctionComponent<BillboardProductsProps> = ({ bi
|
||||
languages_code: { code: { _eq: "${locale === 'fa' ? 'fa-IR' : 'en-US'}" }},
|
||||
${hasValue(searchValue) ? `title: { _icontains: "${searchValue}" }` : ""}
|
||||
},
|
||||
${activeCats[0] !== 0 ?
|
||||
`category: {
|
||||
${activeCats[0] !== 0 ?
|
||||
`category: {
|
||||
local_id: { _in: ${JSON.stringify(activeCats)} }
|
||||
},`
|
||||
: ""},
|
||||
: ""},
|
||||
status: { _eq: "published" }
|
||||
},
|
||||
sort: ["sort", "-date_created"],
|
||||
@ -262,33 +261,52 @@ const BillboardProducts: React.FunctionComponent<BillboardProductsProps> = ({ bi
|
||||
}
|
||||
}
|
||||
`
|
||||
const areProductsClientSide = activeCats[0] === 0 && currentPage === 1 && searchValue === "";
|
||||
|
||||
|
||||
const { data: serviceTypes, error: serviceTypesError } = useSWR(['', serviceTypesQuery], ([path, url]) => fetchPublicData(path, url));
|
||||
const { data: productCats, error: productCatsError } = useSWR(['', categoriesQuery], ([path, url]) => fetchPublicData(path, url));
|
||||
const { data: productRatings, error: productRatingsError } = useSWR(['', ratingsQuery], ([path, url]) => fetchPublicData(path, url));
|
||||
const { data: currentProducts, error: currentProductsError } = useSWR(activeCats[0] !== -1 || currentPage !== 1 || hasValue(searchValue) ? ['', adminAccessToken, itemsQuery] : null, ([path, token, url]) => adminFetchPrivateData(path, token, url));
|
||||
const productsList = areProductsClientSide ? products : currentProducts?.data.billboard_products;
|
||||
const { data: priceData, error: priceDataFetchError } = useSWR(productsList ? [`billboard/product-price-data`, { productIds: productsList.map((x:any) => x.id), billboardId: query.id }] : null, ([apiRoute, data]) => fetchDataRequest({ apiRoute, data }));
|
||||
const { data: currentProducts, error: currentProductsError } = useSWR(activeCats[0] !== -1 || currentPage !== 1 || hasValue(searchValue) ? ['', itemsQuery] : null, ([path, url]) => fetchPublicData(path, url));
|
||||
const productsList: BillboardProduct[] = currentProducts ? currentProducts.data.billboard_products : [];
|
||||
|
||||
serviceTypesError && console.log(serviceTypesError);
|
||||
productRatingsError && console.log(productRatingsError);
|
||||
productCatsError && console.log(productCatsError);
|
||||
currentProductsError && console.log(currentProductsError);
|
||||
priceDataFetchError && console.log(priceDataFetchError);
|
||||
|
||||
const serviceTypesList = serviceTypes?.data.billboard_service_types;
|
||||
const ratings = productRatings?.data.billboard_product_ratings;
|
||||
const cats: BillboardProductsCategory[] = productCats?.data.billboard_product_categories;
|
||||
const currentProductsCount: number = currentProducts?.data.billboard_products_aggregated[0].countDistinct.id;
|
||||
const updateQueryParams = useUpdateQueryParams();
|
||||
|
||||
|
||||
const totalProductsCount = currentProducts?.data.billboard_products_aggregated[0].countDistinct.id;
|
||||
|
||||
// methods
|
||||
const getPriceData = async (products: string[], bId: string) => {
|
||||
const results = await fetch(`${basePath()}/api/billboard/product-price-data`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
productIds: products,
|
||||
billboardId: bId
|
||||
})
|
||||
});
|
||||
|
||||
const { data, error } = await safeJson(results);
|
||||
if (error) {
|
||||
console.log(error);
|
||||
}
|
||||
if (data) {
|
||||
setPriceData(data)
|
||||
}
|
||||
}
|
||||
const handleFetchPriceData = async (products: string[], bId: string) => {
|
||||
await getPriceData(products, bId);
|
||||
}
|
||||
const getRating = (id: number) => {
|
||||
let totalRating = 0;
|
||||
let itemRatings = ratings.filter((x: any) => Number(x.product_id.id) === id);
|
||||
totalRating = itemRatings.length > 0 ? itemRatings.map((x:any) => x.product_quality).reduceRight((total: number, x: number) => total + x) : 0;
|
||||
totalRating = itemRatings.length > 0 ? itemRatings.map((x: any) => x.product_quality).reduceRight((total: number, x: number) => total + x) : 0;
|
||||
return itemRatings.length > 0 ? totalRating / itemRatings.length : 0;
|
||||
}
|
||||
|
||||
@ -310,113 +328,111 @@ const BillboardProducts: React.FunctionComponent<BillboardProductsProps> = ({ bi
|
||||
}
|
||||
}, [query])
|
||||
|
||||
useEffect(() => {
|
||||
if (productsList.length > 0) {
|
||||
handleFetchPriceData(productsList.map(x => String(x.id)), query.id as string);
|
||||
}
|
||||
}, [productsList, query, activeCats])
|
||||
|
||||
return (
|
||||
<div className="block w-full">
|
||||
{/* <HighlightedProducts
|
||||
billboardId={billboard.id}
|
||||
billboardTitle={billboardTitle}
|
||||
/> */}
|
||||
{totalProductsCount > 0 ?
|
||||
<div className="block w-full max-lg:px-4 max-lg:pt-2">
|
||||
<div className="block lg:py-[10px] lg:mt-4 border-b border-gray-200/50 pb-4">
|
||||
{cats ?
|
||||
<ProductCatsFilter
|
||||
cats={cats}
|
||||
billboard={{
|
||||
id: billboard.id,
|
||||
title: billboardTitle,
|
||||
brand_color: billboard.brand_color
|
||||
}}
|
||||
onChange={setActiveCats}
|
||||
/>
|
||||
:
|
||||
<InnerLoading loadingText={""} width={"200"} height={"100"} />
|
||||
}
|
||||
</div>
|
||||
<div className="flex items-center justify-between w-full pt-4 pb-2">
|
||||
{/* number of products */}
|
||||
<div className={`${searchVisible ? "hidden" : "flex"} md:flex shrink-0 items-center space-x-1 rtl:space-x-reverse py-2 px-3 rounded-lg bg-[var(--light-brand-color)]`}>
|
||||
<span className="inline-block text-xs first-letter:capitalize">{`${translate("number-of")} ${translate("products")}`} :</span>
|
||||
<span className="inline-block text-sm font-extrabold text-[var(--brand-color)]">{areProductsClientSide ? totalProductsCount : currentProducts?.data.billboard_products_aggregated[0].countDistinct.id}</span>
|
||||
</div>
|
||||
{/* products search */}
|
||||
<div className={`flex items-center ${searchVisible ? "max-md:w-full" : ""}`}>
|
||||
<Input
|
||||
type="search"
|
||||
value={searchValue}
|
||||
placeholder={translate("billboard-products-search-placeholder")}
|
||||
stripeHtml
|
||||
onInput={handleproductSearch}
|
||||
className={`${searchVisible ? "block" : "hidden"} md:block w-full py-2 px-4 text-sm placeholder:text-xs outline-none rounded-lg`}
|
||||
wrapperClass="w-full md:w-64"
|
||||
inputDelay={350}
|
||||
/>
|
||||
{searchVisible ?
|
||||
<XMark className="inline-block md:hidden shrink-0 size-9 lg:size-3 fill-secondary-light p-2 rounded-lg bg-white rtl:mr-2 ltr:ml-2 lg:rtl:mr-2 lg:ltr:ml-2" onClick={() => setSearchVisible(false)} />
|
||||
:
|
||||
<MagnifyingGlass className="inline-block md:hidden shrink-0 size-9 lg:size-3 fill-secondary-light p-2 rounded-lg bg-white rtl:mr-2 ltr:ml-2 lg:rtl:mr-2 lg:ltr:ml-2" onClick={() => setSearchVisible(true)} />
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* items */}
|
||||
{productRatings && priceData ?
|
||||
<div className="block w-full mb-8">
|
||||
{areProductsClientSide ?
|
||||
<ProductsContainer
|
||||
items={products.slice(itemsPerPage * (currentPage - 1), itemsPerPage * currentPage).map(x => ({ item: x, rating: getRating(Number(x.id))}))}
|
||||
priceData={priceData}
|
||||
billboardId={billboard.id}
|
||||
billboardTitle={billboardTitle}
|
||||
pageSize={itemsPerPage}
|
||||
activePage={currentPage}
|
||||
onPageSelect={setCurrentPage}
|
||||
totalItemsCount={totalProductsCount}
|
||||
productTypes={billboard.product_types}
|
||||
serviceTypes={serviceTypes ? serviceTypesList : []}
|
||||
/>
|
||||
:
|
||||
currentProducts ?
|
||||
currentProductsCount > 0 ?
|
||||
<ProductsContainer
|
||||
items={currentProducts.data.billboard_products.map((x:BillboardProduct) => ({ item: x, rating: getRating(Number(x.id)) }))}
|
||||
priceData={priceData}
|
||||
billboardId={billboard.id}
|
||||
billboardTitle={billboardTitle}
|
||||
pageSize={itemsPerPage}
|
||||
activePage={currentPage}
|
||||
onPageSelect={setCurrentPage}
|
||||
totalItemsCount={currentProductsCount}
|
||||
productTypes={billboard.product_types}
|
||||
serviceTypes={serviceTypes ? serviceTypesList : []}
|
||||
/>
|
||||
:
|
||||
<div className="flex flex-col w-4/5 mx-auto items-center py-8 mt-4">
|
||||
<BoxOpenSolid className="inline-block shrink-0 size-12 lg:size-20 fill-gray-300" />
|
||||
<span className="block text-base md:text-lg font-semibold mt-8">{translate("billboard-products-page-no-products")}</span>
|
||||
<span className="block text-xs/6 md:text-sm mt-5 text-gray-500 text-center">{translate("billboard-products-page-no-products-text")}</span>
|
||||
<Button
|
||||
type="button"
|
||||
text={translate("back")}
|
||||
className="block w-32 md:w-36 py-3 px-gi mt-10 rounded-lg text-sm md:text-base font-semibold !bg-gray-100 text-gray-600 capitalize"
|
||||
rightIcon={<ArrowLeftSolid className="inline-block size-4 fill-current rtl:mr-4 ltr:ml-4" />}
|
||||
onClick={handleGoToProducts}
|
||||
/>
|
||||
</div>
|
||||
{currentProducts ?
|
||||
<>
|
||||
{/* <HighlightedProducts
|
||||
billboardId={billboard.id}
|
||||
billboardTitle={billboardTitle}
|
||||
/> */}
|
||||
{totalProductsCount > 0 ?
|
||||
<div className="block w-full max-lg:px-4 max-lg:pt-2">
|
||||
<div className="block lg:py-[10px] lg:mt-4 border-b border-gray-200/50 pb-4">
|
||||
{cats ?
|
||||
<ProductCatsFilter
|
||||
cats={cats}
|
||||
billboard={{
|
||||
id: billboard.id,
|
||||
title: billboardTitle,
|
||||
brand_color: billboard.brand_color
|
||||
}}
|
||||
onChange={setActiveCats}
|
||||
/>
|
||||
:
|
||||
<ProductsPlaceHolder />
|
||||
<InnerLoading loadingText={""} width={"200"} height={"100"} />
|
||||
}
|
||||
</div>
|
||||
<div className="flex items-center justify-between w-full pt-4 pb-2">
|
||||
{/* number of products */}
|
||||
<div className={`${searchVisible ? "hidden" : "flex"} md:flex shrink-0 items-center space-x-1 rtl:space-x-reverse py-2 px-3 rounded-lg bg-[var(--light-brand-color)]`}>
|
||||
<span className="inline-block text-xs first-letter:capitalize">{`${translate("number-of")} ${translate("products")}`} :</span>
|
||||
<span className="inline-block text-sm font-extrabold text-[var(--brand-color)]">{totalProductsCount}</span>
|
||||
</div>
|
||||
{/* products search */}
|
||||
<div className={`flex items-center ${searchVisible ? "max-md:w-full" : ""}`}>
|
||||
<Input
|
||||
type="search"
|
||||
value={searchValue}
|
||||
placeholder={translate("billboard-products-search-placeholder")}
|
||||
stripeHtml
|
||||
onInput={handleproductSearch}
|
||||
className={`${searchVisible ? "block" : "hidden"} md:block w-full py-2 px-4 text-sm placeholder:text-xs outline-none rounded-lg`}
|
||||
wrapperClass="w-full md:w-64"
|
||||
inputDelay={350}
|
||||
/>
|
||||
{searchVisible ?
|
||||
<XMark className="inline-block md:hidden shrink-0 size-9 lg:size-3 fill-secondary-light p-2 rounded-lg bg-white rtl:mr-2 ltr:ml-2 lg:rtl:mr-2 lg:ltr:ml-2" onClick={() => setSearchVisible(false)} />
|
||||
:
|
||||
<MagnifyingGlass className="inline-block md:hidden shrink-0 size-9 lg:size-3 fill-secondary-light p-2 rounded-lg bg-white rtl:mr-2 ltr:ml-2 lg:rtl:mr-2 lg:ltr:ml-2" onClick={() => setSearchVisible(true)} />
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* items */}
|
||||
{productRatings && priceData && priceData.length > 0 ?
|
||||
<div className="block w-full mb-8">
|
||||
{(currentProducts && productsList.every(x => priceData.some(y => String(y.id) === String(x.id)))) ?
|
||||
currentProductsCount > 0 ?
|
||||
<ProductsContainer
|
||||
items={currentProducts.data.billboard_products.map((x: BillboardProduct) => ({ item: x, rating: getRating(Number(x.id)) }))}
|
||||
priceData={priceData}
|
||||
billboardId={billboard.id}
|
||||
billboardTitle={billboardTitle}
|
||||
pageSize={itemsPerPage}
|
||||
activePage={currentPage}
|
||||
onPageSelect={setCurrentPage}
|
||||
totalItemsCount={currentProductsCount}
|
||||
productTypes={billboard.product_types}
|
||||
serviceTypes={serviceTypes ? serviceTypesList : []}
|
||||
/>
|
||||
:
|
||||
<div className="flex flex-col w-4/5 mx-auto items-center py-8 mt-4">
|
||||
<BoxOpenSolid className="inline-block shrink-0 size-12 lg:size-20 fill-gray-300" />
|
||||
<span className="block text-base md:text-lg font-semibold mt-8">{translate("billboard-products-page-no-products")}</span>
|
||||
<span className="block text-xs/6 md:text-sm mt-5 text-gray-500 text-center">{translate("billboard-products-page-no-products-text")}</span>
|
||||
<Button
|
||||
type="button"
|
||||
text={translate("back")}
|
||||
className="block w-32 md:w-36 py-3 px-gi mt-10 rounded-lg text-sm md:text-base font-semibold !bg-gray-100 text-gray-600 capitalize"
|
||||
rightIcon={<ArrowLeftSolid className="inline-block size-4 fill-current rtl:mr-4 ltr:ml-4" />}
|
||||
onClick={handleGoToProducts}
|
||||
/>
|
||||
</div>
|
||||
:
|
||||
<ProductsPlaceHolder />
|
||||
}
|
||||
</div>
|
||||
:
|
||||
<InnerLoading loadingText={""} width={"200"} height={"100"} />
|
||||
}
|
||||
</div>
|
||||
:
|
||||
<InnerLoading loadingText={""} width={"200"} height={"100"} />
|
||||
<div className="w-full flex flex-col items-center justify-center bg-white px-gi pt-10 pb-4 rounded-lg shadow-bot">
|
||||
<span className="text-xl lg:text-2xl font-extrabold text-secondary-light pb-6 lg:pb-8 capitalize">" {translate("billboard-no-products-title")} "</span>
|
||||
<p className="block w-full lg:w-3/5 text-center text-sm/7 lg:text-base/9 text-secondary-light">{translate("billboard-no-products-text")}</p>
|
||||
<img src="/pics/billboard/money-gold-coin-jar.png" alt={translate("billboard-no-products-title")} className="block w-80 lg:w-96 mx-auto" />
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
:
|
||||
<div className="w-full flex flex-col items-center justify-center bg-white px-gi pt-10 pb-4 rounded-lg shadow-bot">
|
||||
<span className="text-xl lg:text-2xl font-extrabold text-secondary-light pb-6 lg:pb-8 capitalize">" {translate("billboard-no-products-title")} "</span>
|
||||
<p className="block w-full lg:w-3/5 text-center text-sm/7 lg:text-base/9 text-secondary-light">{translate("billboard-no-products-text")}</p>
|
||||
<img src="/pics/billboard/money-gold-coin-jar.png" alt={translate("billboard-no-products-title")} className="block w-80 lg:w-96 mx-auto" />
|
||||
</div>
|
||||
</>
|
||||
:
|
||||
<InnerLoading loadingText={""} width={"200"} height={"100"} />
|
||||
}
|
||||
</div>
|
||||
)
|
||||
|
||||
@ -1,8 +1,6 @@
|
||||
import React, { Children, useMemo } from "react"
|
||||
import Link from "components/link/link"
|
||||
import React, { useMemo } from "react"
|
||||
import useTranslate from "services/translation/translation"
|
||||
import { getLocaleTr, useGetRouter } from 'services/general/general'
|
||||
import { BasketShoppingSolid, CaretLeftSolid, DogPaw, OfficePhoneSolid, PopcornSolid, StethoscopeSolid, TableTennisSolid, ToriiGateSolid } from "components/icons"
|
||||
import { useGetRouter } from 'services/general/general'
|
||||
import { Billboard } from "common/types/billboard"
|
||||
import { getHours } from "services/billboard/general"
|
||||
|
||||
|
||||
@ -1,24 +1,22 @@
|
||||
import { useEffect, useState } from "react"
|
||||
import { useState } from "react"
|
||||
import Button from "components/button/button";
|
||||
import Link from "components/link/link";
|
||||
import useTranslate from "services/translation/translation";
|
||||
import Widget from "../navigation/widget";
|
||||
import Image from "components/image/image";
|
||||
import { adminAccessToken, basePath, hasValue, mtd, url, useGetRouter } from "services/general/general";
|
||||
import { basePath, hasValue, url, useGetRouter } from "services/general/general";
|
||||
import { updateAd } from "./forms/payloads";
|
||||
import { adminDataFetch, privateDataFetch } from "common/data/apollo-client";
|
||||
import { privateDataFetch } from "common/data/apollo-client";
|
||||
import { useMutation } from "@apollo/client";
|
||||
import Modal from "components/modal/modal";
|
||||
import Input from "components/input/text";
|
||||
import { ArrowRotateLeftRegular, ArrowsRotateSolid, BoxArchiveSolid, CaretDownSolid, ChevronDownSolid, CreditCardSolid, ImageSharpSolid, InboxOutSolid, MagnifyingGlass, PenSolid, PlusSolid, WalletSolid } from "components/icons";
|
||||
import RadioButton from "components/radio-button/radio-button";
|
||||
import { useAppSelector } from "common/redux/hooks";
|
||||
import { userData } from "common/redux/slices/user";
|
||||
import { updateUserData } from "../account/payloads";
|
||||
import { newPaymentQuery, updatePaidAdQuery, updateUserWalletQuery } from "services/queries/data-queries";
|
||||
import { addNewPayment } from "services/queries/directus/cms";
|
||||
import { getStripeAdsProductId } from "services/general/stripe";
|
||||
import { useAuth } from "services/user/AuthContext";
|
||||
import { getStripeAdsProductData } from "services/general/stripe";
|
||||
import EventAlert from "components/popover/event-alert";
|
||||
import { safeJson } from "lib/general/safe-response-json";
|
||||
import { createStripePaymentSession } from "lib/stripe/create-stripe-payment-session";
|
||||
|
||||
interface Item {
|
||||
id: string;
|
||||
@ -31,8 +29,8 @@ interface Item {
|
||||
paid: boolean;
|
||||
status: "draft" | "published" | "archived";
|
||||
category: {
|
||||
market_categories_id: {
|
||||
parent: {
|
||||
market_categories_id: {
|
||||
parent: {
|
||||
related_market_categories_id: {
|
||||
id: string;
|
||||
}
|
||||
@ -40,7 +38,7 @@ interface Item {
|
||||
}
|
||||
}[];
|
||||
ad_owner_pic: {
|
||||
id: string
|
||||
id: string
|
||||
};
|
||||
gallery: {
|
||||
directus_files_id: {
|
||||
@ -72,7 +70,7 @@ interface TableItem {
|
||||
onOpenPay?: (id: string, title: string, pic: any) => void;
|
||||
}
|
||||
|
||||
const itemTitle = (item:any, locale:string | undefined) => item.translations.filter((x:any) => x.languages_code.code.slice(0, 2) === locale)[0]?.title ?? item.translations[0].title;
|
||||
const itemTitle = (item: any, locale: string | undefined) => item.translations.filter((x: any) => x.languages_code.code.slice(0, 2) === locale)[0]?.title ?? item.translations[0].title;
|
||||
|
||||
const TableItem: React.FunctionComponent<TableItem> = ({ item, onArchive, onOpenPay, className }) => {
|
||||
|
||||
@ -104,26 +102,6 @@ const TableItem: React.FunctionComponent<TableItem> = ({ item, onArchive, onOpen
|
||||
}
|
||||
});
|
||||
|
||||
// deleting the ad data & pics
|
||||
// const [deleteItem, { data: mutationData, error: mutationErrors, loading: deleting }] = useMutation(deleteAd(item.id), {
|
||||
// client: privateDataFetch("", accessToken),
|
||||
// onError: (err) => {
|
||||
// console.log(err.message);
|
||||
// }
|
||||
// });
|
||||
// const [deleteImages, { data: deletedPicsData, error: deletePicsError, loading: deletingPics }] = useMutation(deletePics(item.gallery.map(x => x.directus_files_id.id)), {
|
||||
// client: privateDataFetch("system", accessToken),
|
||||
// onError: (err) => {
|
||||
// console.log(err.message);
|
||||
// }
|
||||
// });
|
||||
// const [deleteOwnerImage, { data: deletedOwnerPicData, error: deleteOwnerPicError, loading: deletingOwnerPic }] = useMutation(deletePics([hasValue(item.ad_owner_pic) ? item.ad_owner_pic.id : ""]), {
|
||||
// client: privateDataFetch("system", accessToken),
|
||||
// onError: (err) => {
|
||||
// console.log(err.message);
|
||||
// }
|
||||
// });
|
||||
|
||||
// methods
|
||||
const handlePaymentPopup = () => {
|
||||
onOpenPay && onOpenPay(item.id, itemTitle(item, router.locale), item.gallery.length > 0 ? item.gallery[0].directus_files_id : {
|
||||
@ -140,9 +118,6 @@ const TableItem: React.FunctionComponent<TableItem> = ({ item, onArchive, onOpen
|
||||
})
|
||||
}
|
||||
const handleArchive = () => {
|
||||
// deleteItem();
|
||||
// deleteImages();
|
||||
// hasValue(item.ad_owner_pic) && deleteOwnerImage();
|
||||
handleAdArchive();
|
||||
handleCloseArchiveModal();
|
||||
}
|
||||
@ -159,7 +134,7 @@ const TableItem: React.FunctionComponent<TableItem> = ({ item, onArchive, onOpen
|
||||
<div className="flex flex-col sm:grid sm:grid-cols-7 xl:grid-cols-5 items-center w-full">
|
||||
{/* image & title */}
|
||||
<div className="flex items-center relative w-full sm:col-span-5 xl:col-span-2">
|
||||
{item.gallery.length > 0 ?
|
||||
{item.gallery.length > 0 ?
|
||||
<Image
|
||||
src={item.gallery[0]?.directus_files_id?.id}
|
||||
alt={item.gallery[0]?.directus_files_id?.filename_download}
|
||||
@ -199,7 +174,7 @@ const TableItem: React.FunctionComponent<TableItem> = ({ item, onArchive, onOpen
|
||||
|
||||
{/* buttons */}
|
||||
<div className={`${showMore ? "max-sm:flex" : "max-sm:hidden"} flex items-center max-sm:border-t border-gray-50 max-sm:pt-4 max-sm:mt-4 sm:ltr:pl-4 sm:rtl:pr-4 lg:pl-2 lg:pb-1 w-full col-span-2 lg:col-span-1`}>
|
||||
{(item.status === "published" || item.status === "draft") &&
|
||||
{(item.status === "published" || item.status === "draft") &&
|
||||
<>
|
||||
<Button
|
||||
type="button"
|
||||
@ -217,7 +192,7 @@ const TableItem: React.FunctionComponent<TableItem> = ({ item, onArchive, onOpen
|
||||
/>
|
||||
</>
|
||||
}
|
||||
{item.status === "archived" &&
|
||||
{item.status === "archived" &&
|
||||
<Button
|
||||
type="button"
|
||||
text={translate("dashboard-ads-table-archive-ad-unarchive")}
|
||||
@ -263,7 +238,7 @@ const TableItem: React.FunctionComponent<TableItem> = ({ item, onArchive, onOpen
|
||||
const TableHeaderItem: React.FunctionComponent<TableHeaderItem> = ({ title, noIcon = false, onClick, isActive = false, className }) => {
|
||||
return <div className={`flex items-center text-secondary-light ${!noIcon ? "lg:cursor-pointer" : ""} py-4 select-none ${className}`} onClick={onClick}>
|
||||
<span className="text-current text-sm font-semibold">{title}</span>
|
||||
{!noIcon && <CaretDownSolid className={`inline-block fill-current w-3 h-3 rtl:mr-2 ltr:ml-2 ${isActive ? "rotate-0" : "rotate-180"}`} />}
|
||||
{!noIcon && <CaretDownSolid className={`inline-block fill-current w-3 h-3 rtl:mr-2 ltr:ml-2 ${isActive ? "rotate-0" : "rotate-180"}`} />}
|
||||
</div>
|
||||
}
|
||||
|
||||
@ -273,15 +248,16 @@ const AdsTable: React.FunctionComponent<AdsTable> = ({ items, onArchive, classNa
|
||||
|
||||
// states
|
||||
const pageSize = 5;
|
||||
const [dataUpdated, setDataUpdated] = useState(false);
|
||||
const [updating, setUpdating] = useState(false);
|
||||
const [alertType, setAlertType] = useState<"success" | "error">("success");
|
||||
const [alertOpen, setAlertOpen] = useState(false);
|
||||
const [pageIndex, setPageIndex] = useState(1);
|
||||
const [adItems, setAdItems] = useState(items.slice(0, pageSize));
|
||||
const [currentText, setCurrentText] = useState("");
|
||||
const [sortByDate, setSortByDate] = useState(true);
|
||||
const [paymentMethod, setPaymentMethod] = useState<"wallet" | "card">("card");
|
||||
const [searchOpen, setSearchOpen] = useState(false);
|
||||
const [payPopopOpen, setPayPopopOpen] = useState(false);
|
||||
const [itemPaid, setItemPaid] = useState(false);
|
||||
const [selectedPayment, setSelectedPayment] = useState(1);
|
||||
const [unpaidItem, setUnpaidItem] = useState({
|
||||
id: "",
|
||||
title: "",
|
||||
@ -296,54 +272,48 @@ const AdsTable: React.FunctionComponent<AdsTable> = ({ items, onArchive, classNa
|
||||
// variables
|
||||
const translate = useTranslate();
|
||||
const { locale, router } = useGetRouter();
|
||||
const { userData: uData } = useAuth();
|
||||
const user = useAppSelector(userData);
|
||||
const filterItemsBySearch = (items: any) => items.filter((x:any) => hasValue(currentText) ? itemTitle(x, locale).toLowerCase().includes(currentText.toLowerCase()) : x);
|
||||
const filterItemsBySearch = (items: any) => items.filter((x: any) => hasValue(currentText) ? itemTitle(x, locale).toLowerCase().includes(currentText.toLowerCase()) : x);
|
||||
const loadMoreText = translate("load-more");
|
||||
const hasMore = filterItemsBySearch(items).length > pageIndex * pageSize;
|
||||
|
||||
// updating wallet balance (if paid from wallet)
|
||||
const [updateWallet, { data: walletUpdated, error: walletUpdateErrors, loading: updatingWallet }] = useMutation(updateUserData(user.generalDetails.id, updateUserWalletQuery(Number(user.wallet.balance) - AdCost)), {
|
||||
client: adminDataFetch("system", adminAccessToken),
|
||||
onError: (err) => {
|
||||
console.log(err.message);
|
||||
}
|
||||
});
|
||||
|
||||
// update ad paid status after successful payment from wallet
|
||||
const [updateAdData, { data: adUpdated, error: adUpdateErrors, loading: updatingAd }] = useMutation(updateAd(hasValue(unpaidItem.id) ? unpaidItem.id : "0", updatePaidAdQuery()), {
|
||||
client: adminDataFetch("", adminAccessToken),
|
||||
onError: (err) => {
|
||||
console.log(err.message);
|
||||
}
|
||||
});
|
||||
// update ad paid status after successful paymnet from wallet
|
||||
const [addWalletPayment, { data: addedWalletPayment, error: walletPaymentErrors, loading: updatingWalletPayment }] = useMutation(addNewPayment(newPaymentQuery({
|
||||
status: "published",
|
||||
payment_status: "succeeded",
|
||||
stripe_payment_id: "",
|
||||
stripe_customer_id: user.generalDetails.stripe_id,
|
||||
owner_user_id: user.generalDetails.id,
|
||||
order_amount: String(AdCost),
|
||||
payed_amount: String(AdCost),
|
||||
currency: "cad",
|
||||
payment_method: 'wallet',
|
||||
discount_used: 'no',
|
||||
service_type: "flierland-ad",
|
||||
service_id: unpaidItem.id,
|
||||
service_duration: -1,
|
||||
service_duration_term: "month"
|
||||
})), {
|
||||
client: adminDataFetch("", adminAccessToken),
|
||||
onError: (err) => {
|
||||
console.log(err.message);
|
||||
}
|
||||
});
|
||||
// method
|
||||
const handlePayAdWithWallet = async () => {
|
||||
setUpdating(true);
|
||||
try {
|
||||
const response = await fetch(`${basePath()}/api/general/wallet/ad-payment`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
adId: unpaidItem.id,
|
||||
currency: "cad"
|
||||
}),
|
||||
})
|
||||
|
||||
const { data, error } = await safeJson(response);
|
||||
|
||||
if (!response.ok || error) {
|
||||
console.error("Wallet payment failed: ", error || 'Unknown error');
|
||||
// Optional: Show error to user (e.g., toast.error(error || 'Payment failed'))
|
||||
setUpdating(false);
|
||||
setAlertType("error");
|
||||
setAlertOpen(true);
|
||||
return; // Bail out; no success actions
|
||||
}
|
||||
|
||||
setUpdating(false);
|
||||
setAlertType("success");
|
||||
setAlertOpen(true);
|
||||
} catch (error) {
|
||||
console.error("Wallet payment failed: ", error);
|
||||
setUpdating(false);
|
||||
}
|
||||
}
|
||||
|
||||
// methods
|
||||
const handlePaymentPopup = (id: string, title: string, pic: any) => {
|
||||
setItemPaid(false);
|
||||
setPayPopopOpen(true);
|
||||
setUnpaidItem({
|
||||
id: id,
|
||||
@ -353,31 +323,37 @@ const AdsTable: React.FunctionComponent<AdsTable> = ({ items, onArchive, classNa
|
||||
}
|
||||
|
||||
const payFromWallet = async () => {
|
||||
updateWallet();
|
||||
setPaymentMethod("wallet");
|
||||
handlePayAdWithWallet();
|
||||
}
|
||||
|
||||
const payWithGateway = async () => {
|
||||
const response = await fetch(`${basePath()}/api/stripe/checkout-session`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Access-Control-Allow-Origin": "*",
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
price_id: getStripeAdsProductId("ad-cost"),
|
||||
service_type: "flierland-ad",
|
||||
service_id: unpaidItem.id,
|
||||
stripe_id: user.generalDetails.stripe_id,
|
||||
period: -1,
|
||||
user_flierland_id: user.generalDetails.id,
|
||||
user_full_name: `${user.generalDetails.first_name} ${user.generalDetails.last_name}`,
|
||||
user_email: uData.data.email,
|
||||
}),
|
||||
})
|
||||
const paymentSession = await response.json();
|
||||
router.push(paymentSession.session.url);
|
||||
setPaymentMethod("card");
|
||||
setUpdating(true);
|
||||
try {
|
||||
const paymentSession = await createStripePaymentSession({
|
||||
mode: "payment",
|
||||
payment_method_types: ["card"],
|
||||
line_items: [{ price: getStripeAdsProductData().price_id, quantity: 1 }],
|
||||
metadata: {
|
||||
price_id: getStripeAdsProductData().price_id,
|
||||
receipt_type: "flierland-ad",
|
||||
order_amount: String(getStripeAdsProductData().cost),
|
||||
adId: unpaidItem.id,
|
||||
currency: "cad"
|
||||
},
|
||||
customer: user.generalDetails.stripe_id,
|
||||
success_url: `${basePath()}/dashboard/payment-result?session_id={CHECKOUT_SESSION_ID}&success=true`,
|
||||
cancel_url: `${basePath()}/dashboard/payment-result?canceled=true`,
|
||||
});
|
||||
|
||||
router.push(paymentSession.sessionUrl);
|
||||
} catch (error) {
|
||||
console.error("Wallet payment failed: ", error);
|
||||
setUpdating(false);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const sortFunction = () => {
|
||||
return (a: any, b: any) => sortByDate ? Date.parse(b.date_created) - Date.parse(a.date_created) : Date.parse(a.date_created) - Date.parse(b.date_created);
|
||||
}
|
||||
@ -396,29 +372,28 @@ const AdsTable: React.FunctionComponent<AdsTable> = ({ items, onArchive, classNa
|
||||
setCurrentText(text);
|
||||
setAdItems(hasValue(text) ? items.filter(x => itemTitle(x, locale).toLowerCase().includes(text.toLowerCase())).slice(0, pageSize) : items.slice(0, pageSize));
|
||||
}
|
||||
const onAlertClose = () => {
|
||||
setAlertOpen(false);
|
||||
(alertOpen && alertType === "success") && router.reload();
|
||||
}
|
||||
|
||||
// useEffects
|
||||
useEffect(() => {
|
||||
// console.log("I was called!");
|
||||
walletUpdated && !adUpdated && updateAdData();
|
||||
walletUpdated && adUpdated && !addedWalletPayment && addWalletPayment();
|
||||
walletUpdated && adUpdated && addedWalletPayment && !itemPaid && setItemPaid(true);
|
||||
itemPaid && router.push(`${basePath()}/dashboard/payment-result?success=true`);
|
||||
}, [walletUpdated, adUpdated, addedWalletPayment, itemPaid, payPopopOpen])
|
||||
|
||||
useEffect(() => {
|
||||
!dataUpdated && setDataUpdated(true);
|
||||
dataUpdated && router.reload();
|
||||
}, [items])
|
||||
|
||||
|
||||
|
||||
// return
|
||||
return (
|
||||
<div className={`w-full block px-2 py-2 lg:px-4 lg:py-4 rounded-xl mb-1 lg:mb-2 ${className}`}>
|
||||
<Widget
|
||||
title={translate("my-ads")}
|
||||
icon="AdvertiseSolid"
|
||||
<EventAlert
|
||||
text={translate(alertType === "success" ? "payment-successful-message" : "payment-error-message")}
|
||||
open={alertOpen}
|
||||
type={alertType}
|
||||
timeOut={3000}
|
||||
onClose={onAlertClose}
|
||||
hasTime
|
||||
/>
|
||||
<Widget
|
||||
title={translate("my-ads")}
|
||||
icon="AdvertiseSolid"
|
||||
className="w-full capitalize !p-0"
|
||||
headerClass="px-2 py-3 md:px-4 md:py-3"
|
||||
headerItem={
|
||||
@ -506,6 +481,7 @@ const AdsTable: React.FunctionComponent<AdsTable> = ({ items, onArchive, classNa
|
||||
text={translate("ads-table-payment-popup-wallet-cta")}
|
||||
className="w-full flex items-center justify-center bg-market-input text-market-title-light text-[13px] md:text-sm text-center hover:lg:bg-market-title-light shadow-none hover:lg:text-white py-3 px-4 md:py-3 md:px-5 rounded-xl"
|
||||
onClick={payFromWallet}
|
||||
loading={paymentMethod === "wallet" && updating}
|
||||
leftIcon={<WalletSolid className="size-4 md:size-5 fill-current ltr:mr-3 rtl:ml-3 ltr:md:mr-3 rtl:md:ml-3" />}
|
||||
/>}
|
||||
<Button
|
||||
@ -514,6 +490,7 @@ const AdsTable: React.FunctionComponent<AdsTable> = ({ items, onArchive, classNa
|
||||
className="w-full flex items-center justify-center bg-market-input text-market-title-light text-[13px] md:text-sm text-center hover:lg:bg-market-title-light shadow-none hover:lg:text-white py-3 px-4 md:py-3 md:px-5 rounded-xl"
|
||||
leftIcon={<CreditCardSolid className="size-4 md:size-5 fill-current ltr:mr-3 rtl:ml-3 ltr:md:mr-3 rtl:md:ml-3" />}
|
||||
onClick={payWithGateway}
|
||||
loading={paymentMethod === "card" && updating}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -7,6 +7,7 @@ import BillboardSection from './section';
|
||||
import { mutationItemsRequest, updateBillboardItem } from 'services/queries/directus/billboard';
|
||||
import Input from 'components/input/text';
|
||||
import Label from 'components/label/label';
|
||||
import EventAlert from 'components/popover/event-alert';
|
||||
|
||||
interface BrandColorProps {
|
||||
themeColor: {
|
||||
@ -37,6 +38,7 @@ const BrandColor: React.FunctionComponent<BrandColorProps> = ({ themeColor, bran
|
||||
|
||||
// states
|
||||
const [updating, setUpdating] = useState(false);
|
||||
const [alertOpen, setAlertOpen] = useState(false);
|
||||
const [selectedColor, setSelectedColor] = useState(defaultColor);
|
||||
const [customColor, setCustomColor] = useState(selectedColor);
|
||||
const [showColorError, setShowColorError] = useState(false);
|
||||
@ -68,15 +70,19 @@ const BrandColor: React.FunctionComponent<BrandColorProps> = ({ themeColor, bran
|
||||
const handleColorSelection = async () => {
|
||||
setUpdating(true);
|
||||
try {
|
||||
const result = await mutationItemsRequest({ apiRoute: "billboard/private-mutation", mutation: updateBillboardItem(query.id, payload) });
|
||||
await mutationItemsRequest({ apiRoute: "billboard/private-mutation", mutation: updateBillboardItem(query.id, payload) });
|
||||
setUpdating(false);
|
||||
setCustomColor("");
|
||||
router.reload();
|
||||
setAlertOpen(true);
|
||||
} catch (error) {
|
||||
console.error("Update failed:", error);
|
||||
setUpdating(false);
|
||||
}
|
||||
}
|
||||
const onAlertClose = () => {
|
||||
setAlertOpen(false);
|
||||
alertOpen && router.reload();
|
||||
}
|
||||
|
||||
// useEffects
|
||||
useEffect(() => {
|
||||
@ -101,6 +107,14 @@ const BrandColor: React.FunctionComponent<BrandColorProps> = ({ themeColor, bran
|
||||
} as React.CSSProperties}
|
||||
wrapperClass="md:col-span-2"
|
||||
>
|
||||
<EventAlert
|
||||
text={translate("dashboard-changes-successfully-applied")}
|
||||
open={alertOpen}
|
||||
type="success"
|
||||
timeOut={3000}
|
||||
onClose={onAlertClose}
|
||||
hasTime
|
||||
/>
|
||||
<p className="block text-xs/6 border-b border-dashed border-gray-200/75 pb-3">{translate("dashboard-billboard-page-brand-color-text")}</p>
|
||||
<span className="block text-sm font-semibold mt-4 pb-1 px-1">{translate("dashboard-billboard-page-brand-color-suggested-title")}</span>
|
||||
<div className="flex flex-col md:flex-row md:items-center md:space-x-8 md:rtl:space-x-reverse">
|
||||
|
||||
@ -5,7 +5,7 @@ import Image from 'components/image/image';
|
||||
import ImageUploader from 'components/general/image-uploader/image-uploader';
|
||||
import Button from 'components/button/button';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { restBulkDeleteRequest, restBulkUpdateRequest } from 'services/queries/directus/billboard';
|
||||
import { restBulkUpdateRequest } from 'services/queries/directus/billboard';
|
||||
import EventAlert from 'components/popover/event-alert';
|
||||
|
||||
interface Gallery {
|
||||
@ -68,10 +68,15 @@ const CoverPhotoUpdate: React.FunctionComponent<CoverPhotoUpdateProps> = ({ cove
|
||||
const deleteOldCoverPhoto = async () => {
|
||||
try {
|
||||
// Delete old cover photo if it exists
|
||||
await restBulkDeleteRequest({
|
||||
collection: 'directus_files',
|
||||
ids: galleryData.current.picsToRemove,
|
||||
isAdmin: true,
|
||||
await fetch(`/api/billboard/tasks/self/file-delete`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
fileIds: galleryData.current.picsToRemove,
|
||||
billboardId: query.id,
|
||||
}),
|
||||
});
|
||||
|
||||
galleryData.current.picsToRemove = []; // empty pics to remove, as they're already successfully removed above.
|
||||
@ -93,11 +98,7 @@ const CoverPhotoUpdate: React.FunctionComponent<CoverPhotoUpdateProps> = ({ cove
|
||||
{
|
||||
id: String(query.id),
|
||||
data: {
|
||||
cover_photo: {
|
||||
id: pics[0].src,
|
||||
storage: 'local',
|
||||
filename_download: pics[0].name,
|
||||
},
|
||||
cover_photo: pics[0].src,
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
@ -5,7 +5,7 @@ import Image from 'components/image/image';
|
||||
import ImageUploader from 'components/general/image-uploader/image-uploader';
|
||||
import Button from 'components/button/button';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { restBulkDeleteRequest, restBulkUpdateRequest } from 'services/queries/directus/billboard';
|
||||
import { restBulkUpdateRequest } from 'services/queries/directus/billboard';
|
||||
import EventAlert from 'components/popover/event-alert';
|
||||
|
||||
interface Gallery {
|
||||
@ -68,17 +68,22 @@ const LogoUpdate: React.FunctionComponent<LogoUpdateProps> = ({ logo, open = fal
|
||||
const deleteOldLogo = async () => {
|
||||
try {
|
||||
// Delete old logo if it exists
|
||||
await restBulkDeleteRequest({
|
||||
collection: 'directus_files',
|
||||
ids: galleryData.current.picsToRemove,
|
||||
isAdmin: true,
|
||||
await fetch(`/api/billboard/tasks/self/file-delete`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
fileIds: galleryData.current.picsToRemove,
|
||||
billboardId: query.id,
|
||||
}),
|
||||
});
|
||||
|
||||
galleryData.current.picsToRemove = []; // empty pics to remove, as they're already successfully removed above.
|
||||
setUpdating(false);
|
||||
setAlertOpen(true);
|
||||
} catch (error) {
|
||||
console.error("Update failed:", error);
|
||||
console.error("Logo update failed:", error);
|
||||
setUpdating(false);
|
||||
}
|
||||
}
|
||||
@ -93,14 +98,10 @@ const LogoUpdate: React.FunctionComponent<LogoUpdateProps> = ({ logo, open = fal
|
||||
{
|
||||
id: String(query.id),
|
||||
data: {
|
||||
logo: {
|
||||
id: pics[0].src,
|
||||
storage: 'local',
|
||||
filename_download: pics[0].name,
|
||||
},
|
||||
logo: pics[0].src,
|
||||
},
|
||||
},
|
||||
],
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
import { getEngTr, getLocaleTr, getPerTr, hasValue, useGetRouter } from 'services/general/general';
|
||||
import useTranslate from 'services/translation/translation';
|
||||
import { CircleExclamationSolid, DollarSignSolid, PenSolid, PlusSolid, ShopLockSolid, TrashCanXmarkSolid, TriangleExclamationSolid, XmarkSolid } from 'components/icons';
|
||||
import { DollarSignSolid, PenSolid, PlusSolid, TriangleExclamationSolid, XmarkSolid } from 'components/icons';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import Button from 'components/button/button';
|
||||
import BillboardSection from './section';
|
||||
import Modal from 'components/modal/modal';
|
||||
import { createPriceUnitItem, deletePriceUnitItem, mutationItemsRequest, updateBillboardItem, updatePriceUnitItem } from 'services/queries/directus/billboard';
|
||||
import { createPriceUnitItem, deletePriceUnitItem, mutationItemsRequest, updatePriceUnitItem } from 'services/queries/directus/billboard';
|
||||
import EventAlert from 'components/popover/event-alert';
|
||||
import { PriceUnit } from 'common/types/billboard';
|
||||
import Input from 'components/input/text';
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { useGetRouter } from 'services/general/general';
|
||||
import useTranslate from 'services/translation/translation';
|
||||
import { ArrowLeftSolid, BellSolid, BoxOpenSolid, CircleDollarSolid, CircleQuestionSolid, MessagesSolid, RotateRightSolid, UserTieHairSolid} from 'components/icons';
|
||||
import { ArrowLeftSolid, BellSolid, BoxOpenSolid, CircleDollarSolid, CircleQuestionSolid, MessagesSolid, RotateRightSolid } from 'components/icons';
|
||||
import Link from 'components/link/link';
|
||||
|
||||
interface BillboardProductsNewsProps {
|
||||
@ -32,8 +32,6 @@ const BillboardProductsNews: React.FunctionComponent<BillboardProductsNewsProps>
|
||||
|
||||
// }, []);
|
||||
|
||||
// console.log(data);
|
||||
|
||||
// return
|
||||
return (
|
||||
<section className="grid grid-cols-1 md:grid-cols-2 gap-x-4 gap-y-2 w-full mt-4 px-4 md:col-span-2">
|
||||
|
||||
@ -1,12 +1,11 @@
|
||||
import { safeClone, useGetRouter } from 'services/general/general';
|
||||
import useTranslate from 'services/translation/translation';
|
||||
import Link from 'components/link/link';
|
||||
import { BillboardOrderReturnPolicy } from 'common/types/billboard';
|
||||
import { BillboardFormItem } from '../general/fields';
|
||||
import Select from 'components/select/select';
|
||||
import { useRef, useState } from 'react';
|
||||
import { useState } from 'react';
|
||||
import CheckBox from 'components/checkbox/checkbox';
|
||||
import { BagShoppingSolid, BasketShoppingSolid, BoxTapedSolid, CartXmarkSolid, ClockSolid, DollarSignSolid, PercentageSolid, StoreSolid, WineGlassCrackSolid } from 'components/icons';
|
||||
import { BagShoppingSolid, BoxTapedSolid, CartXmarkSolid, ClockSolid, PercentageSolid, WineGlassCrackSolid } from 'components/icons';
|
||||
|
||||
interface ReturnPolicyFormat {
|
||||
number_of_days: number;
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { adminAccessToken, adminFetchPrivateData, getSmartTime, useGetRouter } from 'services/general/general';
|
||||
import { fetchPrivateData, getSmartTime, useGetRouter } from 'services/general/general';
|
||||
import useTranslate from 'services/translation/translation';
|
||||
import { ArrowLeftSolid, BagShoppingSolid } from 'components/icons';
|
||||
import Button from 'components/button/button';
|
||||
@ -22,7 +22,7 @@ const Sales: React.FunctionComponent<SalesProps> = ({ themeColor }) => {
|
||||
// variables
|
||||
const translate = useTranslate();
|
||||
const { locale, query } = useGetRouter();
|
||||
const { data: orders, error: ordersError } = useSWR([billboardOrdersQuery(query.id), "", adminAccessToken], ([query, route, token]) => adminFetchPrivateData(route, token, query));
|
||||
const { data: orders, error: ordersError } = useSWR([billboardOrdersQuery(query.id), ""], ([query, route]) => fetchPrivateData(route, query));
|
||||
ordersError && console.log(ordersError);
|
||||
const ordersList: BillboardOrdersItem[] = orders?.data.billboard_orders;
|
||||
|
||||
@ -43,9 +43,6 @@ const Sales: React.FunctionComponent<SalesProps> = ({ themeColor }) => {
|
||||
// methods
|
||||
|
||||
// useEffects
|
||||
// useEffect(() => {
|
||||
// getTheme !== Sales && setSales(getTheme);
|
||||
// }, [dashboardTheme]);
|
||||
|
||||
// return
|
||||
return (
|
||||
|
||||
@ -1,13 +1,12 @@
|
||||
import { adminAccessToken, adminFetchPrivateData, getWeeksBetween, hasValue, mtd, useGetRouter } from 'services/general/general';
|
||||
import { fetchPrivateData, getWeeksBetween, hasValue, mtd, useGetRouter } from 'services/general/general';
|
||||
import useTranslate from 'services/translation/translation';
|
||||
import { ArrowLeftSolid, ChartSimpleSolid, CircleSolid, EyeSolid, SackDollarSolid, StarSolid } from 'components/icons';
|
||||
import { ChartSimpleSolid, CircleSolid, EyeSolid, SackDollarSolid, StarSolid } from 'components/icons';
|
||||
import BillboardSection from './section';
|
||||
import useSWR from 'swr';
|
||||
import { billboardOrdersQuery } from 'services/queries/directus/billboard';
|
||||
import { BillboardOrdersItem } from 'common/types/billboard';
|
||||
// import { Chart as ChartJS, LineElement, PointElement, LinearScale, CategoryScale, Title, Tooltip, Legend, ChartOptions, } from 'chart.js';
|
||||
import InnerLoading from 'components/loading/inner-loading';
|
||||
import Button from 'components/button/button';
|
||||
import { CartesianGrid, Line, LineChart, ResponsiveContainer, Tooltip, XAxis, YAxis } from 'recharts';
|
||||
|
||||
interface StatisticsOverviewProps {
|
||||
@ -43,8 +42,9 @@ const StatisticsOverview: React.FunctionComponent<StatisticsOverviewProps> = ({
|
||||
return agrVisits.map(x => x.length);
|
||||
}
|
||||
|
||||
const { data: orders, error: ordersError } = useSWR([billboardOrdersQuery(query.id), "", adminAccessToken], ([query, route, token]) => adminFetchPrivateData(route, token, query));
|
||||
const { data: orders, error: ordersError } = useSWR([billboardOrdersQuery(query.id), ""], ([query, route]) => fetchPrivateData(route, query));
|
||||
ordersError && console.log(ordersError);
|
||||
|
||||
const ordersList: BillboardOrdersItem[] = orders?.data.billboard_orders;
|
||||
|
||||
const highlighIconClass = "inline-block size-2 fill-current text-[var(--brand-color)] shrink-0";
|
||||
|
||||
@ -1,12 +1,10 @@
|
||||
import { useAppDispatch, useAppSelector } from "common/redux/hooks";
|
||||
import { setUserData, userData } from "common/redux/slices/user";
|
||||
import { Billboard } from "common/types/billboard";
|
||||
import { Messages, MessagesRecipient, NotificationsContactList } from "common/types/general";
|
||||
import { AddressBookSolid, BarsSolid, BellSlashSolid, BellSolid, BookmarkSolid, CircleCheckSolid, CircleSolid, EllipsisSolid, MagnifyingGlass, MessagesSolid, StoreSolid, TrashCanXmarkSolid, XMark, XmarkSolid } from "components/icons";
|
||||
import { MessagesRecipient, NotificationsContactList } from "common/types/general";
|
||||
import { AddressBookSolid, BarsSolid, BellSlashSolid, BellSolid, BookmarkSolid, EllipsisSolid, MagnifyingGlass, TrashCanXmarkSolid, XMark, XmarkSolid } from "components/icons";
|
||||
import Image from "components/image/image";
|
||||
import Link from "components/link/link";
|
||||
import { useEffect, useRef, useState } from "react"
|
||||
import { getEngTr, getLocaleTr, getPerTr, getSmartTime, hasValue, smartTimeStamp, stripHtml, useGetRouter } from "services/general/general";
|
||||
import { getEngTr, getLocaleTr, getPerTr, hasValue, smartTimeStamp, stripHtml, useGetRouter } from "services/general/general";
|
||||
import useTranslate from "services/translation/translation";
|
||||
import MessageItem from "./message-item";
|
||||
import InnerLoading from "components/loading/inner-loading";
|
||||
@ -14,7 +12,7 @@ import Mustache from 'mustache';
|
||||
import EventAlert from "components/popover/event-alert";
|
||||
import Modal from "components/modal/modal";
|
||||
import Button from "components/button/button";
|
||||
import { mutationItemsRequest, updateMessageRecipientsItem, updateNotificationsContactListItem } from "services/queries/directus/billboard";
|
||||
import { mutationItemsRequest, updateNotificationsContactListItem } from "services/queries/directus/billboard";
|
||||
import Input from "components/input/text";
|
||||
import ClickOutside from "components/click-outside/click-outside";
|
||||
|
||||
|
||||
@ -72,8 +72,8 @@ const GeneralLayout: React.FunctionComponent<GeneralLayout> = ({ children, bodyC
|
||||
|
||||
const { data: messagesData, error: messageFetchError, mutate } = useSWR(isUserDataAvailable ? [`${messagesDataQuery + contactListQuery}`, ""] : null, ([query, route]) => fetchPrivateData(route, query));
|
||||
|
||||
const { data: globalData, error: globalDataError } = useCachedGlobalData();
|
||||
// const { data: globalData, error: globalDataError } = process.env.NODE_ENV === 'production' ? useCachedGlobalData() : useSWRImmutable([globalDataQuery, ""], ([query, route]) => fetchPublicData(route, query));
|
||||
// const { data: globalData, error: globalDataError } = useCachedGlobalData();
|
||||
const { data: globalData, error: globalDataError } = process.env.NODE_ENV === 'production' ? useCachedGlobalData() : useSWRImmutable([globalDataQuery, ""], ([query, route]) => fetchPublicData(route, query));
|
||||
const loadingText = translate("general-loading");
|
||||
|
||||
if (messageFetchError) {
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import Button from 'components/button/button';
|
||||
import { useAppSelector, useAppDispatch } from '../../../redux/hooks';
|
||||
import { useAppSelector } from '../../../redux/hooks';
|
||||
import Widget from '../navigation/widget';
|
||||
import useTranslate from 'services/translation/translation';
|
||||
import { CreditCardSolid, PlusSolid } from 'components/icons';
|
||||
@ -9,7 +9,7 @@ import RadioButton from 'components/radio-button/radio-button';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { userData } from 'common/redux/slices/user';
|
||||
import { getStripeWalletProductId } from 'services/general/stripe';
|
||||
import { useAuth } from 'services/user/AuthContext';
|
||||
import { createStripePaymentSession } from 'lib/stripe/create-stripe-payment-session';
|
||||
|
||||
interface Balance {
|
||||
balance: number;
|
||||
@ -23,7 +23,6 @@ const Balance: React.FunctionComponent<Balance> = ({ balance }) => {
|
||||
|
||||
// variables
|
||||
const translate = useTranslate();
|
||||
const { userData: uData, } = useAuth();
|
||||
const { locale, router } = useGetRouter();
|
||||
const user = useAppSelector(userData);
|
||||
const topupAmounts = [
|
||||
@ -36,24 +35,20 @@ const Balance: React.FunctionComponent<Balance> = ({ balance }) => {
|
||||
|
||||
// methods
|
||||
const handleTopUp = async () => {
|
||||
const response = await fetch(`${basePath()}/api/stripe/checkout-session`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Access-Control-Allow-Origin": "*",
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
const paymentSession = await createStripePaymentSession({
|
||||
mode: "payment",
|
||||
payment_method_types: ["card"],
|
||||
line_items: [{ price: getStripeWalletProductId(topupAmounts.filter(x => x.amount === paymentAmount)[0].amount), quantity: 1 }],
|
||||
metadata: {
|
||||
price_id: getStripeWalletProductId(topupAmounts.filter(x => x.amount === paymentAmount)[0].amount),
|
||||
service_type: "flierland-wallet-top-up",
|
||||
stripe_id: user.generalDetails.stripe_id,
|
||||
user_flierland_id: user.generalDetails.id,
|
||||
user_full_name: `${user.generalDetails.first_name} ${user.generalDetails.last_name}`,
|
||||
user_email: uData.data.email,
|
||||
user_wallet_balance: Number(balance),
|
||||
}),
|
||||
})
|
||||
const paymentSession = await response.json();
|
||||
router.push(paymentSession.session.url);
|
||||
receipt_type: "flierland-wallet-top-up",
|
||||
order_amount: String(paymentAmount),
|
||||
},
|
||||
customer: user.generalDetails.stripe_id,
|
||||
success_url: `${basePath()}/dashboard/payment-result?session_id={CHECKOUT_SESSION_ID}`,
|
||||
cancel_url: `${basePath()}/dashboard/payment-result?canceled=true`,
|
||||
});
|
||||
router.push(paymentSession.sessionUrl);
|
||||
}
|
||||
|
||||
// useEffects
|
||||
|
||||
@ -2,7 +2,7 @@ import SmartTable from 'components/tables/smart-table';
|
||||
import { useAppSelector, useAppDispatch } from '../../../redux/hooks';
|
||||
import Widget from '../navigation/widget';
|
||||
import useTranslate from 'services/translation/translation';
|
||||
import { fetchPrivateData, hasValue, numberFormatterFixed, useGetRouter } from 'services/general/general';
|
||||
import { fetchPrivateData, getLocaleTr, hasValue, numberFormatterFixed, useGetRouter } from 'services/general/general';
|
||||
import { userData } from 'common/redux/slices/user';
|
||||
import { useEffect, useState } from 'react';
|
||||
import InnerLoading from 'components/loading/inner-loading';
|
||||
@ -10,6 +10,7 @@ import { CircleExclamationSolid } from 'components/icons';
|
||||
import Modal from 'components/modal/modal';
|
||||
import Button from 'components/button/button';
|
||||
import useSWR from 'swr';
|
||||
import { PaymentData } from 'common/types/payments';
|
||||
|
||||
interface PaymentHistory {
|
||||
|
||||
@ -51,27 +52,36 @@ const PaymentHistory: React.FunctionComponent<PaymentHistory> = ({ }) => {
|
||||
status
|
||||
payment_status
|
||||
date_created
|
||||
currency
|
||||
currency {
|
||||
id
|
||||
sign
|
||||
translations {
|
||||
languages_code {
|
||||
code
|
||||
}
|
||||
name
|
||||
}
|
||||
}
|
||||
order_amount
|
||||
payed_amount
|
||||
payment_method
|
||||
discount_used
|
||||
discount_code
|
||||
service_type
|
||||
receipt_type
|
||||
service_id
|
||||
service_duration
|
||||
service_duration_term
|
||||
}
|
||||
`
|
||||
const { data: paymentsData, error: paymentsDataError } = useSWR(["", paymentsQuery,], ([route, query]) => fetchPrivateData(route, query));
|
||||
const { data: paymentsData, error: paymentsDataError } = useSWR(["", paymentsQuery], ([route, query]) => fetchPrivateData(route, query));
|
||||
|
||||
const paymentsList = paymentsData?.data.Payments.map((x: any) => x).sort((a: any, b: any) => (Date.parse(b.date_created)) - Date.parse(a.date_created)).map((x: any) => (
|
||||
const paymentsList = paymentsData?.data.Payments.map((x: PaymentData) => x).sort((a: any, b: any) => (Date.parse(b.date_created)) - Date.parse(a.date_created)).map((x: PaymentData) => (
|
||||
{
|
||||
title: x.id.slice(0, 8),
|
||||
pic: "/pics/logos/mc-logo.png",
|
||||
date: x.date_created.slice(0, 10),
|
||||
amount: `$${numberFormatterFixed(Number(x.payed_amount), ",")}`,
|
||||
currency: translate(x.currency)
|
||||
amount: `${x.currency.sign}${numberFormatterFixed(Number(x.payed_amount), ",")}`,
|
||||
currency: getLocaleTr(x.currency, locale).name
|
||||
}
|
||||
));
|
||||
|
||||
@ -81,12 +91,12 @@ const PaymentHistory: React.FunctionComponent<PaymentHistory> = ({ }) => {
|
||||
{ label: translate("profile-payment-receipt-status"), value: paymentDetails.payment_status ? "✔ " + translate(paymentDetails.payment_status) : "", className: "", valueClass:"!text-green-700" },
|
||||
{ label: translate("profile-payment-receipt-payment-method"), value: paymentDetails.payment_method ? translate(paymentDetails.payment_method.replace(/_/g, '-')) : "", className: "", valueClass: "" },
|
||||
{ label: translate("profile-payment-receipt-used-discount"), value: paymentDetails.discount_used ? translate(`logic-${paymentDetails.discount_used}`) : "", className: "", valueClass: "" },
|
||||
{ label: translate("profile-payment-receipt-service-type"), value: paymentDetails.service_type ? translate(paymentDetails.service_type) : "", className: "", valueClass: "" },
|
||||
{ label: translate("profile-payment-receipt-service-type"), value: paymentDetails.receipt_type ? translate(paymentDetails.receipt_type) : "", className: "", valueClass: "" },
|
||||
{ label: translate("profile-payment-receipt-service-id"), value: hasValue(paymentDetails.service_id) ? paymentDetails.service_id : "-", className: "", valueClass: "" },
|
||||
{ label: translate("profile-payment-receipt-service-duration"), value: (paymentDetails.service_duration !== -1 && paymentDetails.service_duration_term) ? `${paymentDetails.service_duration} ${paymentDetails.service_duration_term}${locale === "en" && paymentDetails.service_duration > 1 ? "s" : ""}` : "-", className: "", valueClass: "" },
|
||||
{ label: translate("profile-payment-receipt-total-amount"), value: `$${numberFormatterFixed(Number(paymentDetails.order_amount), ",")}`, className: "border-t-2 border-dashed border-gray-300 mt-4 pt-4", valueClass: "!text-market-title-light" },
|
||||
{ label: translate("profile-payment-receipt-paid-amount"), value: `$${numberFormatterFixed(Number(paymentDetails.payed_amount), ",")}`, className: "", valueClass: "!text-market-title-light" },
|
||||
{ label: translate("profile-payment-history-currency"), value: paymentDetails.currency ? translate(paymentDetails.currency) : "", className: "", valueClass: "" },
|
||||
{ label: translate("profile-payment-receipt-total-amount"), value: `${paymentDetails.currency?.sign}${numberFormatterFixed(Number(paymentDetails.order_amount), ",")}`, className: "border-t-2 border-dashed border-gray-300 mt-4 pt-4", valueClass: "!text-market-title-light" },
|
||||
{ label: translate("profile-payment-receipt-paid-amount"), value: `${paymentDetails.currency?.sign}${numberFormatterFixed(Number(paymentDetails.payed_amount), ",")}`, className: "", valueClass: "!text-market-title-light" },
|
||||
{ label: translate("profile-payment-history-currency"), value: paymentDetails.currency ? getLocaleTr(paymentDetails.currency, locale).name : "", className: "", valueClass: "" },
|
||||
]
|
||||
|
||||
// methods
|
||||
|
||||
157
src/common/templates/general/checkout/cart-item.tsx
Normal file
157
src/common/templates/general/checkout/cart-item.tsx
Normal file
@ -0,0 +1,157 @@
|
||||
import { useAppDispatch } from "common/redux/hooks";
|
||||
import { setShoppingCart } from "common/redux/slices/global";
|
||||
import { ShoppingCartItem } from "common/types/billboard";
|
||||
import { Currencies } from "common/types/general";
|
||||
import { BoxTapedSolid, CircleCheckSolid, CircleExclamationSolid, ImageSharpSolid, MinusSolid, PlusSolid, XmarkSolid } from "components/icons";
|
||||
import Image from "components/image/image";
|
||||
import Link from "components/link/link";
|
||||
import { useEffect, useState } from "react";
|
||||
import { productAvailability } from "services/billboard/general";
|
||||
import { hasValue, numberFormatterFixed, url, useGetRouter } from "services/general/general";
|
||||
import useTranslate from "services/translation/translation";
|
||||
|
||||
|
||||
interface CartItemProps {
|
||||
item: ShoppingCartItem;
|
||||
items: ShoppingCartItem[]; // every other item in the list except the current item
|
||||
currency: Currencies;
|
||||
policiesData: any;
|
||||
onChange: ({ id, totalPrice, inStock }: { id: number; totalPrice: number; inStock: { id: number, available: boolean } }) => void;
|
||||
}
|
||||
|
||||
const CartItem = ({ items, item, currency, policiesData, onChange }: CartItemProps) => {
|
||||
|
||||
// states
|
||||
const [orderQuantity, setOrderQuantity] = useState(item.quantity);
|
||||
|
||||
// variables
|
||||
const translate = useTranslate();
|
||||
const { locale } = useGetRouter();
|
||||
const dispatch = useAppDispatch();
|
||||
const storeHasEnoughItems = orderQuantity <= item.stockCount // returns item's id if not avilable
|
||||
|
||||
const itemPrice = item.discountedPrice ?? item.unitPrice;
|
||||
const itemTotalPrice = Number(itemPrice) * orderQuantity;
|
||||
const availability = productAvailability({ stock: item.stockCount, lowStockTreshold: item.lowStockTreshold });
|
||||
const policiesIconClass = "inline-block size-3 sm:size-4 fill-current me-2";
|
||||
|
||||
const returnPolicy = {
|
||||
id: policiesData.returnPolicies[0].id,
|
||||
icon: <BoxTapedSolid className={`${policiesIconClass}`} />,
|
||||
label: translate("billboard-product-page-order-return-label"),
|
||||
value: policiesData.returnPolicies[0].no_return ? translate("no-return") :
|
||||
`
|
||||
${policiesData.returnPolicies[0].number_of_days} ${translate("day")}
|
||||
, ${policiesData.returnPolicies[0].refund_percentage}% ${translate("refund")}
|
||||
${policiesData.returnPolicies[0].defective_only ? `, ${translate("defective-only")}` : ""}
|
||||
${policiesData.returnPolicies[0].store_credit_only ? `, ${translate("store-ceredit-only")}` : ""}
|
||||
${!policiesData.returnPolicies[0].includes_shipping_fee ? `, ${translate("shipping-fee-not-included")}` : `, ${translate("shipping-fee-included")}`}
|
||||
`
|
||||
}
|
||||
|
||||
// methods
|
||||
const handleRemoveItem = () => {
|
||||
dispatch(setShoppingCart([...items]));
|
||||
onChange({ id: item.id, totalPrice: 0, inStock: { id: item.id, available: storeHasEnoughItems } });
|
||||
}
|
||||
const handleIncreaseQuantity = () => {
|
||||
availability.isLow ? setOrderQuantity(orderQuantity + 1) : setOrderQuantity(orderQuantity + 1);
|
||||
// availability.isLow ? item.stockCount > orderQuantity && setOrderQuantity(orderQuantity + 1) : setOrderQuantity(orderQuantity + 1);
|
||||
}
|
||||
const handleDecreaseQuantity = () => {
|
||||
orderQuantity > 1 && setOrderQuantity(orderQuantity - 1);
|
||||
}
|
||||
|
||||
// useEffects
|
||||
useEffect(() => {
|
||||
onChange({ id: item.id, totalPrice: orderQuantity * item.unitPrice, inStock: { id: item.id, available: storeHasEnoughItems } });
|
||||
dispatch(setShoppingCart([...items, { ...item, quantity: orderQuantity }]));
|
||||
}, [orderQuantity])
|
||||
|
||||
|
||||
return <li className="flex sm:items-center pt-2 pb-3 max-sm:pe-2 sm:px-2 w-full border-b border-dashed border-gray-100 last:border-0">
|
||||
<Link
|
||||
scroll={false}
|
||||
href={item.billboard ? `/billboard/${item.billboard.id}/${url((item.billboard.title as any)[locale as string])}/products/${item.product_id}` : ""}
|
||||
className="inline-block shrink-0 max-sm:pt-4"
|
||||
>
|
||||
{item.pic ?
|
||||
<Image
|
||||
src={`${item.pic.id}/${item.pic.filename_download}`}
|
||||
alt={translate("pic-of") + item.title}
|
||||
width={item.pic.width}
|
||||
height={item.pic.height}
|
||||
quality={75}
|
||||
ar={[1 / 1, 1 / 1, 1 / 1, 1 / 1]}
|
||||
imageSizes={[100, 250, 250, 250]}
|
||||
priority={true}
|
||||
noPreload={true}
|
||||
fetchPriority="low"
|
||||
className="rounded-full will-change-transform !object-contain"
|
||||
figureClass="rounded-full !size-20 transition-transform duration-200 select-none [&>div]:!bg-transparent select-none"
|
||||
/>
|
||||
:
|
||||
<span className="block aspect-square size-20 rounded-lg lg:rounded">
|
||||
<ImageSharpSolid className="inline-block absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 size-16 xl:size-24 fill-gray-200" />
|
||||
</span>
|
||||
}
|
||||
</Link>
|
||||
<div className="block w-full rtl:mr-3 ltr:ml-3 sm:rtl:mr-5 sm:ltr:ml-5">
|
||||
{/* title */}
|
||||
<div className="flex justify-between space-x-10 rtl:space-x-reverse pb-1">
|
||||
<Link
|
||||
scroll={false}
|
||||
href={item.billboard ? `/billboard/${item.billboard.id}/${url((item.billboard.title as any)[locale as string])}/products/${item.product_id}` : ""}
|
||||
className="text-xs/6 sm:text-sm/[1.625rem] font-extrabold text-secondary-light capitalize line-clamp-1 select-none"
|
||||
>
|
||||
{`${(item.title as any)[locale as string].slice(0, 40)}${(item.title as any)[locale as string].length > 40 ? "..." : ""}`}
|
||||
</Link>
|
||||
<XmarkSolid className="reactive-button inline-block size-6 fill-cool-gray ring-1 ring-cool-gray hover:bg-rose-50 hover:fill-primary hover:ring-rose-50 rounded-lg p-1 shrink-0 mt-1" onClick={handleRemoveItem} />
|
||||
</div>
|
||||
<div className="flex items-center justify-between w-full shrink-0 select-none pb-2">
|
||||
<div className="flex flex-col">
|
||||
{/* item details */}
|
||||
{(hasValue(item.color.fa) || hasValue(item.size)) && <div className="flex items-center mb-2">
|
||||
{hasValue(item.color.fa) && <span className="flex items-center w-max capitalize text-xs sm:text-xs text-gray-400 rtl:pl-1 ltr:pr-1">{`${locale === "fa" ? item.color.fa : item.color.en} ${locale === "fa" ? "-" : "-"}`}</span>}
|
||||
{hasValue(item.size) && <span className="flex items-center w-max uppercase text-xs sm:text-xs text-gray-400">{`${item.size}`}</span>}
|
||||
</div>}
|
||||
{/* price */}
|
||||
<span className="text-sm sm:text-base font-extrabold text-market-title-light mt-1">{`${currency.sign}${numberFormatterFixed(itemTotalPrice, ",")}`}</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between ltr:flex-row-reverse text-[var(--brand-color)] space-x-3 space-x-reverse self-end">
|
||||
<PlusSolid className="reactive-button inline-block size-6 fill-white p-[6px] rounded-full bg-market-title-light" onClick={handleIncreaseQuantity} />
|
||||
<span className="inline-block text-secondary-light text-base font-extrabold select-none">{orderQuantity}</span>
|
||||
<MinusSolid className="reactive-button inline-block size-6 fill-white p-[6px] rounded-full bg-cool-gray" onClick={handleDecreaseQuantity} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="inline-flex flex-col max-sm:pt-2">
|
||||
<span className={`inline-flex items-center text-[0.625rem]/5 sm:text-xs ${availability.isLow ? "bg-warning-bg text-warning-text" : "bg-success-bg text-success-text"} py-[2px] sm:py-[5px] px-2 rounded-lg`}>
|
||||
{availability.isLow ?
|
||||
<CircleExclamationSolid className="inline-block size-3 rtl:ml-2 ltr:mr-2 fill-warning-text" />
|
||||
:
|
||||
<CircleCheckSolid className="inline-block size-3 rtl:ml-2 ltr:mr-2 fill-success-text" />
|
||||
}
|
||||
{locale === "fa" ? availability.fa : availability.en}
|
||||
</span>
|
||||
{!storeHasEnoughItems &&
|
||||
<span className={`inline-flex items-center text-[0.625rem] sm:text-xs bg-error-bg text-error-text py-[2px] sm:py-[5px] px-2 rounded-lg mt-2`}>
|
||||
<CircleExclamationSolid className="inline-block size-3 rtl:ml-2 ltr:mr-2 fill-warning-text" />
|
||||
{translate("shopping-cart-review-order-not-enough-stock")}
|
||||
</span>
|
||||
}
|
||||
</div>
|
||||
<ul className="list-none block w-full sm:pt-1">
|
||||
<li key={returnPolicy.id} className="flex max-sm:flex-col items-start w-full py-1">
|
||||
<span className="flex items-center text-[0.625rem]/5 sm:text-xs/6 font-semibold bg-warning-bg text-warning-text px-2 py-[2px] rounded-lg shrink-0">
|
||||
{returnPolicy.icon}
|
||||
{`${returnPolicy.label} :`}
|
||||
</span>
|
||||
<span className="inline-block text-[0.625rem]/5 sm:text-xs/6 px-2 text-secondary-light max-sm:pt-2">
|
||||
{returnPolicy.value}
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</li>
|
||||
}
|
||||
export default CartItem;
|
||||
@ -0,0 +1,39 @@
|
||||
import { BasketShoppingSolid, ClockRotateLeftSolid, ListSolid } from "components/icons";
|
||||
import Link from "components/link/link";
|
||||
import useTranslate from "services/translation/translation";
|
||||
|
||||
|
||||
interface EmptyShoppingCartProps {
|
||||
|
||||
}
|
||||
|
||||
const EmptyShoppingCart = ({ }: EmptyShoppingCartProps) => {
|
||||
|
||||
// states
|
||||
|
||||
// variables
|
||||
const translate = useTranslate();
|
||||
|
||||
return <div className="w-full sm:w-[600px] lg:w-[800px] mx-auto flex flex-col items-center justify-center bg-white pt-8 lg:pt-12 pb-4 rounded-lg">
|
||||
<span className="text-lg lg:text-2xl font-extrabold text-secondary-light pb-4 lg:pb-10 capitalize">{translate("checkout-page-cart-empty-title")}</span>
|
||||
<p className="block w-4/5 lg:w-4/5 text-center text-xs/6 lg:text-sm/8 text-secondary-light lg:mb-4">{translate("checkout-page-cart-empty-text")}</p>
|
||||
<div className="flex items-center flex-col md:flex-row gap-4 md:gap-8 w-5/6 px-2 pt-6 lg:pt-8 mx-auto">
|
||||
<Link
|
||||
href={"/billboard"}
|
||||
className="reactive-button flex items-center justify-center w-full py-3 px-4 rounded-xl shadow-none bg-market-title-light text-white text-sm ring-2 ring-market-title-light sm:text-base"
|
||||
>
|
||||
<BasketShoppingSolid className="inline-block size-5 sm:size-6 fill-current rtl:ml-4 ltr:mr-4" />
|
||||
{translate("checkout-page-visit-shops")}
|
||||
</Link>
|
||||
<Link
|
||||
href={"/dashboard/my-orders"}
|
||||
className="reactive-button flex items-center justify-center w-full py-3 px-4 rounded-xl shadow-none bg-white text-market-title-light ring-2 ring-market-title-light text-sm sm:text-base"
|
||||
>
|
||||
<ListSolid className="inline-block size-5 sm:size-6 fill-current rtl:ml-4 ltr:mr-4" />
|
||||
{translate("checkout-page-see-orders-cta")}
|
||||
</Link>
|
||||
</div>
|
||||
<img src="/pics/shopping-pana.svg" alt={translate("dashboard-billboard-faq-no-questions-title")} className="block w-80 lg:w-[450px] mx-auto mt-10 sm:mt-16 mb-6" />
|
||||
</div>
|
||||
}
|
||||
export default EmptyShoppingCart;
|
||||
39
src/common/templates/general/checkout/not-logged-in.tsx
Normal file
39
src/common/templates/general/checkout/not-logged-in.tsx
Normal file
@ -0,0 +1,39 @@
|
||||
import { LockKeyHoleSolid, UserPlusSolid } from "components/icons";
|
||||
import Link from "components/link/link";
|
||||
import useTranslate from "services/translation/translation";
|
||||
|
||||
|
||||
interface NotLoggedInProps {
|
||||
|
||||
}
|
||||
|
||||
const NotLoggedIn = ({ }: NotLoggedInProps) => {
|
||||
|
||||
// states
|
||||
|
||||
// variables
|
||||
const translate = useTranslate();
|
||||
|
||||
return <div className="w-full sm:w-[600px] lg:w-[800px] mx-auto flex flex-col items-center justify-center bg-white pt-8 lg:pt-12 pb-4 rounded-lg">
|
||||
<span className="text-lg lg:text-2xl font-extrabold text-secondary-light pb-4 lg:pb-10 capitalize">{translate("checkout-page-login-needed-title")}</span>
|
||||
<p className="block w-4/5 lg:w-4/5 text-center text-xs/6 lg:text-sm/8 text-secondary-light lg:mb-4">{translate("checkout-page-login-needed-text")}</p>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3 sm:gap-4 px-4 sm:px-2 pt-10 lg:pt-8 w-full sm:w-4/5">
|
||||
<Link
|
||||
href={"/sign-in"}
|
||||
className="reactive-button flex items-center justify-center w-full py-3 px-4 rounded-xl shadow-none bg-market-title-light text-white text-sm sm:text-base"
|
||||
>
|
||||
<LockKeyHoleSolid className="inline-block size-5 fill-current rtl:ml-4 ltr:mr-4" />
|
||||
{translate("sign-in")}
|
||||
</Link>
|
||||
<Link
|
||||
href={"/sign-in"}
|
||||
className="reactive-button flex items-center justify-center w-full py-3 px-4 rounded-xl shadow-none ring-2 ring-market-title-light text-market-title-light text-sm sm:text-base"
|
||||
>
|
||||
<UserPlusSolid className="inline-block size-5 fill-current rtl:ml-4 ltr:mr-4" />
|
||||
{translate("user-register-text")}
|
||||
</Link>
|
||||
</div>
|
||||
<img src="/pics/sign-in-pana.svg" alt={translate("dashboard-billboard-faq-no-questions-title")} className="block w-80 lg:w-[450px] mx-auto mt-10 sm:mt-16 mb-6" />
|
||||
</div>
|
||||
}
|
||||
export default NotLoggedIn;
|
||||
299
src/common/templates/general/checkout/payment-method.tsx
Normal file
299
src/common/templates/general/checkout/payment-method.tsx
Normal file
@ -0,0 +1,299 @@
|
||||
import { useAppDispatch } from "common/redux/hooks";
|
||||
import { setShoppingCart } from "common/redux/slices/global";
|
||||
import { BillboardOrdersItem, ShoppingCartItem } from "common/types/billboard";
|
||||
import { Currencies } from "common/types/general";
|
||||
import { UserData } from "common/types/user";
|
||||
import Button from "components/button/button";
|
||||
import { CartSolid, CreditCardSolid, WalletSolid } from "components/icons";
|
||||
import Modal from "components/modal/modal";
|
||||
import EventAlert from "components/popover/event-alert";
|
||||
import RadioButton from "components/radio-button/radio-button";
|
||||
import { StoreProps } from "pages/checkout";
|
||||
import { useState } from "react";
|
||||
import { getLocaleTr, hasValue, useGetRouter } from "services/general/general";
|
||||
import { restBulkCreateRequest, restBulkUpdateRequest } from "services/queries/directus/billboard";
|
||||
import useTranslate from "services/translation/translation";
|
||||
|
||||
|
||||
interface PaymentMethodsProps {
|
||||
isOpen: boolean;
|
||||
shoppingCart: ShoppingCartItem[];
|
||||
stores: StoreProps[];
|
||||
ordersList: BillboardOrdersItem[];
|
||||
orderTotalAmount: string;
|
||||
currencies: Currencies[];
|
||||
user: UserData;
|
||||
currency: Currencies;
|
||||
onClose: (open: boolean) => void;
|
||||
onPaymentSuccess: (open: boolean) => void;
|
||||
onPaymentFail: (open: boolean) => void;
|
||||
}
|
||||
|
||||
const PaymentMethods = ({ isOpen = false, shoppingCart, stores, ordersList, currencies, orderTotalAmount, user, currency, onClose, onPaymentSuccess, onPaymentFail }: PaymentMethodsProps) => {
|
||||
|
||||
// states
|
||||
const [updating, setUpdating] = useState(false);
|
||||
const [alertOpen, setAlertOpen] = useState(false);
|
||||
const [selectedPaymentMethod, setSelectedPaymentMethod] = useState<number>(1); // payment method id
|
||||
|
||||
// variables
|
||||
const translate = useTranslate();
|
||||
const { locale, router } = useGetRouter();
|
||||
const dispatch = useAppDispatch();
|
||||
const methodIconClass = "inline-block size-6 fill-current me-4";
|
||||
const paymentMethods = [
|
||||
{ id: 1, title: translate("checkout-page-payment-method-card"), icon: <CreditCardSolid className={`${methodIconClass}`} /> },
|
||||
{ id: 2, title: translate("checkout-page-payment-method-wallet"), icon: <WalletSolid className={`${methodIconClass}`} /> },
|
||||
// { id: 3, title: "checkout-page-payment-method-pod", icon: "" },
|
||||
];
|
||||
|
||||
// orders process flow =>
|
||||
// - payment using card or wallet
|
||||
// - creation of payment receipt item
|
||||
// - creation of orders items
|
||||
// - updating payment receipt to add created order items to the receipt
|
||||
|
||||
// methods
|
||||
const clearCart = () => {
|
||||
dispatch(setShoppingCart([]));
|
||||
}
|
||||
const getShopTotal = (id: number) => {
|
||||
let shopTotal = 0;
|
||||
shopTotal = shoppingCart.filter(item => item.billboard.id === id).map(x => (x.discountedPrice ?? x.unitPrice) * x.quantity).reduceRight((total: number, x: number) => total + x);
|
||||
return Number(shopTotal.toFixed(2));
|
||||
}
|
||||
const getTranslations = (data: any) => {
|
||||
return [
|
||||
{
|
||||
languages_code: {
|
||||
code: "fa-IR"
|
||||
},
|
||||
note: hasValue(data.fa) ? data.fa : "",
|
||||
},
|
||||
{
|
||||
languages_code: {
|
||||
code: "en-US"
|
||||
},
|
||||
note: hasValue(data.en) ? data.en : "",
|
||||
}
|
||||
];
|
||||
}
|
||||
const payFromWallet = async () => {
|
||||
try {
|
||||
await restBulkUpdateRequest({
|
||||
collection: 'directus_users',
|
||||
updates: [
|
||||
{
|
||||
id: user.generalDetails.id,
|
||||
data: {
|
||||
wallet_balance: user.wallet.balance - Number(orderTotalAmount),
|
||||
},
|
||||
},
|
||||
],
|
||||
isAdmin: true,
|
||||
});
|
||||
|
||||
await createPaymentReceipt();
|
||||
} catch (error) {
|
||||
console.error("Update failed:", error);
|
||||
setUpdating(false);
|
||||
}
|
||||
}
|
||||
|
||||
// creating orders
|
||||
const createOrders = async (createdPaymentId: string) => {
|
||||
try {
|
||||
const result = await restBulkCreateRequest({
|
||||
collection: 'billboard_orders',
|
||||
items: stores.map(x => ({
|
||||
status: "published",
|
||||
order_type: "product",
|
||||
order_status: "confirmed",
|
||||
billboard_id: x.id,
|
||||
billboard_order_id: (ordersList.find(order => order.billboard_id === x.id)?.billboard_order_id ?? 0) + 1,
|
||||
user_id: user.generalDetails.id,
|
||||
payment_id: createdPaymentId,
|
||||
currency: currencies.filter(c => c.id === shoppingCart[0].currency)[0].id,
|
||||
products: shoppingCart.filter(item => item.billboard.id === x.id).map(item => ({
|
||||
billboard_products_id: {
|
||||
id: item.id
|
||||
}
|
||||
})),
|
||||
ordered_products: shoppingCart.filter(item => item.billboard.id === x.id).map((item, i) => ({
|
||||
id: i + 1,
|
||||
product_variation: item.variantId, // variation id
|
||||
product_id: item.product_id,
|
||||
product_sku_code: item.code,
|
||||
persian_title: item.title.fa,
|
||||
english_title: item.title.en,
|
||||
ordered_amount: item.quantity,
|
||||
ordered_size: item.size,
|
||||
original_price: item.unitPrice,
|
||||
final_price: item.unitPrice,
|
||||
discount_amount: item.discountedPrice,
|
||||
price_unit: item.unitName.id, // price unit id
|
||||
details: "",
|
||||
})),
|
||||
total_price: getShopTotal(x.id),
|
||||
translations: getTranslations({ fa: "", en: "" }),
|
||||
})),
|
||||
isAdmin: true
|
||||
});
|
||||
|
||||
const createdOrderItems: number[] = result.created.map((x: BillboardOrdersItem) => x.id); // i know, it's number not string
|
||||
|
||||
await updatePaymentReceipt(createdPaymentId, createdOrderItems);
|
||||
} catch (error) {
|
||||
console.error("updating product categories failed:", error);
|
||||
setUpdating(false);
|
||||
}
|
||||
}
|
||||
|
||||
// creating payment receipt if from wallet
|
||||
const createPaymentReceipt = async (stripePaymentId?: string) => {
|
||||
try {
|
||||
const results = await restBulkCreateRequest({
|
||||
collection: 'Payments',
|
||||
items: [{
|
||||
status: "published",
|
||||
receipt_type: "purchase",
|
||||
service_type: "purchase-order",
|
||||
payment_status: "succeeded",
|
||||
owner_user_id: user.generalDetails.id,
|
||||
stripe_customer_id: user.generalDetails.stripe_id,
|
||||
stripe_payment_id: stripePaymentId ?? null,
|
||||
payment_method: "wallet",
|
||||
order_amount: orderTotalAmount,
|
||||
payed_amount: orderTotalAmount,
|
||||
currency: currency.id,
|
||||
}],
|
||||
isAdmin: true
|
||||
});
|
||||
|
||||
const createdPaymentId: string = results.created[0].id;
|
||||
|
||||
await createOrders(createdPaymentId);
|
||||
} catch (error) {
|
||||
console.error("updating product categories failed:", error);
|
||||
setUpdating(false);
|
||||
}
|
||||
}
|
||||
|
||||
// update payment receipt to add order items, if paid by card
|
||||
const updatePaymentReceipt = async (createdPaymentId: string, createdOrderItems: number[]) => {
|
||||
try {
|
||||
await restBulkUpdateRequest({
|
||||
collection: 'Payments',
|
||||
updates: [
|
||||
{
|
||||
id: createdPaymentId,
|
||||
data: {
|
||||
billboard_orders: createdOrderItems.map(x => ({
|
||||
billboard_orders_id: {
|
||||
id: x
|
||||
}
|
||||
})),
|
||||
},
|
||||
},
|
||||
],
|
||||
isAdmin: true
|
||||
});
|
||||
|
||||
setUpdating(false);
|
||||
setAlertOpen(true);
|
||||
} catch (error) {
|
||||
console.error("updating product categories failed:", error);
|
||||
setUpdating(false);
|
||||
}
|
||||
}
|
||||
|
||||
const handlePayment = () => {
|
||||
if (selectedPaymentMethod === 1) handleCardPayment();
|
||||
if (selectedPaymentMethod === 2) handleWalletPayment();
|
||||
}
|
||||
const handleCardPayment = async () => {
|
||||
console.log("card payment");
|
||||
// await createPaymentReceipt(stripePaymentId);
|
||||
}
|
||||
const handleWalletPayment = () => {
|
||||
payFromWallet();
|
||||
}
|
||||
const handleClosePaymentModal = () => {
|
||||
onClose(false);
|
||||
}
|
||||
const onAlertClose = () => {
|
||||
if (alertOpen) {
|
||||
setAlertOpen(false);
|
||||
clearCart();
|
||||
alertOpen && router.reload();
|
||||
}
|
||||
}
|
||||
|
||||
return <>
|
||||
<EventAlert
|
||||
text={translate("checkout-page-payment-success-text")}
|
||||
open={alertOpen}
|
||||
type="success"
|
||||
timeOut={7000}
|
||||
onClose={onAlertClose}
|
||||
hasTime
|
||||
/>
|
||||
<Modal
|
||||
header={true}
|
||||
wrapperId={`checkout-page-select-payment-method`}
|
||||
open={isOpen}
|
||||
onClose={handleClosePaymentModal}
|
||||
title={translate("checkout-page-select-payment-method")}
|
||||
className="flex flex-col bg-white w-[90vw] h-auto max-h-[80vh] lg:h-auto lg:w-[500px] lg:max-w-[90vw] lg:max-h-[90vh] rounded"
|
||||
childrenClass="hide-scrollbar p-4 !flex flex-col items-center"
|
||||
headerClass="!h-14"
|
||||
titleClass="!text-base"
|
||||
closeClass="!size-5"
|
||||
>
|
||||
<img src="/pics/payment-pic.svg" alt="payment method selection" className="block w-72 mx-auto mb-4" />
|
||||
<span className="block text-center text-2xl font-semibold text-secondary-light pb-3">{translate("checkout-page-select-payment-method")}</span>
|
||||
<div className="block w-full my-4">
|
||||
{paymentMethods.map(x => (
|
||||
<RadioButton
|
||||
key={x.id}
|
||||
forId={x.id + x.title}
|
||||
name={x.title}
|
||||
title={
|
||||
<div>
|
||||
{x.icon}
|
||||
<span className="text-sm text-current">{x.title}</span>
|
||||
</div>
|
||||
}
|
||||
value={x.title}
|
||||
checked={selectedPaymentMethod === x.id}
|
||||
onChange={() => setSelectedPaymentMethod(x.id)}
|
||||
labelClass="market-radio-label justify-center px-4 py-4 border-b border-gray-100 last:border-none rounded-xl hover:bg-market-input"
|
||||
inputClass="market-radio-input right-4"
|
||||
titleClass="market-radio-title text-secondary-light p-0"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<div className="inline-flex items-center py-2 px-4 rounded-lg bg-success-bg text-success-text border-2 border-white ring-4 ring-success-bg mt-4 mb-6 select-none">
|
||||
<span className="text-current font-semibold">{`${translate("checkout-page-payment-method-order-total")} :`}</span>
|
||||
<span className="text-current ps-2 font-semibold">{`${orderTotalAmount} ${getLocaleTr(currency, locale).name}`}</span>
|
||||
</div>
|
||||
<div className="flex flex-col sm:flex-row w-full items-center max-sm:space-y-3 sm:space-x-4 rtl:space-x-reverse pt-6">
|
||||
<Button
|
||||
type="button"
|
||||
text={translate("shopping-cart-complete-purchase")}
|
||||
className="w-full py-2 sm:py-3 px-gi rounded-lg text-xs sm:text-sm font-semibold shadow-none bg-market-title-light text-white capitalize"
|
||||
leftIcon={<CreditCardSolid className="inline-block size-6 fill-current rtl:ml-4 ltr:mr-4" />}
|
||||
onClick={handlePayment}
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
text={translate("checkout-page-edit-order")}
|
||||
className="w-full py-2 sm:py-3 px-gi rounded-lg text-xs sm:text-sm font-semibold shadow-none ring-2 ring-market-title-light text-market-title-light capitalize"
|
||||
rightIcon={<CartSolid className="inline-block size-5 fill-current rtl:mr-4 ltr:ml-4" />}
|
||||
onClick={handleClosePaymentModal}
|
||||
/>
|
||||
</div>
|
||||
</Modal>
|
||||
</>
|
||||
}
|
||||
export default PaymentMethods;
|
||||
@ -70,8 +70,8 @@ const Layout = ({ children, fullScreen = false, bgColor = "#fff", trData, header
|
||||
([query, route]) => fetchPrivateData(route, query)
|
||||
);
|
||||
|
||||
const { data: globalData, error: globalDataError } = useCachedGlobalData();
|
||||
// const { data: globalData, error: globalDataError } = process.env.NODE_ENV === 'production' ? useCachedGlobalData() : useSWRImmutable([globalDataQuery, ""], ([query, route]) => fetchPublicData(route, query));
|
||||
// const { data: globalData, error: globalDataError } = useCachedGlobalData();
|
||||
const { data: globalData, error: globalDataError } = process.env.NODE_ENV === 'production' ? useCachedGlobalData() : useSWRImmutable([globalDataQuery, ""], ([query, route]) => fetchPublicData(route, query));
|
||||
|
||||
// methods
|
||||
const setGlobalData = () => {
|
||||
|
||||
@ -755,6 +755,7 @@ export type BillboardOrdersItem = {
|
||||
id: string;
|
||||
status: "draft" | "published" | "archived";
|
||||
date_created: string;
|
||||
date_updated: string;
|
||||
order_type: "product" | "service";
|
||||
order_status: "pending" | "confirmed" | "processing" | "on-hold" | "shipped" | "delivered" | "completed" | "canceled" | "failed" | "returned" | "refunded";
|
||||
billboard_id: number;
|
||||
@ -764,6 +765,8 @@ export type BillboardOrdersItem = {
|
||||
currency: Currencies;
|
||||
products: {
|
||||
billboard_products_id: BillboardProduct;
|
||||
order_status: "pending" | "confirmed" | "processing" | "on-hold" | "shipped" | "delivered" | "completed" | "canceled" | "failed" | "returned" | "refunded";
|
||||
delivery_tracking_code: string;
|
||||
}[];
|
||||
reservation_id: number;
|
||||
ordered_products: {
|
||||
|
||||
@ -283,3 +283,5 @@ export type DirectusPhoto = {
|
||||
type: string;
|
||||
filesize: number,
|
||||
}
|
||||
|
||||
export type CurrenciesAbbr = "usd" | "eur" | "cad" | "irr" | "jpy" | "tm";
|
||||
|
||||
@ -1,18 +1,60 @@
|
||||
import { BillboardOrdersItem } from "./billboard";
|
||||
import { Currencies } from "./general";
|
||||
|
||||
export type PaymentData = {
|
||||
// general
|
||||
id: string;
|
||||
status: "draft" | "published" | "archived";
|
||||
date_created: string;
|
||||
receipt_type: "flierland-ad" | "flierland-billboard" | "flierland-wallet-top-up" | "purchase-order" | "service-order" | "refund" | "withdrawal";
|
||||
payment_status: "canceled" | "processing" | "requires_action" | "requires_capture" | "requires_confirmation" | "requires_payment_method" | "succeeded";
|
||||
stripe_payment_id?: string | any;
|
||||
stripe_customer_id: string | any;
|
||||
owner_user_id: string;
|
||||
stripe_customer_id: string | any;
|
||||
stripe_payment_id: string | any;
|
||||
payment_method: "wallet" | "payment_gateway" | "cash";
|
||||
billboard_orders: {
|
||||
billboard_orders_id: BillboardOrdersItem;
|
||||
}[];
|
||||
// refund details
|
||||
receipt_id: string;
|
||||
refunded_amount: string;
|
||||
refund_format: "store-credit" | "wallet" | "card" | "cash";
|
||||
shipping_fee_included: boolean;
|
||||
refund_reason: RefundReasons;
|
||||
product_id: string;
|
||||
// subscription
|
||||
service_id: string;
|
||||
service_duration: number;
|
||||
service_duration_term: "day" | "month" | "year";
|
||||
// price details
|
||||
order_amount: string;
|
||||
payed_amount: string;
|
||||
currency: string | null;
|
||||
payment_method: "wallet" | "payment_gateway";
|
||||
discount_used: "yes" | "no";
|
||||
discount_code?: string;
|
||||
service_type?: "flierland-ad" | "flierland-billboard" | any;
|
||||
service_id?: string;
|
||||
service_duration?: number;
|
||||
service_duration_term?: string;
|
||||
discount_used: boolean;
|
||||
discount_code: string;
|
||||
currency: Currencies;
|
||||
}
|
||||
|
||||
export type RefundReasons = {
|
||||
reason:
|
||||
"defected-product" |
|
||||
"wrong-item-received" |
|
||||
"item-arrived-late" |
|
||||
"product-not-as-described" |
|
||||
"changed-mind" |
|
||||
"found-better-price-elsewhere" |
|
||||
"size-or-fit-issue" |
|
||||
"allergic-reaction-or-incompatibility" |
|
||||
"missing-parts-or-accessories" |
|
||||
"damaged-during-shipping" |
|
||||
"gift-return" |
|
||||
"subscription-cancellation" |
|
||||
"unauthorized-purchase";
|
||||
translations: {
|
||||
languages_code: {
|
||||
code: string;
|
||||
}
|
||||
title: string;
|
||||
description: string;
|
||||
}[];
|
||||
}
|
||||
|
||||
|
||||
@ -23,6 +23,7 @@ export type UserProps = {
|
||||
last_name: string,
|
||||
email: string,
|
||||
password: string,
|
||||
title: string,
|
||||
description: string,
|
||||
city: {
|
||||
English_name: string,
|
||||
@ -49,5 +50,14 @@ export type UserProps = {
|
||||
filesize: number,
|
||||
}
|
||||
liked_news: string[];
|
||||
}
|
||||
|
||||
wallet_balance: string;
|
||||
stripe_customer_id: string;
|
||||
communication_language: string;
|
||||
policies: string | string[];
|
||||
appearance: string,
|
||||
theme_dark: string,
|
||||
theme_light: string,
|
||||
theme_light_overrides: string,
|
||||
theme_dark_overrides: string,
|
||||
tgs: any;
|
||||
}
|
||||
74
src/common/zod-schemas/create-payment.ts
Normal file
74
src/common/zod-schemas/create-payment.ts
Normal file
@ -0,0 +1,74 @@
|
||||
import { z } from "zod";
|
||||
|
||||
// Shared fields across all payment types
|
||||
const BasePaymentDataSchema = z.object({
|
||||
status: z.enum(["draft", "published", "archived"]),
|
||||
payment_status: z.enum([
|
||||
"canceled",
|
||||
"processing",
|
||||
"requires_action",
|
||||
"requires_capture",
|
||||
"requires_confirmation",
|
||||
"requires_payment_method",
|
||||
"succeeded",
|
||||
]),
|
||||
owner_user_id: z.string(),
|
||||
stripe_customer_id: z.string().or(z.any()),
|
||||
stripe_payment_id: z.string().optional(),
|
||||
payment_method: z.enum(["wallet", "payment_gateway", "cash"]),
|
||||
order_amount: z.string(),
|
||||
payed_amount: z.string(),
|
||||
discount_used: z.boolean(),
|
||||
discount_code: z.string().optional(),
|
||||
currency: z.any(), // Replace with your Currencies enum/schema if needed
|
||||
});
|
||||
|
||||
const AdPaymentSchema = BasePaymentDataSchema.extend({
|
||||
receipt_type: z.literal("flierland-ad"),
|
||||
service_id: z.string(),
|
||||
service_duration: z.number(),
|
||||
service_duration_term: z.enum(["day", "month", "year"]),
|
||||
});
|
||||
const BillboardPaymentSchema = BasePaymentDataSchema.extend({
|
||||
receipt_type: z.literal("flierland-billboard"),
|
||||
service_id: z.string(),
|
||||
service_duration: z.number(),
|
||||
service_duration_term: z.enum(["day", "month", "year"]),
|
||||
});
|
||||
const WalletTopUpPaymentSchema = BasePaymentDataSchema.extend({
|
||||
receipt_type: z.literal("flierland-wallet-top-up"),
|
||||
// Add wallet-specific fields here
|
||||
});
|
||||
const PurchaseOrderPaymentSchema = BasePaymentDataSchema.extend({
|
||||
receipt_type: z.literal("purchase-order"),
|
||||
// Add purchase-order-specific fields here
|
||||
});
|
||||
const ServiceOrderPaymentSchema = BasePaymentDataSchema.extend({
|
||||
receipt_type: z.literal("service-order"),
|
||||
// Add service-order-specific fields here
|
||||
});
|
||||
const RefundPaymentSchema = BasePaymentDataSchema.extend({
|
||||
receipt_type: z.literal("refund"),
|
||||
receipt_id: z.string(),
|
||||
refunded_amount: z.string(),
|
||||
refund_format: z.enum(["store-credit", "wallet", "card", "cash"]),
|
||||
shipping_fee_included: z.boolean(),
|
||||
refund_reason: z.any(), // Replace with RefundReasons enum/schema
|
||||
product_id: z.string(),
|
||||
});
|
||||
const WithdrawalPaymentSchema = BasePaymentDataSchema.extend({
|
||||
receipt_type: z.literal("withdrawal"),
|
||||
// Add withdrawal-specific fields here
|
||||
});
|
||||
|
||||
export const PaymentDataSchema = z.discriminatedUnion("receipt_type", [
|
||||
AdPaymentSchema,
|
||||
BillboardPaymentSchema,
|
||||
WalletTopUpPaymentSchema,
|
||||
PurchaseOrderPaymentSchema,
|
||||
ServiceOrderPaymentSchema,
|
||||
RefundPaymentSchema,
|
||||
WithdrawalPaymentSchema,
|
||||
]);
|
||||
|
||||
export type PaymentData = z.infer<typeof PaymentDataSchema>;
|
||||
15
src/common/zod-schemas/user-register.ts
Normal file
15
src/common/zod-schemas/user-register.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const UserRegisterSchema = z.object({
|
||||
first_name: z.string(),
|
||||
last_name: z.string(),
|
||||
city_id: z.string(),
|
||||
communication_language: z.enum([
|
||||
"fa-IR",
|
||||
"en-US",
|
||||
]),
|
||||
email: z.string().email(),
|
||||
password: z.string(),
|
||||
});
|
||||
|
||||
export type UserRegister = z.infer<typeof UserRegisterSchema>;
|
||||
36
src/lib/directus/bulk-create-logic.ts
Normal file
36
src/lib/directus/bulk-create-logic.ts
Normal file
@ -0,0 +1,36 @@
|
||||
import { safeJson } from "lib/general/safe-response-json";
|
||||
import { getDirectusEndPoint } from "services/directus/endpoint-finder";
|
||||
import { cmsAddress } from "services/general/general";
|
||||
|
||||
interface bulkCreateDirectusProps {
|
||||
collection: string;
|
||||
items: Record<string, any>[];
|
||||
token: string;
|
||||
}
|
||||
|
||||
export const bulkCreateDirectus = async ({ collection, items, token }: bulkCreateDirectusProps): Promise<{ data: any[] | undefined; error: string | undefined }> => {
|
||||
const results = [];
|
||||
|
||||
for (const item of items) {
|
||||
if (!item || Object.keys(item).length === 0) continue;
|
||||
|
||||
const response = await fetch(`${cmsAddress()}${getDirectusEndPoint({ collection: collection, type: "create" })}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
body: JSON.stringify(item),
|
||||
});
|
||||
|
||||
const { data, error } = await safeJson(response);
|
||||
|
||||
if (!response.ok || error) {
|
||||
return { data: undefined, error: error || response.statusText };
|
||||
}
|
||||
|
||||
results.push(data ?? null);
|
||||
}
|
||||
|
||||
return { data: results, error: undefined };
|
||||
};
|
||||
36
src/lib/directus/bulk-delete-logic.ts
Normal file
36
src/lib/directus/bulk-delete-logic.ts
Normal file
@ -0,0 +1,36 @@
|
||||
import { safeJson } from "lib/general/safe-response-json";
|
||||
import { getDirectusEndPoint } from "services/directus/endpoint-finder";
|
||||
import { cmsAddress } from "services/general/general";
|
||||
|
||||
interface bulkDeleteDirectusProps {
|
||||
collection: string;
|
||||
items: any[];
|
||||
token: string;
|
||||
}
|
||||
|
||||
export const bulkDeleteDirectus = async ({ collection, items, token }: bulkDeleteDirectusProps) => {
|
||||
const results = [];
|
||||
|
||||
for (const item of items) {
|
||||
const id = item;
|
||||
|
||||
if (!id) continue;
|
||||
|
||||
const response = await fetch(`${cmsAddress()}${getDirectusEndPoint({ collection: collection, type: "delete", id: id })}`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
}
|
||||
});
|
||||
|
||||
const { data, error } = await safeJson(response);
|
||||
|
||||
if (!response.ok || error) {
|
||||
return { data: undefined, error: error || response.statusText };
|
||||
}
|
||||
|
||||
results.push(data ?? null);
|
||||
}
|
||||
|
||||
return { data: results, error: undefined };
|
||||
}
|
||||
41
src/lib/directus/bulk-update-logic.ts
Normal file
41
src/lib/directus/bulk-update-logic.ts
Normal file
@ -0,0 +1,41 @@
|
||||
import { safeJson } from "lib/general/safe-response-json";
|
||||
import { getDirectusEndPoint } from "services/directus/endpoint-finder";
|
||||
import { cmsAddress } from "services/general/general";
|
||||
|
||||
interface bulkUpdateDirectusProps {
|
||||
collection: string;
|
||||
items: {
|
||||
id: string;
|
||||
data: Record<string, any>
|
||||
}[];
|
||||
token: string;
|
||||
}
|
||||
|
||||
export const bulkUpdateDirectus = async ({ collection, items, token }: bulkUpdateDirectusProps) => {
|
||||
const results = [];
|
||||
|
||||
for (const item of items) {
|
||||
const { id, data } = item;
|
||||
|
||||
if (!id || !data || Object.keys(data).length === 0) continue;
|
||||
|
||||
const response = await fetch(`${cmsAddress()}${getDirectusEndPoint({ collection: collection, type: "edit", id: id })}`, {
|
||||
method: 'PATCH',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
|
||||
const { data: resData, error } = await safeJson(response);
|
||||
|
||||
if (!response.ok || error) {
|
||||
return { data: undefined, error: error || response.statusText };
|
||||
}
|
||||
|
||||
results.push(resData ?? null);
|
||||
}
|
||||
|
||||
return { data: results, error: undefined };
|
||||
}
|
||||
76
src/lib/general/create-payment-receipt.ts
Normal file
76
src/lib/general/create-payment-receipt.ts
Normal file
@ -0,0 +1,76 @@
|
||||
import { safeJson } from "lib/general/safe-response-json";
|
||||
import { basePath } from "services/general/general";
|
||||
|
||||
interface NecessaryData {
|
||||
status: "draft" | "published" | "archived";
|
||||
receipt_type: "flierland-ad" | "flierland-billboard" | "flierland-wallet-top-up" | "purchase-order" | "service-order" | "refund" | "withdrawal";
|
||||
owner_user_id: string;
|
||||
stripe_customer_id: string | any;
|
||||
stripe_payment_id?: string | any;
|
||||
payment_method: "wallet" | "payment_gateway" | "cash";
|
||||
payment_status: "canceled" | "processing" | "requires_action" | "requires_capture" | "requires_confirmation" | "requires_payment_method" | "succeeded";
|
||||
order_amount: string;
|
||||
payed_amount: string;
|
||||
discount_used: boolean;
|
||||
discount_code?: string;
|
||||
currency: string;
|
||||
}
|
||||
interface AdData {
|
||||
service_id: string;
|
||||
service_duration: number;
|
||||
service_duration_term: "day" | "month" | "year";
|
||||
}
|
||||
interface BillboardData {
|
||||
service_id: string;
|
||||
service_duration: number;
|
||||
service_duration_term: "day" | "month" | "year";
|
||||
}
|
||||
interface WalletTopUpData {
|
||||
|
||||
}
|
||||
interface PurchaseOrderData {
|
||||
|
||||
}
|
||||
interface ServiceOrderData {
|
||||
|
||||
}
|
||||
interface RefundData {
|
||||
receipt_id: string;
|
||||
refunded_amount: string;
|
||||
refund_format: "store-credit" | "wallet" | "card" | "cash";
|
||||
shipping_fee_included: boolean;
|
||||
refund_reason: string;
|
||||
product_id: string;
|
||||
}
|
||||
interface WithdrawalData {
|
||||
|
||||
}
|
||||
type ReceiptData =
|
||||
| (NecessaryData & { receipt_type: "flierland-ad" } & AdData)
|
||||
| (NecessaryData & { receipt_type: "flierland-billboard" } & BillboardData)
|
||||
| (NecessaryData & { receipt_type: "flierland-wallet-top-up" } & WalletTopUpData)
|
||||
| (NecessaryData & { receipt_type: "purchase-order" } & PurchaseOrderData)
|
||||
| (NecessaryData & { receipt_type: "service-order" } & ServiceOrderData)
|
||||
| (NecessaryData & { receipt_type: "refund" } & RefundData)
|
||||
| (NecessaryData & { receipt_type: "withdrawal" } & WithdrawalData);
|
||||
|
||||
export const CreatePaymentReceipt = async (props: ReceiptData) => {
|
||||
|
||||
const response = await fetch(`${basePath()}/api/general/payments/create-payment`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Access-Control-Allow-Origin": "*",
|
||||
"Content-Type": "application/json",
|
||||
"x-system-secret": `${process.env.SYSTEM_API_SECRET}`
|
||||
},
|
||||
body: JSON.stringify(props),
|
||||
});
|
||||
|
||||
const data = await safeJson(response);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data?.error || "Creating payment receipt failed");
|
||||
}
|
||||
|
||||
return data ?? null;
|
||||
};
|
||||
47
src/lib/general/safe-response-json.ts
Normal file
47
src/lib/general/safe-response-json.ts
Normal file
@ -0,0 +1,47 @@
|
||||
/**
|
||||
* Safely parse a fetch Response body as JSON, returning data or error details.
|
||||
* Handles empty bodies, invalid JSON, Zod errors, and server error messages.
|
||||
* @param response The fetch Response to parse
|
||||
* @returns An object with parsed data (if any) and error details (if any)
|
||||
*/
|
||||
export const safeJson = async <T = any>(
|
||||
response: Response
|
||||
): Promise<{ data: T | undefined; error: string | undefined }> => {
|
||||
try {
|
||||
const text = await response.text();
|
||||
if (!text) {
|
||||
return { data: undefined, error: undefined }; // Empty body
|
||||
}
|
||||
|
||||
const parsed = JSON.parse(text) as T;
|
||||
|
||||
// Check for server/Zod error structure (e.g., { error: string, details: any })
|
||||
// or Directus error structure (e.g., { errors: [{ message: string, extensions: any }] })
|
||||
if (!response.ok && parsed && typeof parsed === 'object') {
|
||||
const parsedObj = parsed as Record<string, any>;
|
||||
// Handle standard error structure
|
||||
if ('error' in parsedObj || 'details' in parsedObj) {
|
||||
const errorMsg = parsedObj.error || 'Request failed';
|
||||
const errorDetails = parsedObj.details ? JSON.stringify(parsedObj.details) : undefined;
|
||||
return { data: undefined, error: errorDetails ? `${errorMsg}: ${errorDetails}` : errorMsg };
|
||||
}
|
||||
// Handle Directus error structure
|
||||
if ('errors' in parsedObj && Array.isArray(parsedObj.errors)) {
|
||||
const errorMsg = parsedObj.errors
|
||||
.map((err: any) => err.message || 'Unknown error')
|
||||
.join('; ') || 'Request failed';
|
||||
return { data: undefined, error: errorMsg };
|
||||
}
|
||||
}
|
||||
|
||||
return { data: parsed, error: undefined }; // Valid JSON, no error
|
||||
} catch (err) {
|
||||
// Non-JSON or parse failure
|
||||
console.warn("safeJson parse error:", err);
|
||||
let errorMsg = 'Unknown error';
|
||||
if (err instanceof Error) {
|
||||
errorMsg = err.message;
|
||||
}
|
||||
return { data: undefined, error: `Invalid response: ${errorMsg}` };
|
||||
}
|
||||
};
|
||||
13
src/lib/server/validate-system-secret.ts
Normal file
13
src/lib/server/validate-system-secret.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import { NextApiRequest, NextApiResponse } from 'next';
|
||||
|
||||
// Helper function to validate the x-system-secret header
|
||||
export function validateSystemSecret(req: NextApiRequest, res: NextApiResponse): boolean {
|
||||
const secret = req.headers['x-system-secret'] as string | undefined;
|
||||
|
||||
if (!secret || secret !== process.env.SYSTEM_API_SECRET) {
|
||||
res.status(403).json({ error: 'Forbidden: Invalid system secret' });
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
44
src/lib/stripe/create-session-helper.ts
Normal file
44
src/lib/stripe/create-session-helper.ts
Normal file
@ -0,0 +1,44 @@
|
||||
import Stripe from "stripe";
|
||||
import { getStripeSecretKey } from "services/general/stripe";
|
||||
import { basePath } from "services/general/general";
|
||||
|
||||
const stripe = new Stripe(getStripeSecretKey(), {
|
||||
apiVersion: "2025-08-27.basil",
|
||||
});
|
||||
|
||||
export interface CheckoutSessionParams {
|
||||
mode: Stripe.Checkout.SessionCreateParams.Mode;
|
||||
payment_method_types: Stripe.Checkout.SessionCreateParams.PaymentMethodType[];
|
||||
line_items: Stripe.Checkout.SessionCreateParams.LineItem[];
|
||||
metadata?: Record<string, any>;
|
||||
success_url?: string;
|
||||
cancel_url?: string;
|
||||
}
|
||||
|
||||
export const createCheckoutSession = async (params: CheckoutSessionParams) => {
|
||||
// Create the Stripe checkout session with a success_url that includes session_id
|
||||
const session = await stripe.checkout.sessions.create({
|
||||
...params,
|
||||
success_url: `${params.success_url}?success=true`,
|
||||
cancel_url: params.cancel_url ?? `${basePath()}/dashboard/payment-result?canceled=true`,
|
||||
});
|
||||
|
||||
return {
|
||||
...session,
|
||||
sessionUrl: session.url, // Return the Stripe checkout URL for the client
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Example helper: extract amount paid from Checkout session
|
||||
*/
|
||||
export const extractAmountFromSession = async (session: Stripe.Checkout.Session): Promise<number> => {
|
||||
// Fetch line_items to read exact amount
|
||||
const stripe = new Stripe(getStripeSecretKey(), {
|
||||
apiVersion: "2025-08-27.basil",
|
||||
});
|
||||
|
||||
const lineItems = await stripe.checkout.sessions.listLineItems(session.id, { limit: 1 });
|
||||
const amount = lineItems.data[0]?.amount_total ?? 0;
|
||||
return Math.floor(amount / 100); // convert from cents
|
||||
}
|
||||
40
src/lib/stripe/create-stripe-payment-session.ts
Normal file
40
src/lib/stripe/create-stripe-payment-session.ts
Normal file
@ -0,0 +1,40 @@
|
||||
import { safeJson } from "lib/general/safe-response-json";
|
||||
import { basePath } from "services/general/general";
|
||||
import Stripe from "stripe";
|
||||
|
||||
interface CreateStripePaymentSessionProps {
|
||||
mode: Stripe.Checkout.SessionCreateParams.Mode;
|
||||
payment_method_types: Stripe.Checkout.SessionCreateParams.PaymentMethodType[];
|
||||
line_items: Stripe.Checkout.SessionCreateParams.LineItem[];
|
||||
metadata?: Record<string, any>;
|
||||
customer: string;
|
||||
success_url?: string;
|
||||
cancel_url?: string;
|
||||
}
|
||||
|
||||
export const createStripePaymentSession = async ({ mode, payment_method_types, line_items, metadata, customer, success_url, cancel_url }: CreateStripePaymentSessionProps) => {
|
||||
|
||||
const response = await fetch(`${basePath()}/api/stripe/create-checkout-session`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
mode: mode,
|
||||
payment_method_types: payment_method_types,
|
||||
line_items: line_items,
|
||||
metadata: metadata,
|
||||
customer: customer,
|
||||
success_url: success_url,
|
||||
cancel_url: cancel_url,
|
||||
}),
|
||||
})
|
||||
|
||||
const { data, error } = await safeJson(response);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(error || "Creating stripe payment session failed");
|
||||
}
|
||||
|
||||
return data ?? null;
|
||||
};
|
||||
43
src/lib/stripe/payment-status/ad-payment.ts
Normal file
43
src/lib/stripe/payment-status/ad-payment.ts
Normal file
@ -0,0 +1,43 @@
|
||||
import { adminAccessToken, cmsAddress } from "services/general/general";
|
||||
|
||||
export const handleAdPaymentStatus = async (resourceId: string, userId: string): Promise<boolean> => {
|
||||
const query = `
|
||||
query GetTheRequestedAd {
|
||||
Ads(
|
||||
filter: {
|
||||
id: { _eq: "${resourceId}" },
|
||||
user_created: { id: { _eq: "${userId}" } },
|
||||
paid: { _eq: true }
|
||||
}
|
||||
) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
try {
|
||||
const res = await fetch(`${cmsAddress()}/graphql`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${adminAccessToken}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
query
|
||||
}),
|
||||
});
|
||||
|
||||
const json = await res.json();
|
||||
|
||||
if (json.errors) {
|
||||
console.error("GraphQL errors:", json.errors);
|
||||
return false;
|
||||
}
|
||||
|
||||
const adExists = json.data?.Ads?.length > 0;
|
||||
return !!adExists;
|
||||
} catch (err) {
|
||||
console.error("handleAdPaymentStatus error:", err);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
4
src/lib/stripe/payment-status/billboard-payment.ts
Normal file
4
src/lib/stripe/payment-status/billboard-payment.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export const handleBillboardPaymentStatus = async (resourceId: string, userId: string) => {
|
||||
|
||||
return true;
|
||||
}
|
||||
62
src/lib/stripe/payment-status/check-stripe-payment-status.ts
Normal file
62
src/lib/stripe/payment-status/check-stripe-payment-status.ts
Normal file
@ -0,0 +1,62 @@
|
||||
import { safeJson } from "lib/general/safe-response-json";
|
||||
|
||||
interface CheckStripePaymentStatusProps {
|
||||
stripePaymentId: string;
|
||||
resourceType:
|
||||
| "flierland-ad"
|
||||
| "flierland-billboard"
|
||||
| "flierland-wallet-top-up"
|
||||
| "purchase-order"
|
||||
| "service-order"
|
||||
| "refund"
|
||||
| "withdrawal";
|
||||
resourceId: string;
|
||||
interval?: number; // seconds between checks
|
||||
timeout?: number; // max seconds before giving up
|
||||
}
|
||||
|
||||
/**
|
||||
* Polls the backend until either:
|
||||
* - payment is confirmed ✅ → resolves true
|
||||
* - timeout expires ⏱️ → resolves false
|
||||
* - request fails ❌ → rejects
|
||||
*/
|
||||
export async function checkStripePaymentStatus({
|
||||
stripePaymentId,
|
||||
resourceType,
|
||||
resourceId,
|
||||
interval = 3,
|
||||
timeout = 60,
|
||||
}: CheckStripePaymentStatusProps): Promise<boolean> {
|
||||
const start = Date.now();
|
||||
|
||||
while (true) {
|
||||
try {
|
||||
const response = await fetch(`/api/general/payments/stripe-payment-status`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ stripePaymentId, resourceType, resourceId }),
|
||||
});
|
||||
|
||||
const { data, error } = await safeJson(response);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(error || "Checking server's payment status failed");
|
||||
}
|
||||
|
||||
if (data.data === true) {
|
||||
return true; // ✅ confirmed
|
||||
}
|
||||
} catch (err: any) {
|
||||
throw new Error(err.message || "Unknown error while checking payment status");
|
||||
}
|
||||
|
||||
// timeout reached?
|
||||
if ((Date.now() - start) / 1000 >= timeout) {
|
||||
return false; // ⏱️ not confirmed in time
|
||||
}
|
||||
|
||||
// wait for next interval
|
||||
await new Promise((resolve) => setTimeout(resolve, interval * 1000));
|
||||
}
|
||||
}
|
||||
4
src/lib/stripe/payment-status/purchase-order.ts
Normal file
4
src/lib/stripe/payment-status/purchase-order.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export const handlePurchaseOrderPaymentStatus = async (resourceId: string, userId: string) => {
|
||||
|
||||
return true;
|
||||
}
|
||||
4
src/lib/stripe/payment-status/refund.ts
Normal file
4
src/lib/stripe/payment-status/refund.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export const handleRefundPaymentStatus = async (resourceId: string, userId: string) => {
|
||||
|
||||
return true;
|
||||
}
|
||||
4
src/lib/stripe/payment-status/service-order.ts
Normal file
4
src/lib/stripe/payment-status/service-order.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export const handleServiceOrderPaymentStatus = async (resourceId: string, userId: string) => {
|
||||
|
||||
return true;
|
||||
}
|
||||
42
src/lib/stripe/payment-status/wallet-top-up.ts
Normal file
42
src/lib/stripe/payment-status/wallet-top-up.ts
Normal file
@ -0,0 +1,42 @@
|
||||
import { adminAccessToken, cmsAddress } from "services/general/general";
|
||||
|
||||
export const handleWalletTopUpPaymentStatus = async (stripePaymentId: string, userId: string): Promise<boolean> => {
|
||||
const query = `
|
||||
query GetTheRequestedAd {
|
||||
Payments(
|
||||
filter: {
|
||||
status: { _eq: "published" },
|
||||
stripe_payment_id: { _eq: "${stripePaymentId}" },
|
||||
}
|
||||
) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
try {
|
||||
const res = await fetch(`${cmsAddress()}/graphql`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${adminAccessToken}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
query
|
||||
}),
|
||||
});
|
||||
|
||||
const json = await res.json();
|
||||
|
||||
if (json.errors) {
|
||||
console.error("GraphQL errors:", json.errors);
|
||||
return false;
|
||||
}
|
||||
|
||||
const paymentReceiptExists = json.data?.Payments?.length > 0;
|
||||
return !!paymentReceiptExists;
|
||||
} catch (err) {
|
||||
console.error("handleAdPaymentStatus error:", err);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
4
src/lib/stripe/payment-status/withdrawal.ts
Normal file
4
src/lib/stripe/payment-status/withdrawal.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export const handleWithdrawalPaymentStatus = async (resourceId: string, userId: string) => {
|
||||
|
||||
return true;
|
||||
}
|
||||
@ -0,0 +1,4 @@
|
||||
|
||||
export async function activateBillboardSubscription(metaData: any) {
|
||||
// TODO: Store subscriptionId in DB, set user subscription active
|
||||
}
|
||||
@ -0,0 +1,47 @@
|
||||
import Stripe from "stripe";
|
||||
import { increaseWalletBalance } from "./wallet-top-up";
|
||||
import { activateMarketAd } from "./market-ad";
|
||||
import { activateBillboardSubscription } from "./billboard-subscription";
|
||||
import { storePurchaseOrderPaid } from "./store-purchase-order";
|
||||
|
||||
// Handles checkout.session.completed event
|
||||
export const handleCheckoutSessionCompleted = async (session: Stripe.Checkout.Session, secret: string) => {
|
||||
// Metadata is where we stored our business-specific info
|
||||
const metaData = session.metadata || {};
|
||||
|
||||
console.log("✅ Checkout session completed:", {
|
||||
id: session.id,
|
||||
});
|
||||
|
||||
try {
|
||||
switch (metaData.receipt_type) {
|
||||
case "flierland-ad":
|
||||
// Example: Activate a market ad in DB
|
||||
await activateMarketAd(session, secret, metaData);
|
||||
break;
|
||||
|
||||
case "flierland-wallet-top-up":
|
||||
// Amount can be read from line_items or stored in metadata
|
||||
await increaseWalletBalance(session, secret, metaData);
|
||||
break;
|
||||
|
||||
case "flierland-billboard":
|
||||
// Stripe creates a Subscription object linked to this checkout session
|
||||
if (session.subscription) {
|
||||
await activateBillboardSubscription(metaData);
|
||||
}
|
||||
break;
|
||||
|
||||
case "purchase-order":
|
||||
// Mark a customer order as paid
|
||||
await storePurchaseOrderPaid(metaData);
|
||||
break;
|
||||
|
||||
default:
|
||||
console.warn("⚠️ Unknown receipt_type in metadata:", metaData.receipt_type);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("❌ Error handling checkout.session.completed:", err);
|
||||
throw err; // Let webhook.ts return 500
|
||||
}
|
||||
}
|
||||
145
src/lib/stripe/webhooks/checkout-session-completed/market-ad.ts
Normal file
145
src/lib/stripe/webhooks/checkout-session-completed/market-ad.ts
Normal file
@ -0,0 +1,145 @@
|
||||
import { CreatePaymentReceipt } from "lib/general/create-payment-receipt";
|
||||
import { safeJson } from "lib/general/safe-response-json";
|
||||
import { extractAmountFromSession } from "lib/stripe/create-session-helper";
|
||||
import { getCurrencyId } from "services/directus/general";
|
||||
import { adminAccessToken, cmsAddress } from "services/general/general";
|
||||
import { restBulkUpdateRequest } from "services/queries/directus/billboard";
|
||||
import Stripe from "stripe";
|
||||
|
||||
export async function activateMarketAd(session: Stripe.Checkout.Session, secret: string, metaData: any) {
|
||||
const adPrice = 20; // Static, server-only—safe for comparison
|
||||
const paidAmount = await extractAmountFromSession(session); // From verified Stripe session
|
||||
const adId = metaData.adId;
|
||||
const userFlierlandId = metaData.user_flierland_id; // UUID string—no parseInt
|
||||
const stripeCustomerId = metaData.stripe_customer_id;
|
||||
const stripePaymentId = session.payment_intent as string;
|
||||
|
||||
if (!adId || !userFlierlandId || !stripeCustomerId) return; // Bail on bad metadata
|
||||
console.log("good metaData");
|
||||
|
||||
// Quick cross-check: Ensure metadata matches session's Stripe customer
|
||||
if (session.customer !== stripeCustomerId) {
|
||||
console.warn(`Mismatch: Session customer ${session.customer} vs metadata ${stripeCustomerId}`);
|
||||
return;
|
||||
}
|
||||
console.log("correct customer id");
|
||||
|
||||
try {
|
||||
// --- Check ownership of the ad (with variables for security) ---
|
||||
const query = `
|
||||
query GetTheRequestedAd {
|
||||
Ads (
|
||||
filter: {
|
||||
id: { _eq: "${adId}" },
|
||||
user_created: { id: { _eq: "${userFlierlandId}" } },
|
||||
paid: { _eq: false }
|
||||
}
|
||||
) {
|
||||
id
|
||||
user_created {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const adResponse = await fetch(`${cmsAddress()}/graphql`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${adminAccessToken}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
query
|
||||
}),
|
||||
});
|
||||
|
||||
const { data: adData, error: adError } = await safeJson(adResponse);
|
||||
console.log(adData);
|
||||
if (!adResponse.ok || adError || !adData.data?.Ads?.[0]) return; // Invalid ad/ownership
|
||||
|
||||
const ad = adData.data.Ads[0];
|
||||
|
||||
// Double-check fetched ownership matches metadata (defense-in-depth)
|
||||
if (ad.user_created.id !== userFlierlandId) {
|
||||
console.warn(`Ownership mismatch: Fetched ${ad.user_created.id} vs metadata ${userFlierlandId}`);
|
||||
return;
|
||||
}
|
||||
console.log("ad ownership verified");
|
||||
|
||||
// Validate payment details
|
||||
if (paidAmount !== adPrice || session.currency !== metaData.currency) {
|
||||
console.warn(`Payment mismatch: Paid ${paidAmount} (expected ${adPrice}), currency ${session.currency} vs ${metaData.currency}`);
|
||||
return;
|
||||
}
|
||||
console.log("payment details validated");
|
||||
|
||||
// Idempotency check: See if receipt already exists for this Stripe payment
|
||||
if (!stripePaymentId) {
|
||||
console.warn('No stripe_payment_id in session');
|
||||
return;
|
||||
}
|
||||
console.log("stripe payment id available");
|
||||
|
||||
const receiptQuery = `
|
||||
query CheckExistingReceipt($stripePaymentId: String!) {
|
||||
Payments(filter: { stripe_payment_id: { _eq: $stripePaymentId } }) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`;
|
||||
const receiptRes = await fetch(`${cmsAddress()}/graphql`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${adminAccessToken}`
|
||||
},
|
||||
body: JSON.stringify({ query: receiptQuery, variables: { stripePaymentId } }),
|
||||
});
|
||||
const { data: receiptData, error: receiptError } = await safeJson(receiptRes);
|
||||
if (!receiptRes.ok || receiptError || receiptData?.payments?.[0]) {
|
||||
console.log(`⏭️ Receipt already exists for stripe_payment_id: ${stripePaymentId}`);
|
||||
return; // Skip—no duplicates
|
||||
}
|
||||
console.log("no pre-existing payments");
|
||||
|
||||
// Create receipt with stripe_payment_id for idempotency
|
||||
// const txId = `stripe-ad-${session.id}-${Date.now()}`;
|
||||
await CreatePaymentReceipt({
|
||||
status: "published",
|
||||
receipt_type: metaData.receipt_type,
|
||||
owner_user_id: userFlierlandId,
|
||||
stripe_customer_id: stripeCustomerId,
|
||||
stripe_payment_id: stripePaymentId,
|
||||
payment_method: "payment_gateway",
|
||||
payment_status: "succeeded",
|
||||
order_amount: metaData.order_amount,
|
||||
payed_amount: String(paidAmount),
|
||||
discount_used: false,
|
||||
currency: String(getCurrencyId({ abbr: session.currency as string })),
|
||||
service_id: adId,
|
||||
service_duration: -1,
|
||||
service_duration_term: "month",
|
||||
});
|
||||
|
||||
// Update ad
|
||||
await restBulkUpdateRequest({
|
||||
collection: 'Ads',
|
||||
updates: [
|
||||
{
|
||||
id: adId,
|
||||
data: {
|
||||
status: "published",
|
||||
paid: true
|
||||
},
|
||||
},
|
||||
],
|
||||
systemSecret: secret,
|
||||
});
|
||||
|
||||
console.log(`✅ Ad ${adId} activated for user ${userFlierlandId}`);
|
||||
} catch (error) {
|
||||
console.error('Error in activateMarketAd:', error);
|
||||
// Optional: Alert team (e.g., Sentry) for manual review
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,4 @@
|
||||
|
||||
export async function storePurchaseOrderPaid(metaData: any) {
|
||||
// TODO: Update DB → mark order paid, notify seller
|
||||
}
|
||||
@ -0,0 +1,42 @@
|
||||
import { CreatePaymentReceipt } from "lib/general/create-payment-receipt";
|
||||
import { extractAmountFromSession } from "lib/stripe/create-session-helper";
|
||||
import { getCurrencyId } from "services/directus/general";
|
||||
import { restBulkUpdateRequest } from "services/queries/directus/billboard";
|
||||
import Stripe from "stripe";
|
||||
|
||||
export const increaseWalletBalance = async (session: Stripe.Checkout.Session, secret: string, metaData: any) => {
|
||||
const paidTopUpAmount = await extractAmountFromSession(session);
|
||||
|
||||
try {
|
||||
await restBulkUpdateRequest({
|
||||
collection: 'directus_users',
|
||||
updates: [
|
||||
{
|
||||
id: metaData.user_flierland_id,
|
||||
data: {
|
||||
wallet_balance: String(Number(metaData.wallet_balance) + paidTopUpAmount)
|
||||
},
|
||||
},
|
||||
],
|
||||
systemSecret: secret
|
||||
});
|
||||
|
||||
// call payment receipt creation api
|
||||
await CreatePaymentReceipt({
|
||||
status: "published",
|
||||
receipt_type: metaData.receipt_type,
|
||||
owner_user_id: metaData.user_flierland_id,
|
||||
stripe_customer_id: metaData.stripe_customer_id,
|
||||
stripe_payment_id: session.payment_intent,
|
||||
payment_method: "payment_gateway",
|
||||
payment_status: "succeeded",
|
||||
order_amount: metaData.order_amount,
|
||||
payed_amount: String(paidTopUpAmount),
|
||||
discount_used: false,
|
||||
currency: String(getCurrencyId({ abbr: session.currency as string })),
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error updating user wallet:', error);
|
||||
}
|
||||
}
|
||||
8
src/lib/stripe/webhooks/invoice-paid.ts
Normal file
8
src/lib/stripe/webhooks/invoice-paid.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import Stripe from "stripe";
|
||||
|
||||
/**
|
||||
* Handles checkout.session.completed event
|
||||
*/
|
||||
export async function handleInvoicePaid(invoice: Stripe.Invoice) {
|
||||
|
||||
}
|
||||
8
src/lib/stripe/webhooks/payment-intent-succeeded.ts
Normal file
8
src/lib/stripe/webhooks/payment-intent-succeeded.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import Stripe from "stripe";
|
||||
|
||||
/**
|
||||
* Handles checkout.session.completed event
|
||||
*/
|
||||
export async function handlePaymentIntentSucceeded(intent: Stripe.PaymentIntent) {
|
||||
|
||||
}
|
||||
46
src/pages/api/auth/create-user.ts
Normal file
46
src/pages/api/auth/create-user.ts
Normal file
@ -0,0 +1,46 @@
|
||||
import { UserRegisterSchema } from "common/zod-schemas/user-register";
|
||||
import { NextApiRequest, NextApiResponse } from "next";
|
||||
import { restBulkCreateRequest } from "services/queries/directus/billboard";
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
if (req.method !== "POST") {
|
||||
return res.status(405).json({ error: "Method Not Allowed" });
|
||||
}
|
||||
|
||||
const parsed = UserRegisterSchema.safeParse(req.body);
|
||||
if (!parsed.success) {
|
||||
return res.status(400).json({ error: "Invalid payload", detail: parsed.error.errors });
|
||||
}
|
||||
|
||||
const userData = parsed.data; // Fully typed & validated
|
||||
|
||||
try {
|
||||
const { data, error } = await restBulkCreateRequest({
|
||||
collection: 'directus_users',
|
||||
items: [
|
||||
{
|
||||
status: "unverified",
|
||||
first_name: userData.first_name,
|
||||
last_name: userData.last_name,
|
||||
city: {
|
||||
id: userData.city_id
|
||||
},
|
||||
communication_language: {
|
||||
code: userData.communication_language
|
||||
},
|
||||
email: userData.email,
|
||||
password: userData.password,
|
||||
role: "5892560c-92ad-4d2b-9446-b5bdf0399e99"
|
||||
}
|
||||
],
|
||||
systemSecret: process.env.SYSTEM_API_SECRET
|
||||
});
|
||||
|
||||
if (error) {
|
||||
return res.status(400).json({ error });
|
||||
}
|
||||
return res.status(200).json({ data });
|
||||
} catch (err: any) {
|
||||
return res.status(500).json({ error: "User creation error:", detail: err.message });
|
||||
}
|
||||
}
|
||||
@ -1,27 +0,0 @@
|
||||
import { adminDataFetch } from 'common/data/apollo-client';
|
||||
import { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { adminAccessToken } from 'services/general/general';
|
||||
|
||||
export default async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
if (req.method === 'POST') {
|
||||
|
||||
let mutation = req.body.mutation;
|
||||
let system = req.body.system;
|
||||
let variables = req.body.variables;
|
||||
|
||||
try {
|
||||
const { data } = await adminDataFetch(system, adminAccessToken).mutate({
|
||||
mutation: mutation,
|
||||
variables: variables,
|
||||
});
|
||||
res.status(200).json(data);
|
||||
} catch (error:any) {
|
||||
console.error('Error from backend:', error.networkError?.result || error.message);
|
||||
return res.status(400).json(error);
|
||||
}
|
||||
|
||||
} else {
|
||||
res.setHeader('Allow', 'POST');
|
||||
res.status(405).end('Method Not Allowed');
|
||||
}
|
||||
};
|
||||
@ -1,6 +1,6 @@
|
||||
import { gql } from '@apollo/client';
|
||||
import { adminDataFetch } from 'common/data/apollo-client';
|
||||
import { Billboard, BillboardProduct } from 'common/types/billboard';
|
||||
import { Billboard } from 'common/types/billboard';
|
||||
import { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { adminAccessToken, getEngTr, getPerTr } from 'services/general/general';
|
||||
|
||||
|
||||
@ -3,6 +3,7 @@ import { adminDataFetch } from 'common/data/apollo-client';
|
||||
import { Billboard, BillboardProduct, ProductPriceEntry } from 'common/types/billboard';
|
||||
import { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { adminAccessToken, getEngTr, getPerTr, hasValue } from 'services/general/general';
|
||||
import z from 'zod';
|
||||
|
||||
export interface PriceDataProps {
|
||||
id: string;
|
||||
@ -21,12 +22,25 @@ export interface PriceDataProps {
|
||||
}[];
|
||||
}
|
||||
|
||||
// ✅ Inline Zod schema for validation
|
||||
const ProductsPriceDataSchema = z.object({
|
||||
productIds: z.array(z.string()),
|
||||
billboardId: z.string(),
|
||||
});
|
||||
|
||||
export default async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
if (req.method === 'POST') {
|
||||
|
||||
let productIds = req.body.data.productIds;
|
||||
let billboardId = req.body.data.billboardId;
|
||||
let system = req.body.system;
|
||||
const parsed = ProductsPriceDataSchema.safeParse(req.body);
|
||||
if (!parsed.success) {
|
||||
return res.status(400).json({
|
||||
error: "Invalid payload",
|
||||
detail: parsed.error.errors,
|
||||
});
|
||||
}
|
||||
|
||||
// safe, type validated body data
|
||||
const { productIds, billboardId } = parsed.data;
|
||||
|
||||
if (!Array.isArray(productIds) || !hasValue(billboardId)) {
|
||||
return res.status(400).json({ error: "Invalid productIds or billboardId" });
|
||||
@ -102,7 +116,7 @@ export default async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
`
|
||||
|
||||
try {
|
||||
const { data } = await adminDataFetch(system, adminAccessToken).query({
|
||||
const { data } = await adminDataFetch("", adminAccessToken).query({
|
||||
query: gql`
|
||||
query GetData {
|
||||
${getPriceData()}
|
||||
|
||||
96
src/pages/api/billboard/tasks/self/file-delete.ts
Normal file
96
src/pages/api/billboard/tasks/self/file-delete.ts
Normal file
@ -0,0 +1,96 @@
|
||||
import { Billboard } from "common/types/billboard";
|
||||
import { safeJson } from "lib/general/safe-response-json";
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import { adminAccessToken, cmsAddress } from "services/general/general";
|
||||
import { restBulkDeleteRequest } from "services/queries/directus/billboard";
|
||||
import { getUserFromRequest } from "services/user/get-user-from-request";
|
||||
|
||||
// DELETE multiple files (logo / cover_photo , etc.) if owned by the user
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
if (req.method !== "POST") {
|
||||
return res.status(405).json({ error: "Method not allowed" });
|
||||
}
|
||||
|
||||
try {
|
||||
const user = await getUserFromRequest(req);
|
||||
if (!user) {
|
||||
return res.status(401).json({ error: "Unauthorized" });
|
||||
}
|
||||
|
||||
const { fileIds, billboardId } = req.body as { fileIds?: string[]; billboardId: string };
|
||||
if (!fileIds || !Array.isArray(fileIds) || fileIds.length === 0) {
|
||||
return res.status(400).json({ error: "No file IDs provided" });
|
||||
}
|
||||
|
||||
// --- Check ownership of billboards ---
|
||||
const query = `
|
||||
query BillboardWithFiles {
|
||||
Billboards (
|
||||
filter: {
|
||||
ownership: { directus_users_id: { id: { _eq: "${user.id}" } } },
|
||||
id: { _eq: "${billboardId}" }
|
||||
}
|
||||
)
|
||||
{
|
||||
id
|
||||
logo {
|
||||
id
|
||||
}
|
||||
cover_photo {
|
||||
id
|
||||
}
|
||||
gallery {
|
||||
directus_files_id {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const response = await fetch(`${cmsAddress()}/graphql`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${adminAccessToken}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
query
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`GraphQL request failed: ${await response.text()}`);
|
||||
}
|
||||
|
||||
const { data, error } = await safeJson(response);
|
||||
|
||||
// Collect file IDs the user is actually allowed to delete
|
||||
const billboard: Billboard = data.data.Billboards[0];
|
||||
const billboardFileIds = new Set<string>();
|
||||
const allowedFileIds = new Set<string>();
|
||||
|
||||
billboardFileIds.add(billboard.logo.id);
|
||||
billboardFileIds.add(billboard.cover_photo.id);
|
||||
billboard.gallery.map(x => billboardFileIds.add(x.directus_files_id.id));
|
||||
|
||||
fileIds.filter(x => Array.from(billboardFileIds).includes(x)).map(x => allowedFileIds.add(x));
|
||||
|
||||
if (allowedFileIds.size === 0) {
|
||||
return res.status(403).json({ error: "Not authorized to delete these files" });
|
||||
}
|
||||
|
||||
// --- Delete authorized files only ---
|
||||
const deleteRes = await restBulkDeleteRequest({
|
||||
collection: 'directus_files',
|
||||
ids: Array.from(allowedFileIds),
|
||||
systemSecret: process.env.SYSTEM_API_SECRET
|
||||
});
|
||||
|
||||
return res.status(200).json(deleteRes);
|
||||
|
||||
} catch (err: any) {
|
||||
console.error("delete-file error:", err);
|
||||
return res.status(500).json({ error: err.message || "Server error" });
|
||||
}
|
||||
}
|
||||
@ -1,71 +1,68 @@
|
||||
import { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { adminAccessToken, cmsAddress } from 'services/general/general';
|
||||
import { NextApiRequest, NextApiResponse } from "next";
|
||||
import { bulkCreateDirectus } from "lib/directus/bulk-create-logic";
|
||||
import { cmsAddress } from "services/general/general";
|
||||
|
||||
/**
|
||||
* Public bulk-create API
|
||||
* - Requires a valid Directus user token (from cookies).
|
||||
* - Verifies collection permissions before attempting creations.
|
||||
*/
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
if (req.method !== 'POST') {
|
||||
return res.status(405).json({ error: 'Method Not Allowed' });
|
||||
// ✅ Only POST is allowed
|
||||
if (req.method !== "POST") {
|
||||
return res.status(405).json({ error: "Method Not Allowed" });
|
||||
}
|
||||
|
||||
const token = req.cookies['directus_session_token'];
|
||||
// ✅ Require session token from cookies
|
||||
const token = req.cookies["directus_session_token"];
|
||||
if (!token) {
|
||||
return res.status(401).json({ error: 'Not authenticated: No token' });
|
||||
return res.status(401).json({ error: "Not authenticated: No token" });
|
||||
}
|
||||
|
||||
try {
|
||||
const permsRes = await fetch(`${cmsAddress()}/permissions/me`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (!permsRes.ok) {
|
||||
return res.status(401).json({ error: 'Not authenticated: Invalid token' });
|
||||
}
|
||||
|
||||
const { collection, items, isAdmin }: { collection: string; items: Record<string, any>[]; isAdmin?: boolean; } = req.body;
|
||||
const perms = await permsRes.json();
|
||||
|
||||
const collectionAccess = perms.data[collection]; // Check if user has create access to this collection
|
||||
const createAccess = collectionAccess?.create;
|
||||
const hasCreateAccess =
|
||||
isAdmin ||
|
||||
createAccess?.access === 'full' ||
|
||||
(createAccess?.access === 'partial' && Array.isArray(createAccess.fields) && createAccess.fields.length > 0);
|
||||
|
||||
if (!hasCreateAccess) {
|
||||
return res.status(403).json({ error: 'Forbidden: you cannot create items in this collection' });
|
||||
}
|
||||
// ✅ Extract payload
|
||||
const { collection, items } = req.body as {
|
||||
collection: string;
|
||||
items: Record<string, any>[];
|
||||
};
|
||||
|
||||
if (!collection || !Array.isArray(items) || items.length === 0) {
|
||||
return res.status(400).json({ error: 'Invalid request structure.' });
|
||||
return res.status(400).json({ error: "Invalid request structure" });
|
||||
}
|
||||
|
||||
const createdItems = [];
|
||||
|
||||
for (const item of items) {
|
||||
if (!item || Object.keys(item).length === 0) continue;
|
||||
|
||||
const createRes = await fetch(`${cmsAddress()}/items/${collection}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${isAdmin ? adminAccessToken : token}`,
|
||||
},
|
||||
body: JSON.stringify(item),
|
||||
});
|
||||
|
||||
if (!createRes.ok) {
|
||||
const err = await createRes.text();
|
||||
throw new Error(`Failed to create item: ${err}`);
|
||||
}
|
||||
|
||||
const createdItem = await createRes.json();
|
||||
createdItems.push(createdItem.data);
|
||||
// ✅ Check permissions before updating
|
||||
const permsRes = await fetch(`${cmsAddress()}/permissions/me`, {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
});
|
||||
|
||||
if (!permsRes.ok) {
|
||||
return res.status(401).json({ error: "Not authenticated: Invalid token" });
|
||||
}
|
||||
|
||||
return res.status(200).json({ success: true, created: createdItems });
|
||||
} catch (error: any) {
|
||||
console.error('Bulk create error:', error);
|
||||
res.status(500).json({ error: 'Internal Server Error', detail: error.message });
|
||||
const perms = await permsRes.json();
|
||||
const collectionAccess = perms.data?.[collection];
|
||||
const creationAccess = collectionAccess?.create;
|
||||
|
||||
const hasCreateAccess =
|
||||
creationAccess?.access === "full" || creationAccess?.access === "partial";
|
||||
|
||||
if (!hasCreateAccess) {
|
||||
return res.status(403).json({ error: "Forbidden: no create access to this collection" });
|
||||
}
|
||||
|
||||
// ✅ Perform bulk create via shared logic
|
||||
const { data, error } = await bulkCreateDirectus({
|
||||
collection,
|
||||
items: items,
|
||||
token,
|
||||
});
|
||||
|
||||
if (error) {
|
||||
return res.status(400).json({ error });
|
||||
}
|
||||
return res.status(200).json({ data });
|
||||
|
||||
} catch (err: any) {
|
||||
return res.status(500).json({ error: "System bulk creation error:", detail: err.message });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,84 +1,69 @@
|
||||
import { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { adminAccessToken, cmsAddress } from 'services/general/general';
|
||||
import { NextApiRequest, NextApiResponse } from "next";
|
||||
import { bulkDeleteDirectus } from "lib/directus/bulk-delete-logic";
|
||||
import { cmsAddress } from "services/general/general";
|
||||
|
||||
export default async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
if (req.method !== 'POST') {
|
||||
return res.status(405).json({ error: 'Method Not Allowed' });
|
||||
/**
|
||||
* Public bulk-delete API
|
||||
* - Requires a valid Directus user token (from cookies).
|
||||
* - Verifies collection permissions before attempting deletes.
|
||||
*/
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
// ✅ Only POST is allowed
|
||||
if (req.method !== "POST") {
|
||||
return res.status(405).json({ error: "Method Not Allowed" });
|
||||
}
|
||||
|
||||
const token = req.cookies['directus_session_token'];
|
||||
// ✅ Require session token from cookies
|
||||
const token = req.cookies["directus_session_token"];
|
||||
if (!token) {
|
||||
return res.status(401).json({ error: 'Not authenticated: No token' });
|
||||
return res.status(401).json({ error: "Not authenticated: No token" });
|
||||
}
|
||||
|
||||
try {
|
||||
const { collection, ids, isAdmin }: { collection: string; ids: string[]; isAdmin?: boolean; } = req.body;
|
||||
|
||||
const getEndPoint = (id: string) => {
|
||||
let endPointAddress = "";
|
||||
switch (collection) {
|
||||
case "directus_files":
|
||||
endPointAddress = `/files/${id}`;
|
||||
break;
|
||||
|
||||
default:
|
||||
endPointAddress = `/items/${collection}/${id}`;
|
||||
break;
|
||||
}
|
||||
return endPointAddress;
|
||||
}
|
||||
// ✅ Extract payload
|
||||
const { collection, ids } = req.body as {
|
||||
collection: string;
|
||||
ids: any[];
|
||||
};
|
||||
|
||||
// Basic validation
|
||||
if (!collection || !Array.isArray(ids) || ids.length === 0) {
|
||||
return res.status(400).json({ error: 'Invalid request structure.' });
|
||||
return res.status(400).json({ error: "Invalid request structure" });
|
||||
}
|
||||
|
||||
// Check delete permissions
|
||||
// ✅ Check permissions before deleting
|
||||
const permsRes = await fetch(`${cmsAddress()}/permissions/me`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
});
|
||||
|
||||
|
||||
if (!permsRes.ok) {
|
||||
return res.status(401).json({ error: 'Not authenticated: Invalid token' });
|
||||
return res.status(401).json({ error: "Not authenticated: Invalid token" });
|
||||
}
|
||||
|
||||
const perms = await permsRes.json();
|
||||
const collectionAccess = perms.data[collection];
|
||||
const collectionAccess = perms.data?.[collection];
|
||||
const deleteAccess = collectionAccess?.delete;
|
||||
|
||||
const hasDeleteAccess =
|
||||
isAdmin ||
|
||||
deleteAccess?.access === 'full' ||
|
||||
deleteAccess?.access === 'partial';
|
||||
deleteAccess?.access === "full" || deleteAccess?.access === "partial";
|
||||
|
||||
if (!hasDeleteAccess) {
|
||||
return res.status(403).json({ error: 'Forbidden: you cannot delete from this collection' });
|
||||
return res.status(403).json({ error: "Forbidden: no delete access to this collection" });
|
||||
}
|
||||
|
||||
// Perform deletes
|
||||
const results = [];
|
||||
// ✅ Perform bulk delete via shared logic
|
||||
const { data, error } = await bulkDeleteDirectus({
|
||||
collection,
|
||||
items: ids,
|
||||
token,
|
||||
});
|
||||
|
||||
for (const id of ids) {
|
||||
const deleteRes = await fetch(`${cmsAddress()}${getEndPoint(id)}`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
Authorization: `Bearer ${isAdmin ? adminAccessToken : token}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (!deleteRes.ok) {
|
||||
const err = await deleteRes.text();
|
||||
throw new Error(`Failed to delete item ${id}: ${err}`);
|
||||
}
|
||||
|
||||
results.push({ id, deleted: true });
|
||||
if (error) {
|
||||
return res.status(400).json({ error });
|
||||
}
|
||||
return res.status(200).json({ data });
|
||||
|
||||
res.status(200).json({ success: true, deleted: results });
|
||||
} catch (error: any) {
|
||||
console.error('Bulk delete error:', error);
|
||||
res.status(500).json({ error: 'Internal Server Error', detail: error.message });
|
||||
} catch (err: any) {
|
||||
return res.status(500).json({ error: "System bulk delete error:", detail: err.message });
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -1,77 +1,68 @@
|
||||
import { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { adminAccessToken, cmsAddress } from 'services/general/general';
|
||||
import { NextApiRequest, NextApiResponse } from "next";
|
||||
import { bulkUpdateDirectus } from "lib/directus/bulk-update-logic";
|
||||
import { cmsAddress } from "services/general/general";
|
||||
|
||||
export default async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
|
||||
// only POST request is accepted
|
||||
if (req.method !== 'POST') {
|
||||
return res.status(405).json({ error: 'Method Not Allowed' });
|
||||
/**
|
||||
* Public bulk-update API
|
||||
* - Requires a valid Directus user token (from cookies).
|
||||
* - Verifies collection permissions before attempting updates.
|
||||
*/
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
// ✅ Only POST is allowed
|
||||
if (req.method !== "POST") {
|
||||
return res.status(405).json({ error: "Method Not Allowed" });
|
||||
}
|
||||
|
||||
// only authenticated requests are accepted
|
||||
const token = req.cookies['directus_session_token'];
|
||||
// ✅ Require session token from cookies
|
||||
const token = req.cookies["directus_session_token"];
|
||||
if (!token) {
|
||||
return res.status(401).json({ error: 'Not authenticated: No token' });
|
||||
return res.status(401).json({ error: "Not authenticated: No token" });
|
||||
}
|
||||
|
||||
try {
|
||||
const permsRes = await fetch(`${cmsAddress()}/permissions/me`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
if (!permsRes.ok) {
|
||||
return res.status(401).json({ error: 'Not authenticated: Invalid token' });
|
||||
// ✅ Extract payload
|
||||
const { collection, updates } = req.body as {
|
||||
collection: string;
|
||||
updates: { id: string; data: Record<string, any> }[];
|
||||
};
|
||||
|
||||
if (!collection || !Array.isArray(updates) || updates.length === 0) {
|
||||
return res.status(400).json({ error: "Invalid request structure" });
|
||||
}
|
||||
|
||||
// limit updates to only allowed collections
|
||||
const { collection, updates, isAdmin }: { collection: string, updates: any; isAdmin?: boolean; } = req.body;
|
||||
const perms = await permsRes.json();
|
||||
// ✅ Check permissions before updating
|
||||
const permsRes = await fetch(`${cmsAddress()}/permissions/me`, {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
});
|
||||
|
||||
const collectionAccess = perms.data[collection]; // Check if user has update access to this collection
|
||||
if (!permsRes.ok) {
|
||||
return res.status(401).json({ error: "Not authenticated: Invalid token" });
|
||||
}
|
||||
|
||||
const perms = await permsRes.json();
|
||||
const collectionAccess = perms.data?.[collection];
|
||||
const updateAccess = collectionAccess?.update;
|
||||
|
||||
const hasUpdateAccess =
|
||||
isAdmin ||
|
||||
updateAccess?.access === 'full' ||
|
||||
updateAccess?.access === 'partial'; // ← Don't block based on `fields` length
|
||||
updateAccess?.access === "full" || updateAccess?.access === "partial";
|
||||
|
||||
if (!hasUpdateAccess) {
|
||||
return res.status(403).json({ error: 'Forbidden: you cannot update this collection' });
|
||||
return res.status(403).json({ error: "Forbidden: no update access to this collection" });
|
||||
}
|
||||
|
||||
// Invalid request structure
|
||||
if (!collection || !Array.isArray(updates) || updates.length === 0) {
|
||||
return res.status(400).json({ error: 'Invalid request structure.' });
|
||||
// ✅ Perform bulk update via shared logic
|
||||
const { data, error } = await bulkUpdateDirectus({
|
||||
collection,
|
||||
items: updates,
|
||||
token,
|
||||
});
|
||||
|
||||
if (error) {
|
||||
return res.status(400).json({ error });
|
||||
}
|
||||
return res.status(200).json({ data });
|
||||
|
||||
// Bulk update logic
|
||||
const results = [];
|
||||
|
||||
for (const item of updates) {
|
||||
const { id, data } = item;
|
||||
|
||||
if (!id || !data || Object.keys(data).length === 0) continue;
|
||||
|
||||
const updateRes = await fetch(`${cmsAddress()}/items/${collection}/${id}`, {
|
||||
method: 'PATCH',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${isAdmin ? adminAccessToken : token}`,
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
|
||||
if (!updateRes.ok) {
|
||||
const err = await updateRes.text();
|
||||
throw new Error(`Failed to update item ${id}: ${err}`);
|
||||
}
|
||||
|
||||
const updatedItem = await updateRes.json();
|
||||
results.push(updatedItem.data);
|
||||
}
|
||||
|
||||
res.status(200).json({ success: true, updated: results });
|
||||
} catch (error: any) {
|
||||
console.error('Bulk update error:', error);
|
||||
res.status(500).json({ error: 'Internal Server Error', detail: error.message });
|
||||
} catch (err: any) {
|
||||
return res.status(500).json({ error: "System bulk update error:", detail: err.message });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
33
src/pages/api/general/payments/create-payment.ts
Normal file
33
src/pages/api/general/payments/create-payment.ts
Normal file
@ -0,0 +1,33 @@
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import { validateSystemSecret } from "lib/server/validate-system-secret";
|
||||
import { restBulkCreateRequest } from "services/queries/directus/billboard";
|
||||
import { PaymentDataSchema } from "common/zod-schemas/create-payment";
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
if (req.method !== "POST") {
|
||||
return res.status(405).json({ error: "Method Not Allowed" });
|
||||
}
|
||||
|
||||
// Require system secret header
|
||||
if (!validateSystemSecret(req, res)) return;
|
||||
|
||||
const parsed = PaymentDataSchema.safeParse(req.body);
|
||||
if (!parsed.success) {
|
||||
return res.status(400).json({ error: "Invalid payload", detail: parsed.error.errors });
|
||||
}
|
||||
|
||||
const receiptData = parsed.data; // Fully typed & validated
|
||||
|
||||
try {
|
||||
const results = await restBulkCreateRequest({
|
||||
collection: 'Payments',
|
||||
items: [receiptData],
|
||||
systemSecret: process.env.SYSTEM_API_SECRET
|
||||
});
|
||||
|
||||
return res.status(200).json({ success: true, created: results });
|
||||
} catch (error: any) {
|
||||
console.error("System bulk creation error:", error);
|
||||
return res.status(500).json({ error: "Internal Server Error" });
|
||||
}
|
||||
}
|
||||
90
src/pages/api/general/payments/stripe-payment-status.ts
Normal file
90
src/pages/api/general/payments/stripe-payment-status.ts
Normal file
@ -0,0 +1,90 @@
|
||||
import { handleAdPaymentStatus } from "lib/stripe/payment-status/ad-payment";
|
||||
import { handleBillboardPaymentStatus } from "lib/stripe/payment-status/billboard-payment";
|
||||
import { handlePurchaseOrderPaymentStatus } from "lib/stripe/payment-status/purchase-order";
|
||||
import { handleRefundPaymentStatus } from "lib/stripe/payment-status/refund";
|
||||
import { handleServiceOrderPaymentStatus } from "lib/stripe/payment-status/service-order";
|
||||
import { handleWalletTopUpPaymentStatus } from "lib/stripe/payment-status/wallet-top-up";
|
||||
import { handleWithdrawalPaymentStatus } from "lib/stripe/payment-status/withdrawal";
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import { adminAccessToken } from "services/general/general";
|
||||
import { getUserFromRequest } from "services/user/get-user-from-request";
|
||||
import { z } from "zod";
|
||||
|
||||
// ✅ Inline Zod schema for validation
|
||||
const PaymentDataSchema = z.object({
|
||||
stripePaymentId: z.string(),
|
||||
resourceType: z.enum([
|
||||
"flierland-ad",
|
||||
"flierland-billboard",
|
||||
"flierland-wallet-top-up",
|
||||
"purchase-order",
|
||||
"service-order",
|
||||
"refund",
|
||||
"withdrawal",
|
||||
]),
|
||||
resourceId: z.string(),
|
||||
});
|
||||
|
||||
// ✅ Main route
|
||||
const stripePaymentStatus = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
if (req.method !== "POST") {
|
||||
return res.status(405).json({ error: "Method Not Allowed" });
|
||||
}
|
||||
|
||||
const user = await getUserFromRequest(req);
|
||||
if (!user) {
|
||||
return res.status(401).json({ error: "Unauthorized" });
|
||||
}
|
||||
|
||||
const parsed = PaymentDataSchema.safeParse(req.body);
|
||||
if (!parsed.success) {
|
||||
return res.status(400).json({
|
||||
error: "Invalid payload",
|
||||
detail: parsed.error.errors,
|
||||
});
|
||||
}
|
||||
|
||||
const { stripePaymentId, resourceType, resourceId } = parsed.data;
|
||||
|
||||
try {
|
||||
// 🔒 Security measure: Only allow checking for your own CMS
|
||||
if (!adminAccessToken) {
|
||||
return res.status(500).json({ error: "Missing server credentials" });
|
||||
}
|
||||
|
||||
// ✅ Delegate to correct handler for further checks
|
||||
let finished = false;
|
||||
switch (resourceType) {
|
||||
case "flierland-ad":
|
||||
finished = await handleAdPaymentStatus(resourceId, user.id);
|
||||
break;
|
||||
case "flierland-billboard":
|
||||
finished = await handleBillboardPaymentStatus(resourceId, user.id);
|
||||
break;
|
||||
case "flierland-wallet-top-up":
|
||||
finished = await handleWalletTopUpPaymentStatus(stripePaymentId, user.id);
|
||||
break;
|
||||
case "purchase-order":
|
||||
finished = await handlePurchaseOrderPaymentStatus(resourceId, user.id);
|
||||
break;
|
||||
case "service-order":
|
||||
finished = await handleServiceOrderPaymentStatus(resourceId, user.id);
|
||||
break;
|
||||
case "refund":
|
||||
finished = await handleRefundPaymentStatus(resourceId, user.id);
|
||||
break;
|
||||
case "withdrawal":
|
||||
finished = await handleWithdrawalPaymentStatus(resourceId, user.id);
|
||||
break;
|
||||
default:
|
||||
return res.status(400).json({ error: "Unsupported resource type" });
|
||||
}
|
||||
|
||||
return res.status(200).json({ data: finished });
|
||||
} catch (error: any) {
|
||||
console.error("Stripe payment status check error:", error);
|
||||
return res.status(500).json({ error: "Internal Server Error" });
|
||||
}
|
||||
};
|
||||
|
||||
export default stripePaymentStatus;
|
||||
0
src/pages/api/general/payments/update-payment.ts
Normal file
0
src/pages/api/general/payments/update-payment.ts
Normal file
141
src/pages/api/general/wallet/ad-payment.ts
Normal file
141
src/pages/api/general/wallet/ad-payment.ts
Normal file
@ -0,0 +1,141 @@
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import { getUserFromRequest } from "services/user/get-user-from-request";
|
||||
import { restBulkUpdateRequest } from "services/queries/directus/billboard";
|
||||
import { CreatePaymentReceipt } from "lib/general/create-payment-receipt";
|
||||
import { getCurrencyId } from "services/directus/general";
|
||||
import { adminAccessToken, cmsAddress } from "services/general/general";
|
||||
import { safeJson } from "lib/general/safe-response-json";
|
||||
import { MarketAd } from "common/types/market";
|
||||
import z from "zod";
|
||||
|
||||
// Define schema once—reusable and type-safe
|
||||
const AdPaymentSchema = z.object({
|
||||
adId: z.string().min(1, 'adId must be a non-empty string'), // Basic sanitization; add .uuid() if IDs are UUIDs
|
||||
currency: z.enum(["usd", "eur", "cad", "irr", "jpy", "tm"] as const, { message: 'Invalid currency' }), // Or z.nativeEnum(CurrenciesAbbr) if it's an enum
|
||||
});
|
||||
// Infer TS type for free
|
||||
type AdPaymentInput = z.infer<typeof AdPaymentSchema>;
|
||||
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
if (req.method !== "POST") {
|
||||
return res.status(405).json({ error: "Method Not Allowed" });
|
||||
}
|
||||
|
||||
// get user data from the session token
|
||||
const user = await getUserFromRequest(req);
|
||||
if (!user) {
|
||||
return res.status(401).json({ error: "Unauthorized" });
|
||||
}
|
||||
|
||||
const adPrice = 20; // the static, standard ad price
|
||||
|
||||
// check if user's wallet balance is sufficient for the transacation
|
||||
if (Number(user.wallet_balance) < adPrice) {
|
||||
return res.status(400).json({ error: "Insufficient wallet balance" });
|
||||
}
|
||||
|
||||
// Parse & validate early—replaces manual checks
|
||||
let input: AdPaymentInput;
|
||||
try {
|
||||
input = AdPaymentSchema.parse(req.body); // Throws ZodError on failure
|
||||
} catch (error) {
|
||||
console.log('Invalid request body');
|
||||
return res.status(400).json({ error: 'Invalid request body', details: error }); // Or just error.message for simpler
|
||||
}
|
||||
|
||||
// Now use input.adId & input.currency—guaranteed valid!
|
||||
const { adId, currency } = input;
|
||||
|
||||
try {
|
||||
// --- Check ownership of the ad ---
|
||||
const query = `
|
||||
query GetTheRequestedAd {
|
||||
Ads (
|
||||
filter: {
|
||||
id: { _eq: "${adId}" },
|
||||
user_created: { id: { _eq: "${user.id}" } },
|
||||
paid: { _eq: ${false} }
|
||||
}
|
||||
) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const response = await fetch(`${cmsAddress()}/graphql`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${adminAccessToken}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
query,
|
||||
}),
|
||||
});
|
||||
|
||||
const { data, error } = await safeJson(response);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`GraphQL request failed: ${error}`);
|
||||
}
|
||||
|
||||
// Collect file IDs the user is actually allowed to delete
|
||||
const Ad: MarketAd = data.data.Ads[0];
|
||||
|
||||
if (!Ad) {
|
||||
return res.status(400).json({ error: "Invalid ad" });
|
||||
}
|
||||
|
||||
// updating user wallet to deduct the ad price
|
||||
await restBulkUpdateRequest({
|
||||
collection: 'directus_users',
|
||||
updates: [
|
||||
{
|
||||
id: user.id,
|
||||
data: {
|
||||
wallet_balance: Number(user.wallet_balance) - adPrice, // Calculated as currentBalance - ad price
|
||||
},
|
||||
},
|
||||
],
|
||||
systemSecret: process.env.SYSTEM_API_SECRET,
|
||||
});
|
||||
|
||||
// call payment receipt creation api
|
||||
await CreatePaymentReceipt({
|
||||
status: "published",
|
||||
receipt_type: "flierland-ad",
|
||||
owner_user_id: user.id,
|
||||
stripe_customer_id: user.raw.stripe_customer_id,
|
||||
payment_method: "wallet",
|
||||
payment_status: "succeeded",
|
||||
order_amount: String(adPrice),
|
||||
payed_amount: String(adPrice),
|
||||
discount_used: false,
|
||||
currency: String(getCurrencyId({ abbr: currency })),
|
||||
service_id: adId,
|
||||
service_duration: -1,
|
||||
service_duration_term: "month"
|
||||
});
|
||||
|
||||
// updating ad to set status to paid
|
||||
const results = await restBulkUpdateRequest({
|
||||
collection: 'Ads',
|
||||
updates: [
|
||||
{
|
||||
id: adId,
|
||||
data: {
|
||||
status: "published",
|
||||
paid: true
|
||||
},
|
||||
},
|
||||
],
|
||||
systemSecret: process.env.SYSTEM_API_SECRET,
|
||||
});
|
||||
|
||||
return res.status(200).json({ success: true, updated: results });
|
||||
} catch (error: any) {
|
||||
console.error("System bulk update error:", error);
|
||||
return res.status(500).json({ error: "Internal Server Error", detail: error.message });
|
||||
}
|
||||
}
|
||||
0
src/pages/api/general/wallet/wallwt-refund.ts
Normal file
0
src/pages/api/general/wallet/wallwt-refund.ts
Normal file
@ -7,7 +7,7 @@ const stripe = require('stripe')(getStripeSecretKey());
|
||||
const checkoutHandler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
|
||||
let priceId = req.body.price_id;
|
||||
let serviceType = req.body.service_type;
|
||||
let receiptType = req.body.receipt_type;
|
||||
let serviceId = req.body.service_id;
|
||||
let stripeId = req.body.stripe_id;
|
||||
let period = req.body.period;
|
||||
@ -16,8 +16,6 @@ const checkoutHandler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
let userEmail = req.body.user_email;
|
||||
let userWalletBalance = req.body.user_wallet_balance;
|
||||
|
||||
console.log(stripeId);
|
||||
|
||||
if (req.method === 'POST') {
|
||||
try {
|
||||
// Create Checkout Sessions from body params.
|
||||
@ -35,7 +33,7 @@ const checkoutHandler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
customer: stripeId,
|
||||
metadata: {
|
||||
service_period: period === -1 ? -1 : period,
|
||||
service_type: serviceType,
|
||||
receipt_type: receiptType,
|
||||
service_id: serviceId ?? "",
|
||||
user_flierland_id: userId,
|
||||
user_full_name: userFullName,
|
||||
|
||||
40
src/pages/api/stripe/create-checkout-session.ts
Normal file
40
src/pages/api/stripe/create-checkout-session.ts
Normal file
@ -0,0 +1,40 @@
|
||||
import { NextApiRequest, NextApiResponse } from "next";
|
||||
import { createCheckoutSession, CheckoutSessionParams } from "lib/stripe/create-session-helper";
|
||||
import { getUserFromRequest } from "services/user/get-user-from-request";
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
if (req.method !== "POST") {
|
||||
res.setHeader("Allow", "POST");
|
||||
return res.status(405).end("Method Not Allowed");
|
||||
}
|
||||
|
||||
const user = await getUserFromRequest(req);
|
||||
if (!user) {
|
||||
return res.status(401).json({ error: "Unauthorized" });
|
||||
}
|
||||
|
||||
try {
|
||||
const sessionParams: CheckoutSessionParams = req.body;
|
||||
|
||||
// Build safe metadata: Only include string values, stringify non-strings if needed. No sensitive data
|
||||
const userMetadata = {
|
||||
user_flierland_id: user.id,
|
||||
stripe_customer_id: user.raw.stripe_customer_id,
|
||||
user_email: user.email || '',
|
||||
wallet_balance: user.wallet_balance,
|
||||
user_role: typeof user.role === 'string' ? user.role : JSON.stringify(user.role),
|
||||
};
|
||||
|
||||
const session = await createCheckoutSession({ ...sessionParams, metadata: { ...sessionParams.metadata, ...userMetadata } });
|
||||
|
||||
// Double-check session.sessionUrl (not session.url) before responding
|
||||
if (!session.sessionUrl) {
|
||||
throw new Error('Failed to generate Stripe session URL');
|
||||
}
|
||||
|
||||
res.status(200).json({ sessionUrl: session.sessionUrl }); // Use session.sessionUrl
|
||||
} catch (err: any) {
|
||||
console.error("Stripe checkout session error:", err);
|
||||
res.status(err.statusCode || 500).json({ error: err.message });
|
||||
}
|
||||
}
|
||||
29
src/pages/api/stripe/get-session.ts
Normal file
29
src/pages/api/stripe/get-session.ts
Normal file
@ -0,0 +1,29 @@
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import { getStripeSecretKey } from "services/general/stripe";
|
||||
import Stripe from "stripe";
|
||||
|
||||
const stripe = new Stripe(getStripeSecretKey(), {
|
||||
apiVersion: "2025-08-27.basil",
|
||||
});
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
if (req.method !== "GET") {
|
||||
return res.status(405).json({ error: "Method not allowed" });
|
||||
}
|
||||
|
||||
const { session_id } = req.query;
|
||||
|
||||
if (!session_id || typeof session_id !== "string") {
|
||||
return res.status(400).json({ error: "Missing or invalid session_id" });
|
||||
}
|
||||
|
||||
try {
|
||||
const session = await stripe.checkout.sessions.retrieve(session_id, {
|
||||
expand: ["payment_intent"], // optional: expands related data
|
||||
});
|
||||
|
||||
return res.status(200).json(session);
|
||||
} catch (err: any) {
|
||||
return res.status(500).json({ error: err.message });
|
||||
}
|
||||
}
|
||||
@ -9,6 +9,7 @@ import { updateUserData, updateUserDetails } from 'common/templates/dashboard/ac
|
||||
import { newPaymentQuery } from 'services/queries/data-queries';
|
||||
import { addNewPayment } from 'services/queries/directus/cms';
|
||||
import { getStripeSecretKey } from 'services/general/stripe';
|
||||
import { getCurrencyId } from 'services/directus/general';
|
||||
|
||||
const stripe = new Stripe(getStripeSecretKey());
|
||||
|
||||
@ -111,10 +112,10 @@ const webhookHandler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
owner_user_id: session.metadata?.user_flierland_id ?? "",
|
||||
order_amount: String(session.amount_subtotal).slice(0, -2),
|
||||
payed_amount: String(session.amount_total).slice(0, -2),
|
||||
currency: session.currency,
|
||||
currency: getCurrencyId({ abbr: session.currency as string }),
|
||||
payment_method: 'payment_gateway',
|
||||
discount_used: 'no',
|
||||
service_type: session.metadata?.service_type,
|
||||
discount_used: false,
|
||||
service_type: session.metadata?.service_type as any,
|
||||
service_id: session.metadata?.service_id ?? "",
|
||||
service_duration: Number(session.metadata?.service_period) !== -1 ? Number(session.metadata?.service_period) : -1,
|
||||
service_duration_term: "month"
|
||||
|
||||
@ -2,13 +2,14 @@ import { NextApiRequest, NextApiResponse } from 'next';
|
||||
import Stripe from 'stripe';
|
||||
import { buffer } from 'micro';
|
||||
import Cors from 'micro-cors';
|
||||
import { adminAccessToken, hasValue, mtd } from 'services/general/general';
|
||||
import { adminAccessToken } from 'services/general/general';
|
||||
import { adminDataFetch } from 'common/data/apollo-client';
|
||||
import { updateAd } from 'common/templates/dashboard/ads/forms/payloads';
|
||||
import { updateUserData, updateUserDetails } from 'common/templates/dashboard/account/payloads';
|
||||
import { updateUserData } from 'common/templates/dashboard/account/payloads';
|
||||
import { newPaymentQuery } from 'services/queries/data-queries';
|
||||
import { addNewPayment } from 'services/queries/directus/cms';
|
||||
import { getStripeSecretKey } from 'services/general/stripe';
|
||||
import { getCurrencyId } from 'services/directus/general';
|
||||
|
||||
const stripe = new Stripe(getStripeSecretKey());
|
||||
|
||||
@ -111,10 +112,10 @@ const webhookHandler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
owner_user_id: session.metadata?.user_flierland_id ?? "",
|
||||
order_amount: String(session.amount_subtotal).slice(0, -2),
|
||||
payed_amount: String(session.amount_total).slice(0, -2),
|
||||
currency: session.currency,
|
||||
currency: getCurrencyId({ abbr: session.currency as string }),
|
||||
payment_method: 'payment_gateway',
|
||||
discount_used: 'no',
|
||||
service_type: session.metadata?.service_type,
|
||||
discount_used: false,
|
||||
service_type: session.metadata?.service_type as any,
|
||||
service_id: session.metadata?.service_id ?? "",
|
||||
service_duration: Number(session.metadata?.service_period) !== -1 ? Number(session.metadata?.service_period) : -1,
|
||||
service_duration_term: "month"
|
||||
|
||||
64
src/pages/api/stripe/webhook.ts
Normal file
64
src/pages/api/stripe/webhook.ts
Normal file
@ -0,0 +1,64 @@
|
||||
import { buffer } from "micro";
|
||||
import Stripe from "stripe";
|
||||
import { NextApiRequest, NextApiResponse } from "next";
|
||||
import { handleCheckoutSessionCompleted } from "lib/stripe/webhooks/checkout-session-completed/checkout-session-completed";
|
||||
import { handleInvoicePaid } from "lib/stripe/webhooks/invoice-paid";
|
||||
import { handlePaymentIntentSucceeded } from "lib/stripe/webhooks/payment-intent-succeeded";
|
||||
import { getStripeSecretKey, getWebhookSecretKey } from "services/general/stripe";
|
||||
|
||||
export const config = {
|
||||
api: {
|
||||
bodyParser: false, // Stripe requires raw body
|
||||
},
|
||||
};
|
||||
|
||||
// creating stripe instance
|
||||
const stripe = new Stripe(getStripeSecretKey(), {
|
||||
apiVersion: "2025-08-27.basil",
|
||||
});
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
if (req.method !== "POST") {
|
||||
res.setHeader("Allow", "POST");
|
||||
return res.status(405).end("Method Not Allowed");
|
||||
}
|
||||
|
||||
// Verifying signature first to make sure it's safe & authentic
|
||||
const sig = req.headers["stripe-signature"]!;
|
||||
const buf = await buffer(req);
|
||||
|
||||
let event: Stripe.Event;
|
||||
|
||||
try {
|
||||
event = stripe.webhooks.constructEvent(buf, sig, getWebhookSecretKey());
|
||||
} catch (err: any) {
|
||||
console.error("Webhook signature verification failed:", err.message);
|
||||
return res.status(400).send(`Webhook Error: ${err.message}`);
|
||||
}
|
||||
|
||||
// ✅ At this point, `event` is safe and version-stable
|
||||
try {
|
||||
switch (event.type) {
|
||||
case "checkout.session.completed":
|
||||
await handleCheckoutSessionCompleted(event.data.object as Stripe.Checkout.Session, process.env.SYSTEM_API_SECRET!);
|
||||
break;
|
||||
|
||||
case "invoice.payment_succeeded":
|
||||
await handleInvoicePaid(event.data.object as Stripe.Invoice);
|
||||
break;
|
||||
|
||||
case "payment_intent.succeeded":
|
||||
await handlePaymentIntentSucceeded(event.data.object as Stripe.PaymentIntent);
|
||||
break;
|
||||
|
||||
// Add more cases as needed
|
||||
default:
|
||||
console.log(`Unhandled event type ${event.type}`);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Error handling webhook:", err);
|
||||
return res.status(500).send("Webhook handler failed");
|
||||
}
|
||||
|
||||
res.json({ received: true });
|
||||
}
|
||||
35
src/pages/api/system/system-bulk-create.ts
Normal file
35
src/pages/api/system/system-bulk-create.ts
Normal file
@ -0,0 +1,35 @@
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import { bulkCreateDirectus } from "lib/directus/bulk-create-logic";
|
||||
import { adminAccessToken } from "services/general/general";
|
||||
import { validateSystemSecret } from "lib/server/validate-system-secret";
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
if (req.method !== "POST") {
|
||||
return res.status(405).json({ error: "Method Not Allowed" });
|
||||
}
|
||||
|
||||
// Require system secret header
|
||||
if (!validateSystemSecret(req, res)) return;
|
||||
|
||||
try {
|
||||
const { collection, items } = req.body;
|
||||
|
||||
if (!collection || !Array.isArray(items) || items.length === 0) {
|
||||
return res.status(400).json({ error: "Invalid request structure" });
|
||||
}
|
||||
|
||||
const { data, error } = await bulkCreateDirectus({
|
||||
collection,
|
||||
items: items,
|
||||
token: adminAccessToken!, // always use admin token here
|
||||
});
|
||||
|
||||
if (error) {
|
||||
return res.status(400).json({ error });
|
||||
}
|
||||
return res.status(200).json({ data });
|
||||
|
||||
} catch (err: any) {
|
||||
return res.status(500).json({ error: "System bulk creation error:", detail: err.message });
|
||||
}
|
||||
}
|
||||
35
src/pages/api/system/system-bulk-delete.ts
Normal file
35
src/pages/api/system/system-bulk-delete.ts
Normal file
@ -0,0 +1,35 @@
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import { bulkDeleteDirectus } from "lib/directus/bulk-delete-logic";
|
||||
import { adminAccessToken } from "services/general/general";
|
||||
import { validateSystemSecret } from "lib/server/validate-system-secret";
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
if (req.method !== "POST") {
|
||||
return res.status(405).json({ error: "Method Not Allowed" });
|
||||
}
|
||||
|
||||
// Require system secret header
|
||||
if (!validateSystemSecret(req, res)) return;
|
||||
|
||||
try {
|
||||
const { collection, ids } = req.body;
|
||||
|
||||
if (!collection || !Array.isArray(ids) || ids.length === 0) {
|
||||
return res.status(400).json({ error: "Invalid request structure" });
|
||||
}
|
||||
|
||||
const { data, error } = await bulkDeleteDirectus({
|
||||
collection,
|
||||
items: ids,
|
||||
token: adminAccessToken!, // always use admin token here
|
||||
});
|
||||
|
||||
if (error) {
|
||||
return res.status(400).json({ error });
|
||||
}
|
||||
return res.status(200).json({ data });
|
||||
|
||||
} catch (err: any) {
|
||||
return res.status(500).json({ error: "System bulk delete error:", detail: err.message });
|
||||
}
|
||||
}
|
||||
35
src/pages/api/system/system-bulk-update.ts
Normal file
35
src/pages/api/system/system-bulk-update.ts
Normal file
@ -0,0 +1,35 @@
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import { bulkUpdateDirectus } from "lib/directus/bulk-update-logic";
|
||||
import { adminAccessToken } from "services/general/general";
|
||||
import { validateSystemSecret } from "lib/server/validate-system-secret";
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
if (req.method !== "POST") {
|
||||
return res.status(405).json({ error: "Method Not Allowed" });
|
||||
}
|
||||
|
||||
// Require system secret header
|
||||
if (!validateSystemSecret(req, res)) return;
|
||||
|
||||
try {
|
||||
const { collection, updates } = req.body;
|
||||
|
||||
if (!collection || !Array.isArray(updates) || updates.length === 0) {
|
||||
return res.status(400).json({ error: "Invalid request structure" });
|
||||
}
|
||||
|
||||
const { data, error } = await bulkUpdateDirectus({
|
||||
collection,
|
||||
items: updates,
|
||||
token: adminAccessToken!, // always use admin token here
|
||||
});
|
||||
|
||||
if (error) {
|
||||
return res.status(400).json({ error });
|
||||
}
|
||||
return res.status(200).json({ data });
|
||||
|
||||
} catch (err: any) {
|
||||
return res.status(500).json({ error: "System bulk update error:", detail: err.message });
|
||||
}
|
||||
}
|
||||
64
src/pages/api/user/get-billboards.ts
Normal file
64
src/pages/api/user/get-billboards.ts
Normal file
@ -0,0 +1,64 @@
|
||||
import { gql } from "@apollo/client";
|
||||
import { adminDataFetch } from "common/data/apollo-client";
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import { adminAccessToken } from "services/general/general";
|
||||
import { getUserFromRequest } from "services/user/get-user-from-request";
|
||||
|
||||
const stripePaymentStatus = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
if (req.method !== "POST") {
|
||||
return res.status(405).json({ error: "Method Not Allowed" });
|
||||
}
|
||||
|
||||
const user = await getUserFromRequest(req);
|
||||
if (!user) {
|
||||
return res.status(401).json({ error: "Unauthorized" });
|
||||
}
|
||||
|
||||
// 🔒 Security measure: Only allow checking for your own CMS
|
||||
if (!adminAccessToken) {
|
||||
return res.status(500).json({ error: "Missing server credentials" });
|
||||
}
|
||||
|
||||
|
||||
const query = () => `
|
||||
Billboards (
|
||||
filter: {
|
||||
ownership: { directus_users_id: { id: { _eq: "${user.id}" } } },
|
||||
},
|
||||
sort: ["sort", "-date_created"],
|
||||
limit: -1
|
||||
)
|
||||
{
|
||||
id
|
||||
}
|
||||
`
|
||||
try {
|
||||
const { data } = await adminDataFetch("", adminAccessToken).query({
|
||||
query: gql`
|
||||
query GetData {
|
||||
${query()}
|
||||
}
|
||||
`,
|
||||
context: {
|
||||
headers: {
|
||||
Cookie: req.headers.cookie || '', // Forward incoming cookies
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
// Ensuring the data exists
|
||||
if (!data) {
|
||||
return res.status(404).json({ error: "No ownership data found" });
|
||||
}
|
||||
|
||||
const billboards: string[] = data.Billboards.map((x:any) => x.id);
|
||||
|
||||
return res.status(200).json({ billboards });
|
||||
} catch (error: any) {
|
||||
console.error("Stripe payment status check error:", error);
|
||||
return res.status(500).json({ error: "Internal Server Error" });
|
||||
}
|
||||
};
|
||||
|
||||
export default stripePaymentStatus;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user