made billboards dashboard page + general updates

This commit is contained in:
Deltora72 2024-10-17 11:50:05 +03:30
parent 4c25f8ca6c
commit af5294a6f8
38 changed files with 6352 additions and 1903 deletions

2
next-env.d.ts vendored
View File

@ -2,4 +2,4 @@
/// <reference types="next/image-types/global" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/basic-features/typescript for more information.
// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information.

5317
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -16,6 +16,10 @@
"@dnd-kit/sortable": "^7.0.2",
"@fortawesome/react-fontawesome": "^0.2.0",
"@googlemaps/js-api-loader": "^1.16.6",
"@nivo/bar": "^0.87.0",
"@nivo/core": "^0.87.0",
"@nivo/line": "^0.87.0",
"@nivo/pie": "^0.87.0",
"@reduxjs/toolkit": "^2.2.3",
"@sendgrid/mail": "^7.7.0",
"@stripe/stripe-js": "^3.0.10",
@ -26,10 +30,12 @@
"@types/react-redux": "^7.1.23",
"axios": "^1.7.2",
"bcrypt": "^5.1.1",
"chart.js": "^4.4.4",
"colorthief": "^2.4.0",
"concurrently": "^7.6.0",
"cookie": "^0.6.0",
"cors": "^2.8.5",
"d3": "^7.9.0",
"dompurify": "^3.1.0",
"embla-carousel-autoplay": "^8.0.0-rc14",
"embla-carousel-react": "^8.0.0-rc22",
@ -46,6 +52,7 @@
"next-sitemap": "^4.2.3",
"react": "^18.2.0",
"react-beautiful-dnd": "^13.1.1",
"react-chartjs-2": "^5.2.0",
"react-dom": "^18.2.0",
"react-infinite-scroll-hook": "^4.1.1",
"react-player": "^2.16.0",

View File

@ -0,0 +1,183 @@
<?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" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 500 500" style="enable-background:new 0 0 500 500;" xml:space="preserve">
<g id="BACKGROUND">
<ellipse transform="matrix(0.9475 -0.3196 0.3196 0.9475 -67.2231 78.8269)" style="fill:#F5F5FF;" cx="206.549" cy="244.221" rx="142.424" ry="160.232"/>
</g>
<g id="OBJECTS">
<ellipse style="fill:#DFDFEB;" cx="349.605" cy="437.787" rx="60.92" ry="11.298"/>
<g>
<path style="fill:#D3DDF7;" d="M287.494,313.87H58.911c-5.61,0-10.158-4.548-10.158-10.158V160.014
c0-5.61,4.548-10.158,10.158-10.158h228.583c5.61,0,10.158,4.548,10.158,10.158v143.698
C297.652,309.322,293.104,313.87,287.494,313.87z"/>
<path style="fill:#7584F1;" d="M287.494,149.856H58.911c-5.61,0-10.158,4.548-10.158,10.158v9.984h248.898v-9.984
C297.652,154.404,293.104,149.856,287.494,149.856z"/>
<g>
<circle style="fill:none;stroke:#000000;stroke-linejoin:round;stroke-miterlimit:10;" cx="256.366" cy="160.247" r="3.597"/>
<circle style="fill:none;stroke:#000000;stroke-linejoin:round;stroke-miterlimit:10;" cx="269.815" cy="160.247" r="3.597"/>
<circle style="fill:none;stroke:#000000;stroke-linejoin:round;stroke-miterlimit:10;" cx="283.265" cy="160.247" r="3.597"/>
</g>
<g>
<g>
<path style="fill:#7584F1;" d="M150.813,200.125c-2.837,0-5.583-1.895-7.761-3.713l-2.652-2.213
c-1.64-1.369-3.589-2.102-5.586-2.102h-22.625c-2.053,0-3.717,1.664-3.717,3.717v4.311v79.022c0,2.756,2.234,4.989,4.989,4.989
h114.208c2.756,0,4.989-2.234,4.989-4.989v-74.032c0-2.756-2.234-4.989-4.989-4.989H150.813z"/>
<path style="fill:none;stroke:#000000;stroke-miterlimit:10;" d="M223.563,216.111h-32.308c-1.44,0-2.608-1.168-2.608-2.608l0,0
c0-1.44,1.168-2.608,2.608-2.608h32.308c1.44,0,2.608,1.168,2.608,2.608l0,0C226.171,214.944,225.003,216.111,223.563,216.111z"
/>
</g>
</g>
</g>
<g>
<path style="fill:#BA6F4C;" d="M416.122,193.9c0,0,5.736,13.49,6.476,17.404c2.267,11.98,13.033,44.926,16.675,49.055
c0,0,6.8,0.729,10.199-1.214c0,0-8.5-53.912-12.142-61.197c-3.643-7.285-6.071-19.185-6.071-19.185L416.122,193.9z"/>
<g>
<path style="fill:#BA6F4C;" d="M448.643,257.27c-3.761-3.129-11.35-1.271-12.673,3.439c-1.077,3.834-1.115,5.994-1.213,6.648
c-0.191,1.279-4.845,9.195-3.953,10.819c0.645,1.176,3.936-2.52,5.583-5.114c0.339-0.534,1.158-0.33,1.214,0.3
c0.312,3.522,1.004,10.567,1.611,11.443c0.813,1.173,3.77,6.141,4.647,5.603c0.877-0.539-0.107-3.998-0.877-6.313
c-0.77-2.315,0.178-7.466,0.178-7.466s3.905,11.755,5.559,13.878c2.464,3.162,3.284-1.999,3.284-1.999
c1.478,1.617,2.914-2.232,2.658-3.441c-0.505-2.389-4.206-10.852-3.551-10.542c0.655,0.31,4.83,9.25,6.725,9.511
c1.895,0.262,0.977-2.965-0.086-5.101c-1.063-2.135-3.942-8.283-3.942-8.283s-0.964-5.823-4.542-12.08
C449.235,258.522,448.687,257.307,448.643,257.27z"/>
<line style="fill:none;stroke:#000000;stroke-linecap:round;stroke-miterlimit:10;" x1="451.925" y1="288.224" x2="446.798" y2="276.303"/>
</g>
<polygon points="413.418,171.436 359.745,169.244 359.745,90.049 387.996,85.97 "/>
<path style="fill:none;stroke:#000000;stroke-linecap:round;stroke-miterlimit:10;" d="M387.158,85.297
c0,0,10.489,24.656,19.479,57.893"/>
<path style="fill:#BA6F4C;" d="M292.214,183.714c0,0,12.629,5.613,41.922-16.488l-12.454-13.857L292.214,183.714z"/>
<path style="fill:#9EC68A;" d="M384.214,127.936c0.534-0.093,3.816,2.98,4.397,3.335c5.588,3.413,10.221,3.909,16.528,5.108
c12.44,2.365,21.211,22.194,22.476,26.336c1.495,4.895,4.09,13.313,5.585,18.208c0,0-2.452,13.622-18.203,13.316l-7.893-16.839
c0,0-8.946,20.171-9.472,21.75c-0.526,1.579,5.438,21.224,5.438,21.224s-47.008,18.067-74.371,8.332
c0,0,9.281-35.462,6.787-36.613c-2.361-1.09-7.265-5.358-1.35-24.866c0,0-9.91-5.788-12.278-21.049
c0,0,12.892-11.927,21.048-12.191C351.061,133.724,384.201,127.938,384.214,127.936z"/>
<path style="fill:#FFFFFF;" d="M364.481,141.486c0,0,3.42,9.077,13.813,3.947c8.561-4.226,11.403-11.118,12.141-13.421
c-3.572-2.039-6.222-4.076-6.222-4.076s-14.568,2.543-26.766,4.335C356.787,133.64,350.898,146.47,364.481,141.486z"/>
<path style="fill:#BA6F4C;" d="M383.293,110.966l0.921,16.97c0,0-7.893,11.971-17.365,10.656l-1.842-10.787L383.293,110.966z"/>
<path style="fill:#BA6F4C;" d="M367.254,307.55l5.642,71.039l1.231,46.393c0,0,10.245,6.626,15.524-3.082
c0,0,9.468-48.149,8.405-64.702c-1.063-16.552,0.804-36.037,0.804-36.037L367.254,307.55z"/>
<path d="M327.186,417.879c0,0-28.935,24.907-26.331,24.907c2.604,0,14.902-1.754,22.859-3.683
c7.957-1.929,11.14-7.016,11.14-7.016l0.579,4.911l5.498-0.702c0,0,6.221-13.155,1.881-20.522
C338.471,408.407,327.186,417.879,327.186,417.879z"/>
<polygon points="331.237,433.315 335.433,436.998 337.458,430.771 334.203,429.982 "/>
<path d="M374.089,418.844c0,0-10.808,32.342-7.161,32.757c6.944,0.789,15.372-6.885,17.976-11.796
c2.604-4.911,2.315-12.629,2.315-12.629l-2.46,5.788h5.064c0,0,2.749-2.631,0.579-15.26
C390.402,417.703,374.089,433.709,374.089,418.844z"/>
<path style="fill:#BA6F4C;" d="M329.356,305.27l-2.17,112.609c0,0,9.983,6.841,15.625-2.105c0,0,11.285-45.254,10.851-61.041
c-0.434-15.786,2.17-34.204,2.17-34.204L329.356,305.27z"/>
<path d="M333.434,207.394c0,0,12.629,23.153,66.829,3.157c0,0,21.048,54.726,4.736,115.767c0,0-46.307,21.575-90.508-4.21
C314.491,322.108,321.858,246.334,333.434,207.394z"/>
<path style="fill:#BA6F4C;" d="M334.136,130.041c0,0-40.08,28.24-43.062,33.151c-2.982,4.911-9.209,16.137-0.439,20.347
s43.588-34.116,52.007-47.096L334.136,130.041z"/>
<path style="fill:none;stroke:#000000;stroke-linecap:round;stroke-miterlimit:10;" d="M298.003,182.136
c0,0,12.498-6.797,27.407-23.811"/>
<path style="fill:none;stroke:#000000;stroke-linecap:round;stroke-miterlimit:10;" d="M336.212,192.546
c0,0,33.18,15.9,53.702-1.64"/>
<line style="fill:none;stroke:#000000;stroke-linecap:round;stroke-miterlimit:10;" x1="407.104" y1="177.4" x2="399.963" y2="166.211"/>
<line style="fill:none;stroke:#000000;stroke-linecap:round;stroke-miterlimit:10;" x1="334.136" y1="167.227" x2="338.17" y2="159.158"/>
<path style="fill:#BA6F4C;" d="M355.721,121.064c-2.254-0.215-4.401,2.147-5.26,1.878c-0.684-0.214-1.606-3.456-1.952-4.766
c1.302-7.606,0.408-13.507-1.375-11.926c-0.095,0.085-0.207,0.262-0.324,0.49c-0.263-2.413-0.948-3.62-1.876-2.797
c-0.254,0.225-0.615,1.069-0.938,1.943c-0.279-2.273-0.952-3.385-1.853-2.587c-0.444,0.393-1.208,2.663-1.539,3.693
c-0.178-0.217-0.439-0.309-0.769-0.419c-1.288-0.429-4.258,10.698-4.401,11.271c-0.143,0.573-3.292,13.024-3.292,13.024
c-1.145,4.508,7.853,6.577,10.501,5.576c2.648-1.002,8.677-7.508,8.677-7.508c-0.007-0.012-0.017-0.023-0.024-0.035
c0.208-0.301,0.512-0.76,0.936-1.45c0.912-1.485,5.528-5.206,5.635-5.528S357.975,121.278,355.721,121.064z"/>
<path d="M357.868,121.922c0.107-0.322,0.107-0.644-2.147-0.859c-2.254-0.215-4.401,2.147-5.26,1.878
c-0.684-0.214-1.606-3.456-1.952-4.766c1.302-7.606,0.408-13.507-1.375-11.926c-0.095,0.085-0.207,0.262-0.324,0.49
c-0.263-2.413-0.948-3.62-1.876-2.797c-0.254,0.225-0.615,1.069-0.938,1.943c-0.131-1.063-0.35-1.859-0.636-2.338
c-0.294,7.765,0.566,19.538,7.459,25.919c0.312-0.327,0.502-0.531,0.502-0.531c-0.007-0.012-0.017-0.023-0.024-0.035
c0.208-0.301,0.512-0.76,0.936-1.45C353.145,125.966,357.761,122.244,357.868,121.922z"/>
<g>
<path d="M381.254,114.189l2.039-3.223l-18.286,16.839l1.395,8.171C373.995,134.735,377.55,122.467,381.254,114.189z"/>
<path style="fill:#BA6F4C;" d="M346.315,95.132c0,0-5.408,30.74,12.426,36.949c17.835,6.209,24.108-23.125,24.146-33.408
c0.039-10.283-4.747-15.674-17.446-16.959C346.655,79.812,346.315,95.132,346.315,95.132z"/>
<path d="M386.849,83.268c-3.649-5.279-9.009-8.183-22.449-7.939c-10.23,0.186-16.651,7.017-18.833,10.563
c-0.707,1.149-2.454,4.396,0.91,7.882c0,0,0.112-2.465,2.387-5.21c0,0,21.288,11.538,29.794,9.844l2.028,7.977
c0,0,2.878-4.084,5.835-5.405C386.521,100.98,390.498,88.548,386.849,83.268z"/>
<path style="fill:#BA6F4C;" d="M390.227,108.652c-1.385,4.013-4.774,6.505-7.57,5.566c-2.796-0.938-3.939-4.952-2.554-8.965
c1.385-4.013,4.774-6.505,7.57-5.566C390.468,100.625,391.612,104.639,390.227,108.652z"/>
<path d="M363.953,108.317c-0.017,1.055-0.574,1.904-1.245,1.894c-0.671-0.009-1.202-0.873-1.185-1.928
c0.017-1.056,0.574-1.904,1.245-1.894C363.439,106.398,363.969,107.261,363.953,108.317z"/>
<path d="M349.889,108.273c0.039,0.993-0.42,1.818-1.025,1.843c-0.606,0.025-1.129-0.759-1.168-1.752
c-0.039-0.993,0.42-1.818,1.025-1.843C349.326,106.495,349.849,107.28,349.889,108.273z"/>
<path style="fill:none;stroke:#000000;stroke-linecap:round;stroke-miterlimit:10;" d="M368.492,100.242
c0,0-4.877,0.719-4.902-3.865"/>
<path style="fill:none;stroke:#000000;stroke-linecap:round;stroke-miterlimit:10;" d="M352.217,98.81
c0,0-1.501,4.161-4.958,1.471"/>
<path d="M363.692,125.333c0,0,2.006-6.359,6.688-2.335L363.692,125.333z"/>
<path style="fill:none;stroke:#000000;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;" d="M387.603,104.909
c0,0-3.021-2.96-5.096,1.96c0,0,1.741,2.105,0.46,3.585c0,0,1.815,0.538,2.83-1.028"/>
<path style="fill:none;stroke:#000000;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;" d="M386.093,106.745
c0,0-0.105-1.13-1.265-2.487"/>
<path style="fill:none;stroke:#000000;stroke-linecap:round;stroke-miterlimit:10;" d="M351.727,112.201
c-0.651,0.724-0.835,1.626-0.595,2.629c0.237,0.991,1.188,1.645,2.192,1.69"/>
</g>
<path style="fill:#FFFFFF;" d="M384.871,114.221c0,1.508-1.222,2.73-2.73,2.73s-2.73-1.222-2.73-2.73
c0-1.508,1.222-2.73,2.73-2.73S384.871,112.714,384.871,114.221z"/>
<path style="fill:none;stroke:#000000;stroke-linecap:round;stroke-miterlimit:10;" d="M339.047,134.601
c0,0,1.052,2.28,3.596,1.842"/>
<path style="fill:none;stroke:#000000;stroke-linecap:round;stroke-miterlimit:10;" d="M340.604,106.992
c0,0,1.907,1.014-1.184,11.933c0,0,1.908,6.907,1.381,8.222c-0.526,1.315-3.157,3.508-3.947,3.596"/>
<path style="fill:none;stroke:#000000;stroke-linecap:round;stroke-miterlimit:10;" d="M345.976,123.814
c0,0-2.631,2.982-2.719,7.104"/>
<path style="fill:none;stroke:#000000;stroke-linecap:round;stroke-miterlimit:10;" d="M343.995,105.886
c0,0,0.621,8.369-0.695,12.907"/>
</g>
<g>
<path style="fill:#DFDFEB;" d="M260.514,455.211c0,3.885-9.053,7.035-20.22,7.035c-11.167,0-20.22-3.15-20.22-7.035
c0-3.885,9.053-7.035,20.22-7.035C251.462,448.176,260.514,451.326,260.514,455.211z"/>
<g>
<g>
<path style="fill:#7E9E6E;" d="M239.841,350.569c0,0-6.115,0.572-4.034-15.26c1.977-15.04,9.559-26.004,10.138-26.048
c0,0,10.904,15.641,4.069,29.205C243.18,352.03,239.841,350.569,239.841,350.569z"/>
<path style="fill:#7E9E6E;" d="M289.89,327.68c0,0-14.006,16.787-48.095,28.614c0,0-0.21-4.475,11.579-15.306
C265.162,330.158,291.521,322.542,289.89,327.68z"/>
<path style="fill:none;stroke:#000000;stroke-linecap:round;stroke-miterlimit:10;" d="M289.89,327.68
c0,0-26.707,4.192-48.095,28.614c0,0-2.101,2.671-2.362,8.417"/>
<path style="fill:#7E9E6E;" d="M189.383,360.174c0,0,14.006,16.787,48.095,28.614c0,0,0.21-4.475-11.579-15.306
C214.111,362.651,187.752,355.035,189.383,360.174z"/>
<path style="fill:none;stroke:#000000;stroke-linecap:round;stroke-miterlimit:10;" d="M189.383,360.174
c0,0,26.707,4.192,48.095,28.614c0,0,2.101,2.671,2.362,8.417"/>
<path style="fill:#7E9E6E;" d="M208.302,394.539c0,0,9.316,7.616,29.532,1.52c0,0-0.261-3.896-7.822-7.508
C222.45,384.94,206.947,390.951,208.302,394.539z"/>
<path style="fill:none;stroke:#000000;stroke-linecap:round;stroke-miterlimit:10;" d="M208.302,394.539
c0,0,15.408-9.081,29.532,1.52c0,0,1.41,1.272,2.044,6.024"/>
<path style="fill:#7E9E6E;" d="M286.289,354.49c0,0-9.787,26.34-43.877,38.166c0,0-1.174-14.868,10.614-25.699
C264.815,356.127,287.92,349.352,286.289,354.49z"/>
<path style="fill:none;stroke:#000000;stroke-linecap:round;stroke-miterlimit:10;" d="M286.289,354.49
c0,0-24.875,5.318-43.877,38.166c0,0-2.181,3.949-2.442,9.695"/>
<path style="fill:#7E9E6E;" d="M234.006,358.642c0,0-2.867,4.662-14.162,3.21c-11.295-1.452-23.507-14.878-31.133-12.441
C188.711,349.411,207.466,342.688,234.006,358.642z"/>
<path style="fill:none;stroke:#000000;stroke-linecap:round;stroke-miterlimit:10;" d="M241.873,422.615
c-2.797-23.384-3.986-51.511-0.712-81.881c0,0,0.435-8.056,4.784-31.473"/>
<path style="fill:none;stroke:#000000;stroke-linecap:round;stroke-miterlimit:10;" d="M191.042,348.805
c0,0,18.964-4.598,42.964,9.837c5.814,3.497,5.55,10.09,5.18,15.228"/>
</g>
<path style="fill:#545FAC;" d="M218.155,412.943l6.576,40.481c0,2.481,7.015,4.491,15.668,4.491s15.669-2.011,15.669-4.491
l6.576-40.481H218.155z"/>
<ellipse cx="240.399" cy="412.944" rx="22.244" ry="4.491"/>
</g>
</g>
<g>
<g>
<path style="fill:#D3DDF7;" d="M264.859,50.713c17.001,0.463,31.067,14.08,32.045,31.059c0.575,9.984-3.294,19.069-9.805,25.478
c-0.962,0.947-1.409,2.297-1.184,3.628l2.445,14.47l-15.045-8.766c-0.857-0.499-1.867-0.683-2.839-0.486
c-3.678,0.742-7.575,0.878-11.591,0.27c-14.863-2.252-26.459-14.556-27.83-29.527C229.228,66.887,245.165,50.176,264.859,50.713z
"/>
</g>
<g>
<path d="M263.031,98.473c-0.457,0-0.807-0.145-1.05-0.434c-0.244-0.289-0.365-0.692-0.365-1.21c0-0.487,0.129-0.883,0.388-1.187
c0.258-0.304,0.601-0.457,1.027-0.457c0.486,0,0.845,0.145,1.073,0.434c0.229,0.29,0.343,0.693,0.343,1.21
c0,0.457-0.122,0.845-0.366,1.165C263.837,98.313,263.488,98.473,263.031,98.473z M262.75,87.101
c-0.035-0.943-0.069-1.91-0.104-2.9c-0.035-0.989-0.061-1.979-0.078-2.969c-0.018-0.989-0.01-2.002,0.026-3.037V66.138h1.146
l0.052,12.057c0,1.035-0.009,2.048-0.026,3.037c-0.018,0.99-0.044,1.98-0.078,2.969c-0.036,0.99-0.07,1.957-0.105,2.9H262.75z"/>
</g>
</g>
</g>
<g id="TEXTS">
<text transform="matrix(1 0 0 1 132.677 250.9341)" style="font-family:'Lexend-Light'; font-size:18.1198px;">No Data</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 14 KiB

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 110 KiB

View File

@ -38,9 +38,9 @@ const LoginNeeded = ({ wrapperClass, itemClass, isVisible = false, onClose }:Log
return (
<ClickOutside wrapperClass="login-needed" handleClass="login-needed-handle" className={`!p-0 !shadow-none`} onClickOutside={handleClickOutside}>
<div className={`${show ? "visible -translate-y-4" : "invisible"} select-none block p-2 transition-transform duration-500 [transition-timing-function:cubic-bezier(0.2,1,0.3,1)] bottom-12 bg-market-input w-full sm:w-96 absolute shadow-lg left-1/2 -translate-x-1/2 rounded-xl z-20`}>
<div className="flex flex-col space-y-6 items-center justify-center p-4 bg-white rounded-lg">
<span className="block text-sm text-center">{translate("login-needed-text")}</span>
<div className={`${show ? "visible -translate-y-4" : "invisible"} select-none block p-2 transition-transform duration-500 [transition-timing-function:cubic-bezier(0.2,1,0.3,1)] bottom-12 bg-market-input w-80 sm:w-96 absolute shadow-lg left-1/2 -translate-x-1/2 rounded-xl z-20`}>
<div className="flex flex-col space-y-4 sm:space-y-6 items-center justify-center p-4 bg-white rounded-lg">
<span className="block text-xs/6 sm:text-sm/7 text-center">{translate("login-needed-text")}</span>
<Button
type='button'
text={translate("user-login-text")}

View File

@ -1,4 +1,4 @@
interface IconProps {
export type IconProps = {
className?: string;
onClick?: (e: React.MouseEvent) => void;
}
@ -796,6 +796,18 @@ export const ArrowUpRightFromSquareSolid: React.FC<IconProps> = ({ className, on
</svg>;
return Icon;
}
export const UpRightFromSquareSolid: 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="M352 0c-12.9 0-24.6 7.8-29.6 19.8s-2.2 25.7 6.9 34.9L370.7 96 201.4 265.4c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0L416 141.3l41.4 41.4c9.2 9.2 22.9 11.9 34.9 6.9s19.8-16.6 19.8-29.6V32c0-17.7-14.3-32-32-32H352zM80 32C35.8 32 0 67.8 0 112V432c0 44.2 35.8 80 80 80H400c44.2 0 80-35.8 80-80V320c0-17.7-14.3-32-32-32s-32 14.3-32 32V432c0 8.8-7.2 16-16 16H80c-8.8 0-16-7.2-16-16V112c0-8.8 7.2-16 16-16H192c17.7 0 32-14.3 32-32s-14.3-32-32-32H80z" />
</svg>;
return Icon;
}
export const SquareArrowUpRightSolid: 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 448 512">
<path d="M384 32c35.3 0 64 28.7 64 64V416c0 35.3-28.7 64-64 64H64c-35.3 0-64-28.7-64-64V96C0 60.7 28.7 32 64 32H384zM160 144c-13.3 0-24 10.7-24 24s10.7 24 24 24h94.1L119 327c-9.4 9.4-9.4 24.6 0 33.9s24.6 9.4 33.9 0l135-135V328c0 13.3 10.7 24 24 24s24-10.7 24-24V168c0-13.3-10.7-24-24-24H160z" />
</svg>;
return Icon;
}
export const TireLight: 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="M96 256C96 167.6 167.6 96 256 96C344.4 96 416 167.6 416 256C416 344.4 344.4 416 256 416C167.6 416 96 344.4 96 256zM381.7 280.2C383.2 272.3 384 264.3 384 256C384 219.2 368.5 186.1 343.7 162.7L305.5 215.4C314.5 226.4 320 240.6 320 256C320 257.4 319.1 258.7 319.9 260L381.7 280.2zM309.9 290.4C301.3 303.9 287.8 313.9 271.9 317.1L271.1 383C316.2 377.5 353.5 349.4 371.8 310.6L309.9 290.4zM239.1 383L239.9 317.1C224.1 313.9 210.6 303.9 202.1 290.4L140.2 310.6C158.5 349.4 195.7 377.5 239.1 383V383zM192.1 260C192 258.7 192 257.4 192 256C192 240.6 197.5 226.4 206.5 215.4L168.3 162.7C143.5 186.1 128 219.2 128 255.1C128 264.3 128.8 272.3 130.3 280.2L192.1 260zM232.4 196.5C239.7 193.6 247.7 192 256 192C264.3 192 272.3 193.6 279.6 196.5L317.8 143.9C299.5 133.8 278.4 128 256 128C233.6 128 212.5 133.8 194.2 143.9L232.4 196.5zM256 224C238.3 224 224 238.3 224 256C224 273.7 238.3 288 256 288C273.7 288 288 273.7 288 256C288 238.3 273.7 224 256 224zM512 256C512 397.4 397.4 512 256 512C114.6 512 0 397.4 0 256C0 114.6 114.6 0 256 0C397.4 0 512 114.6 512 256zM256 32C132.3 32 32 132.3 32 256C32 379.7 132.3 480 256 480C379.7 480 480 379.7 480 256C480 132.3 379.7 32 256 32z" />
@ -1096,6 +1108,12 @@ export const MessagesSolid: React.FC<IconProps> = ({ className, onClick }) => {
</svg>;
return Icon;
}
export const MessageSolid: 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="M64 0C28.7 0 0 28.7 0 64V352c0 35.3 28.7 64 64 64h96v80c0 6.1 3.4 11.6 8.8 14.3s11.9 2.1 16.8-1.5L309.3 416H448c35.3 0 64-28.7 64-64V64c0-35.3-28.7-64-64-64H64z" />
</svg>;
return Icon;
}
export const InboxLight: 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="M508.3 304.9l-61.25-248.7C443.5 42 430.7 31.1 416 31.1H96c-14.69 0-27.47 10-31.03 24.25L3.715 304.9C1.248 314.9 0 325.2 0 335.5v96.47c0 26.51 21.49 48 48 48h416c26.51 0 48-21.49 48-48v-96.47C512 325.2 510.8 314.9 508.3 304.9zM96 64h319.1l55.18 224H368c-6.062 0-11.59 3.438-14.31 8.844L326.1 352H185.9L158.3 296.8C155.6 291.4 150.1 288 144 288H40.84L96 64zM480 432c0 8.822-7.178 16-16 16h-416C39.18 448 32 440.8 32 432v-96.47C32 330.3 33.04 325.2 33.88 320h100.2l27.58 55.16C164.4 380.6 169.9 384 176 384h160c6.062 0 11.59-3.438 14.31-8.844L377.9 320h100.2C478.1 325.2 480 330.3 480 335.5V432z" />
@ -1560,19 +1578,25 @@ export const WaterLight: React.FC<IconProps> = ({ className, onClick }) => {
}
export const ArrowRotateLeftLight: 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 xmlns="http://www.w3.org/2000/svg" d="M480 256c0 123.5-100.4 223.9-223.9 223.9c-69.41 0-133.9-31.3-176.7-86.05c-5.438-6.938-4.203-17 2.75-22.44c6.984-5.531 17.03-4.25 22.47 2.75C141.3 421.1 196.5 448 256 448c105.9 0 192-86.13 192-192s-86.13-192-192-192C187.1 64 124.5 100.7 90.15 160H176C184.8 160 192 167.2 192 176S184.8 192 176 192h-128C39.16 192 32 184.8 32 176v-128C32 39.16 39.16 32 48 32S64 39.16 64 48v93.56C104.4 73.87 176.6 32.11 256.1 32.11C379.6 32.11 480 132.5 480 256z" />
<path d="M480 256c0 123.5-100.4 223.9-223.9 223.9c-69.41 0-133.9-31.3-176.7-86.05c-5.438-6.938-4.203-17 2.75-22.44c6.984-5.531 17.03-4.25 22.47 2.75C141.3 421.1 196.5 448 256 448c105.9 0 192-86.13 192-192s-86.13-192-192-192C187.1 64 124.5 100.7 90.15 160H176C184.8 160 192 167.2 192 176S184.8 192 176 192h-128C39.16 192 32 184.8 32 176v-128C32 39.16 39.16 32 48 32S64 39.16 64 48v93.56C104.4 73.87 176.6 32.11 256.1 32.11C379.6 32.11 480 132.5 480 256z" />
</svg>;
return Icon;
}
export const ArrowRotateLeftRegular: 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 xmlns="http://www.w3.org/2000/svg" d="M40 16C53.25 16 64 26.75 64 40v102.1C103.7 75.57 176.3 32.11 256.1 32.11C379.6 32.11 480 132.5 480 256s-100.4 223.9-223.9 223.9c-52.31 0-103.3-18.33-143.5-51.77c-10.19-8.5-11.56-23.62-3.062-33.81c8.5-10.22 23.66-11.56 33.81-3.062C174.9 417.5 214.9 432 256 432c97.03 0 176-78.97 176-176S353 80 256 80c-66.54 0-126.8 38.28-156.5 96H200C213.3 176 224 186.8 224 200S213.3 224 200 224h-160C26.75 224 16 213.3 16 200v-160C16 26.75 26.75 16 40 16z" />
<path d="M40 16C53.25 16 64 26.75 64 40v102.1C103.7 75.57 176.3 32.11 256.1 32.11C379.6 32.11 480 132.5 480 256s-100.4 223.9-223.9 223.9c-52.31 0-103.3-18.33-143.5-51.77c-10.19-8.5-11.56-23.62-3.062-33.81c8.5-10.22 23.66-11.56 33.81-3.062C174.9 417.5 214.9 432 256 432c97.03 0 176-78.97 176-176S353 80 256 80c-66.54 0-126.8 38.28-156.5 96H200C213.3 176 224 186.8 224 200S213.3 224 200 224h-160C26.75 224 16 213.3 16 200v-160C16 26.75 26.75 16 40 16z" />
</svg>;
return Icon;
}
export const RotateRightSolid: 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="M463.5 224H472c13.3 0 24-10.7 24-24V72c0-9.7-5.8-18.5-14.8-22.2s-19.3-1.7-26.2 5.2L413.4 96.6c-87.6-86.5-228.7-86.2-315.8 1c-87.5 87.5-87.5 229.3 0 316.8s229.3 87.5 316.8 0c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0c-62.5 62.5-163.8 62.5-226.3 0s-62.5-163.8 0-226.3c62.2-62.2 162.7-62.5 225.3-1L327 183c-6.9 6.9-8.9 17.2-5.2 26.2s12.5 14.8 22.2 14.8H463.5z" />
</svg>;
return Icon;
}
export const WavePulseSolid: 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 640 512">
<path xmlns="http://www.w3.org/2000/svg" d="M319.1 0c14.8-.4 27.9 9.3 31.8 23.6l74 271.2 17.7-35.4c10.8-21.7 33-35.4 57.2-35.4H608c17.7 0 32 14.3 32 32s-14.3 32-32 32H499.8L444.6 398.3c-5.9 11.9-18.6 18.8-31.8 17.5s-24.2-10.6-27.7-23.4L323.7 167.3 255.3 486.7c-3.1 14.4-15.5 24.8-30.2 25.3s-27.8-9.1-31.8-23.2L135.9 288H32c-17.7 0-32-14.3-32-32s14.3-32 32-32H135.9c28.6 0 53.7 18.9 61.5 46.4L219.6 348 288.7 25.3C291.8 10.9 304.4 .4 319.1 0z" />
<path d="M319.1 0c14.8-.4 27.9 9.3 31.8 23.6l74 271.2 17.7-35.4c10.8-21.7 33-35.4 57.2-35.4H608c17.7 0 32 14.3 32 32s-14.3 32-32 32H499.8L444.6 398.3c-5.9 11.9-18.6 18.8-31.8 17.5s-24.2-10.6-27.7-23.4L323.7 167.3 255.3 486.7c-3.1 14.4-15.5 24.8-30.2 25.3s-27.8-9.1-31.8-23.2L135.9 288H32c-17.7 0-32-14.3-32-32s14.3-32 32-32H135.9c28.6 0 53.7 18.9 61.5 46.4L219.6 348 288.7 25.3C291.8 10.9 304.4 .4 319.1 0z" />
</svg>;
return Icon;
}
@ -1852,4 +1876,28 @@ export const UnlockSolid: React.FC<IconProps> = ({ className, onClick }) => {
</svg>;
return Icon;
}
export const ShopLockSolid: 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 640 512">
<path d="M36.8 192H449.6c20.2-19.8 47.9-32 78.4-32c30.5 0 58.1 12.2 78.3 31.9c18.9-1.6 33.7-17.4 33.7-36.7c0-7.3-2.2-14.4-6.2-20.4L558.2 21.4C549.3 8 534.4 0 518.3 0H121.7c-16 0-31 8-39.9 21.4L6.2 134.7c-4 6.1-6.2 13.2-6.2 20.4C0 175.5 16.5 192 36.8 192zM384 224H320V384H128V224H64V384v80c0 26.5 21.5 48 48 48H336c26.5 0 48-21.5 48-48V384 352 224zm144 16c17.7 0 32 14.3 32 32v48H496V272c0-17.7 14.3-32 32-32zm-80 32v48c-17.7 0-32 14.3-32 32V480c0 17.7 14.3 32 32 32H608c17.7 0 32-14.3 32-32V352c0-17.7-14.3-32-32-32V272c0-44.2-35.8-80-80-80s-80 35.8-80 80z" />
</svg>;
return Icon;
}
export const PaletteSolid: 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="M512 256c0 .9 0 1.8 0 2.7c-.4 36.5-33.6 61.3-70.1 61.3H344c-26.5 0-48 21.5-48 48c0 3.4 .4 6.7 1 9.9c2.1 10.2 6.5 20 10.8 29.9c6.1 13.8 12.1 27.5 12.1 42c0 31.8-21.6 60.7-53.4 62c-3.5 .1-7 .2-10.6 .2C114.6 512 0 397.4 0 256S114.6 0 256 0S512 114.6 512 256zM128 288c0-17.7-14.3-32-32-32s-32 14.3-32 32s14.3 32 32 32s32-14.3 32-32zm0-96c17.7 0 32-14.3 32-32s-14.3-32-32-32s-32 14.3-32 32s14.3 32 32 32zM288 96c0-17.7-14.3-32-32-32s-32 14.3-32 32s14.3 32 32 32s32-14.3 32-32zm96 96c17.7 0 32-14.3 32-32s-14.3-32-32-32s-32 14.3-32 32s14.3 32 32 32z" />
</svg>;
return Icon;
}
export const BagShoppingSolid: 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 448 512">
<path d="M160 112c0-35.3 28.7-64 64-64s64 28.7 64 64v48H160V112zm-48 48H48c-26.5 0-48 21.5-48 48V416c0 53 43 96 96 96H352c53 0 96-43 96-96V208c0-26.5-21.5-48-48-48H336V112C336 50.1 285.9 0 224 0S112 50.1 112 112v48zm24 96c-13.3 0-24-10.7-24-24s10.7-24 24-24s24 10.7 24 24s-10.7 24-24 24zm200-24c0 13.3-10.7 24-24 24s-24-10.7-24-24s10.7-24 24-24s24 10.7 24 24z" />
</svg>;
return Icon;
}
export const ChartSimpleSolid: 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 448 512">
<path d="M160 80c0-26.5 21.5-48 48-48h32c26.5 0 48 21.5 48 48V432c0 26.5-21.5 48-48 48H208c-26.5 0-48-21.5-48-48V80zM0 272c0-26.5 21.5-48 48-48H80c26.5 0 48 21.5 48 48V432c0 26.5-21.5 48-48 48H48c-26.5 0-48-21.5-48-48V272zM368 96h32c26.5 0 48 21.5 48 48V432c0 26.5-21.5 48-48 48H368c-26.5 0-48-21.5-48-48V144c0-26.5 21.5-48 48-48z" />
</svg>;
return Icon;
}

View File

@ -1,8 +1,8 @@
import { ApolloClient, InMemoryCache } from "@apollo/client";
const cmsAddress = () => {
const serverAddress = process.env.NODE_ENV === 'production' ? `https://backend.flierland.ca` : `http://192.168.1.105:8055`;
// const serverAddress = process.env.NODE_ENV === 'production' ? `http://192.168.1.105:8055` : `http://192.168.1.105:8055`;
// const serverAddress = process.env.NODE_ENV === 'production' ? `https://backend.flierland.ca` : `http://192.168.1.105:8055`;
const serverAddress = process.env.NODE_ENV === 'production' ? `http://192.168.1.105:8055` : `http://192.168.1.105:8055`;
return serverAddress;
}
const defaultOptions = {

View File

@ -1,4 +1,16 @@
import { hasValue } from "services/general/general";
import { BasketShoppingSolid, BrushSolid, CarMirrorsSolid, GamepadModernSolid, GraduationCapSolid, HandHoldingHeartSolid, HouseChimneySolid, PopcornSolid, ScaleBalancedSolid, StethoscopeSolid, UtensilsSolid } from "components/icons";
import { Billboard } from "common/types/billboard";
interface GetBillboardCatIconProps {
catId: number;
billboard: Billboard;
catIconClass: string;
}
interface CatIconProps {
id: number;
icon: React.ReactNode;
}
export const getHours = ({ billboard }: any) => {
const date = new Date();
@ -59,3 +71,22 @@ export const getHours = ({ billboard }: any) => {
return { hours: finalWeekDays, today: finalWeekDays.indexOf(today), isOpen: isOpen, nextOpenDay: nextOpenDay, daysTillNextOpenDay: Math.abs(daysTillNextOpenDay) <= 1 ? Math.abs(daysTillNextOpenDay) : 7 - Math.abs(daysTillNextOpenDay) };
}
export const getBillboardCatIcon = ({ catId, billboard, catIconClass }: GetBillboardCatIconProps) => {
const catIcons: CatIconProps[] = [
{ id: 8, icon: <BasketShoppingSolid className={catIconClass} /> },
{ id: 9, icon: <HouseChimneySolid className={catIconClass} /> },
{ id: 10, icon: <StethoscopeSolid className={catIconClass} /> },
{ id: 11, icon: <CarMirrorsSolid className={catIconClass} /> },
{ id: 12, icon: <ScaleBalancedSolid className={catIconClass} /> },
{ id: 13, icon: <UtensilsSolid className={catIconClass} /> },
{ id: 14, icon: <BrushSolid className={catIconClass} /> },
{ id: 15, icon: <GamepadModernSolid className={catIconClass} /> },
{ id: 16, icon: <GraduationCapSolid className={catIconClass} /> },
{ id: 17, icon: <PopcornSolid className={catIconClass} /> },
{ id: 18, icon: <HandHoldingHeartSolid className={catIconClass} /> }
];
const currecntCatIcon = catIcons.filter(x => (x.id === Number(catId) || x.id === Number(billboard.billboard_categories[0].billboard_categories_id.parent[0]?.related_billboard_categories_id.id)))[0].icon
return currecntCatIcon;
}

View File

@ -67,22 +67,22 @@ export const iranianLandlineNumberValidationRegex = new RegExp("^\\d{11}$");
// Website general base path generator for both local and production
// 167.99.189.252 live server's ip address
export const basePath = () => {
const basePath = process.env.NODE_ENV === 'production' ? `https://flierland.ca` : `http://localhost:3000`;
// const basePath = process.env.NODE_ENV === 'production' ? `http://localhost:3000` : `http://localhost:3000`;
// const basePath = process.env.NODE_ENV === 'production' ? `https://flierland.ca` : `http://192.168.1.105:3000`;
const basePath = process.env.NODE_ENV === 'production' ? `http://localhost:3000` : `http://localhost:3000`;
return basePath;
}
// Website server address generator for both local and production
export const serverAddress = () => {
const serverAddress = process.env.NODE_ENV === 'production' ? `https://cdn.flierland.ca/assets/` : `http://192.168.1.105:8055/assets/`;
// const serverAddress = process.env.NODE_ENV === 'production' ? `http://192.168.1.105:8055/assets/` : `http://192.168.1.105:8055/assets/`;
// const serverAddress = process.env.NODE_ENV === 'production' ? `https://cdn.flierland.ca/assets/` : `http://192.168.1.105:8055/assets/`;
const serverAddress = process.env.NODE_ENV === 'production' ? `http://192.168.1.105:8055/assets/` : `http://192.168.1.105:8055/assets/`;
return serverAddress;
}
// Website server address generator for both local and production
export const cmsAddress = () => {
const serverAddress = process.env.NODE_ENV === 'production' ? `https://backend.flierland.ca` : `http://192.168.1.105:8055`;
// const serverAddress = process.env.NODE_ENV === 'production' ? `http://192.168.1.105:8055` : `http://192.168.1.105:8055`;
// const serverAddress = process.env.NODE_ENV === 'production' ? `https://backend.flierland.ca` : `http://192.168.1.105:8055`;
const serverAddress = process.env.NODE_ENV === 'production' ? `http://192.168.1.105:8055` : `http://192.168.1.105:8055`;
return serverAddress;
}
@ -104,7 +104,7 @@ export const adminAccessToken = process.env.NODE_ENV === 'production' ? process.
// Strips html tags from text
export const stripHtml = (text) => {
const strippedText = text.replace(/(<([^>]+)>)/gi, '').replace('&nbsp;', '').replace(/&zwnj;/gi, ' ');
const strippedText = text.replace(/(<([^>]+)>)/gi, '').replace(/&nbsp;/gi, ' ').replace(/&zwnj;/gi, ' ').replace(/&rsquo;/gi, "");
return strippedText;
}
// Strips html tags from text
@ -124,6 +124,16 @@ export const hasValue = (value) => {
return checkedValue;
}
// one day to milliseconds
export const oneDay = () => {
let milliseconds = 86400000; // 24 * 60 * 60 * 1000 milliseconds in a day
let seconds = 86400; // 24 * 60 * 60 mseconds in a day
let minutes = 1440; // 24 * 60 minutes in a day
let hours = 24; // 24 hours in a day
return { milliseconds: milliseconds, seconds: seconds, minutes: minutes, hours: hours };
}
// get weekday
export const getWeekDay = (date) => {
let weekday = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
@ -173,6 +183,21 @@ export const getDatesBetween = (start, end) => {
return datesBetween;
}
// get dates weeks two dates
export const getWeeksBetween = (start, end) => {
const datesBetween = [];
let startDate = new Date(mtd(start));
let endDate = new Date(mtd(end));
const oneDay = 86400000; // 24 * 60 * 60 * 1000 milliseconds in a day
while (endDate >= startDate) {
datesBetween.push(new Date(endDate));
endDate = new Date(endDate.getTime() - (7 * oneDay));
}
return datesBetween.reverse();
}
// get milliseconds & returns HH:MM string
export const getDateHours = (milliseconds) => {
const date = new Date(milliseconds);
@ -773,7 +798,7 @@ export const getCatData = (cat) => {
// Get the array of visited page IDs from local storage
export const getVisitedPages = () => {
const visitedPages = localStorage.getItem('visitedPages');
let finalVisitedObject = "";
let finalVisitedObject = { ads: [], billboards: [], billboard_news: [], articles: [], wiki: [] };
if (hasValue(visitedPages)) {
// ads
if (!hasValue(JSON.parse(visitedPages).ads) || (hasValue(JSON.parse(visitedPages).ads) && JSON.parse(visitedPages).ads.length > 1000)) {
@ -786,7 +811,10 @@ export const getVisitedPages = () => {
}));
}
// billboards
if (!hasValue(JSON.parse(visitedPages).billboards) || (hasValue(JSON.parse(visitedPages).billboards) && JSON.parse(visitedPages).billboards.length > 1000)) {
if (
!hasValue(JSON.parse(visitedPages).billboards) ||
(hasValue(JSON.parse(visitedPages).billboards) && JSON.parse(visitedPages).billboards.length > 1000)
) {
localStorage.setItem('visitedPages', JSON.stringify({
ads: JSON.parse(visitedPages).ads ?? [],
billboards: [],
@ -836,7 +864,7 @@ export const getVisitedPages = () => {
// Add a new page ID to the array and update local storage
export const addVisitedPage = (pageId, pageType) => {
const visitedPages = getVisitedPages();
visitedPages[pageType].push(pageId);
visitedPages[pageType].push({ id: pageId, time: Date.now()});
localStorage.setItem('visitedPages', JSON.stringify(visitedPages));
};

View File

@ -0,0 +1,275 @@
import { gql } from "@apollo/client";
import { UUID } from "crypto";
import { basePath } from "services/general/general";
export const getBillboardFullDataQuery = (id: string | string[] | undefined, userId: string) => `
Billboards (
filter: {
user_created: { id: {_eq: "${userId}"} },
id : { _eq: "${id}" }
},
sort: ["sort", "-date_created"],
limit: -1
)
{
id
date_created
status
visits
translations {
languages_code {
code
}
title
body
meta_description
slogan
faq
}
city {
name
English_name
state {
name
English_name
Initials
}
country {
name
}
}
address
phone_number
email
website
instagram
facebook
twitter
youtube
working_hours
temporarily_closed
brand_color
dashboard_theme
dashboard_color
billboard_categories {
billboard_categories_id {
id
translations {
languages_code {
code
}
name
}
parent {
related_billboard_categories_id {
id
translations {
languages_code {
code
}
name
}
}
}
}
}
cover_photo {
id
title
description
filename_download
width
height
type
}
logo {
id
title
description
filename_download
width
height
type
}
gallery {
directus_files_id {
id
description
filename_download
width
height
}
}
seats_data {
id
directus_files_id {
id
description
filename_download
width
height
}
persian_name
english_name
types
}
}
`
export const getBillboardRatingsQuery = (id: string | string[] | undefined) => `
billboard_ratings_aggregated(filter: { status: { _eq: "published" }, billboard_id: { id: { _eq: "${id}" } } }) {
avg {
service_quality
}
countDistinct {
id
}
}
billboard_ratings(filter: { status: { _eq: "published" }, billboard_id: { id: { _eq: "${id}" } } }) {
id
date_created
user {
id
first_name
last_name
city {
name
English_name
state {
name
English_name
}
}
avatar {
id
width
height
}
}
billboard_id {
id
}
service_quality
translations {
languages_code {
code
}
title
review
}
pics {
directus_files_id {
id
title
description
filename_download
width
height
type
}
}
recommend
useful {
directus_users_id {
id
}
}
not_useful {
directus_users_id {
id
}
}
}
`
export const billboardProductsAggregatedQuery = (id: string | string[] | undefined) => `
billboard_products_aggregated (
filter: { status: { _eq: "published" }, billboard: { Billboards_id: { id: { _eq: "${id}" }}} }
)
{
countDistinct {
id
}
}
`
export const billboardOrdersQuery = (id: string | string[] | undefined) => `
billboard_orders (
filter: { billboard_id: { _eq: ${id} } },
sort: ["sort", "-date_created"]
)
{
id
status
date_created
order_status
billboard_id
billboard_order_id
user_id
payment_id
reservation_id
ordered_products
total_price
translations {
languages_code {
code
}
note
}
}
`
export const billboardNewsAggregatedQuery = (id: string | string[] | undefined) => `
billboard_news_aggregated (
filter: { status: { _eq: "published" }, billboards: { Billboards_id: { id: { _eq: "${id}" }}} }
)
{
countDistinct {
id
}
}
`
// update billboard item
export const updateBillboardItem = (id: string | string[] | undefined, payload: any) => gql`
mutation {
update_Billboards_item (
id: ${id},
data: ${payload}
)
{
id
}
}
`
// update billboard post request
export const updateBillboard = async ({ id, payload }: { id: string | string[] | undefined, payload: any }) => {
try {
const response = await fetch(`${basePath()}/api/billboard/update-billboard`, {
method: "POST",
headers: {
"Access-Control-Allow-Origin": "*",
'Content-Type': 'application/json',
},
body: JSON.stringify({
id: id,
payload: payload
}),
});
// Check if the response is successful
if (!response.ok) {
throw new Error(`Failed to update billboard: ${response.statusText}`);
}
const updateResponse = await response.json();
return updateResponse; // Return the successful response
} catch (error) {
console.error("Error updating billboard:", error);
// You can return a custom error object or just re-throw the error
throw error;
}
};

View File

@ -0,0 +1,19 @@
import { gql } from "@apollo/client";
// get currencies
export const getGlobalCurrencies = `
currencies
(
filter: {
status: { _eq: "published" }
},
),
{
id
persian_name
english_name
abbreviation
sign
}
`

View File

@ -27,11 +27,11 @@ export const updateAdViews = (views_count: number, id: string | string[] | undef
`
// update billboard views
export const updateBillboardViews = (views_count: number, id: string | string[] | undefined) => gql`
export const updateBillboardViews = (visits: string[], id: string | string[] | undefined) => gql`
mutation {
update_Billboards_item (
id: ${id},
data: { views_count: ${views_count + 1} }
data: { visits: [${visits}] }
)
{
id

View File

@ -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, fetchPrivateData, fetchPublicData, getLocaleTr, getSmartTime, getVisitedPages, hasValue, useGetRouter } from "services/general/general";
import { addVisitedPage, adminAccessToken, adminFetchPrivateData, basePath, fetchPrivateData, fetchPublicData, getLocaleTr, getSmartTime, getVisitedPages, hasValue, oneDay, useGetRouter } from "services/general/general";
import { CalendarDaysSolid, CopySolid, EllipsisSolid, EmptyHeart, EyeSolid, FilledHeart, ImageSharpSolid, PercentageSolid, ReplySolid, ShareNodesSolid, SplitSolid, Telegram, Whatsapp, XMark, XTwitter } from "components/icons";
import Gallery from "components/gallery/gallery";
import Image from "components/image/image";
@ -313,7 +313,7 @@ const NewsDetails: React.FunctionComponent<NewsDetailsProps> = ({ billboardId, b
const pageType: any = "billboard_news";
const visitedPages = getVisitedPages();
if (hasValue(newsData) && !visitedPages[pageType].includes(String(newsData.id))) {
if (hasValue(newsData) && !(visitedPages["billboard_news"]).some((x: { id: string, time: number }) => (x.id === String(newsData.id)) && (x.time > (Date.now() - oneDay().milliseconds * 30)))) {
// If not, update the page views count and mark the page as visited
addVisitedPage(newsData.id, pageType);
updateViews();

View File

@ -0,0 +1,169 @@
import { Billboard } from 'common/types/billboard';
import { getLocaleTr, hasValue, mtd, serverAddress, stripHtml, url, useGetRouter } from 'services/general/general';
import useTranslate from 'services/translation/translation';
import Image from 'components/image/image';
import { ArrowUpRightFromSquareSolid, BasketShoppingSolid, BellSolid, BoxArchiveSolid, ImageSharpSolid, MessagesSolid, PenSolid, SquareArrowUpRightSolid, UpRightFromSquareSolid } from 'components/icons';
import StarRating from 'components/rating/star-rating';
import { useMemo } from 'react';
import InnerLoading from 'components/loading/inner-loading';
import { getBillboardCatIcon } from 'services/billboard/general';
import Button from 'components/button/button';
import Link from 'components/link/link';
interface BillboardOverviewProps {
data: Billboard;
ratings: any;
products: number;
news: number;
}
const BillboardOverview: React.FunctionComponent<BillboardOverviewProps> = ({ data, ratings, products, news }) => {
// states
// const [value, setValue] = useState<string[]>([]);
// variables
const { locale, query } = useGetRouter();
const translate = useTranslate();
const billboardTitle = data ? getLocaleTr(data, locale).title : "";
const highlighIconClass = "inline-block size-9 fill-current text-[var(--brand-color)] shrink-0 bg-[var(--light-brand-color)] rounded-full p-2";
const highlights = [
{ id: 1, title: translate("billboard-tabs-products"), icon: <BasketShoppingSolid className={highlighIconClass} />, value: products, link: "products" },
{ id: 2, title: translate("billboard-tabs-updates"), icon: <BellSolid className={highlighIconClass} />, value: news, link: "news" },
{ id: 3, title: translate("billboard-reviews-title"), icon: <MessagesSolid className={highlighIconClass} />, value: ratings.count, link: "reviews" },
]
// methods
// useEffects
// useEffect(() => {
// }, []);
// console.log(data);
// return
return (
<section
style={{
"--image-bg": hasValue(data.cover_photo) ? `url("${serverAddress()}${data.cover_photo?.id}/${data.cover_photo?.filename_download}")` : `url("${serverAddress()}${data.gallery[0].directus_files_id?.id}/${data.gallery[0].directus_files_id?.filename_download}")`,
} as React.CSSProperties}
className="block w-full px-0 rounded-b-[2.25rem] md:col-span-2"
>
{/* cover photo */}
<div
className={`block w-full relative lg:mt-[5px] h-48 sm:h-80 lg:h-[375px] bg-white
${(data.cover_photo) ?
"[background-image:var(--image-bg)] [background-size:cover] [background-position:50%_50%] md:[background-position:50%_55%]"
:
"[background-image:var(--image-bg)] md:[background-image:url('/pics/patterns/greyzz.webp')] max-md:[background-size:cover] [background-position:50%_50%] md:[background-position:50%_50%] md:bg-blend-multiply" }
`}
>
<ImageSharpSolid className={`${(data.cover_photo) ? "hidden" : "hidden md:inline-block"} absolute top-16 sm:top-32 lg:top-36 left-1/2 -translate-x-1/2 -translate-y-1/2 w-12 sm:w-24 md:w-36 fill-gray-200 rounded-lg md:rounded-xl bg-white px-1 md:px-2`} />
{/* Link to billboard */}
<Link
href={`/billboard/${data.id}/${url(getLocaleTr(data, locale).title)}`}
target="_blank"
className="reactive-button flex items-center justify-center w-max absolute top-4 rtl:right-4 ltr:left-4 mx-auto rounded-lg text-xs text-[var(--brand-color)] capitalize border-2 border-transparent ring-2 ring-white"
>
<div className="bg-white py-[6px] px-3 rounded-md">
<SquareArrowUpRightSolid className="inline-block size-4 fill-current rtl:ml-2 ltr:mr-2" />
{translate("visit-billboard")}
</div>
</Link>
</div>
<div className="block md:flex md:flex-col w-[calc(100%_-_2rem)] relative bg-white rounded-[2.25rem] pt-14 pb-2 px-4 -mt-10 mx-4 sm:pt-20 md:pt-10 md:pb-8 md:px-8 shadow-bot">
<div className="flex items-start md:space-x-12 rtl:space-x-reverse">
{/* logo */}
<div className={`block max-md:absolute relative max-md:left-1/2 max-md:-translate-x-1/2 max-md:-top-10 size-20 sm:size-24 md:size-48 bg-white rounded-full ring-[6px] ring-white md:ring-[var(--light-brand-color)] md:border-2 md:border-white max-md:mx-auto sm:mt-0 shrink-0 overflow-hidden ${data.temporarily_closed ? "grayscale" : ""}`}>
{hasValue(data.logo) ?
<Image
src={`${data.logo.id}/${data.logo.filename_download}`}
alt={billboardTitle + " logo"}
width={data.logo.width}
height={data.logo.height}
quality={75}
ar={[1 / 1, 1 / 1, 1 / 1, 1 / 1]}
imageSizes={[250, 250, 250, 250]}
priority={true}
noPreload={true}
fetchPriority="low"
className="rounded-full"
figureClass="rounded-full w-full"
/>
:
<span className="block size-full aspect-1/1 md:aspect-1/1 bg-gray-100 lg:bg-gray-50 rounded-full sm:border-[6px] border-gray-100">
<ImageSharpSolid className="inline-block absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 size-12 xl:size-24 fill-gray-200" />
</span>
}
</div>
<div className="flex flex-col">
<div className="flex flex-col md:items-start">
{/* title */}
<h1 className="block max-md:text-center text-xl/9 md:text-2xl/10 xl:text-3xl px-2 md:px-0 mb-3 capitalize">{billboardTitle}</h1>
{/* rating */}
<div className="flex items-center justify-center max-md:mx-auto mt-2 md:mt-2 space-x-2 rtl:space-x-reverse">
<StarRating rating={ratings.totalRating} className="text-primary -mt-[2px]" starClass="size-4 shrink-0" />
</div>
{/* mobile category */}
<span className="flex md:hidden items-center justify-center w-max py-[6px] px-3 mt-5 mb-5 max-md:mx-auto rounded-lg text-xs text-[var(--brand-color)] bg-[var(--light-brand-color)] capitalize">
{getBillboardCatIcon({ catId: Number(data.billboard_categories[0].billboard_categories_id.id), billboard: data, catIconClass: "inline-block size-4 fill-current text-current shrink-0 rtl:ml-2 ltr:mr-2"})}
{getLocaleTr(data.billboard_categories[0].billboard_categories_id, locale).name}
</span>
</div>
<hr className="block md:hidden w-full mb-1 pb-2 border-t border-dashed border-gray-200/75" />
{/* mobile description */}
<p className="md:hidden w-full text-xs/7 text-center line-clamp-2 px-2 first-letter:capitalize">{stripHtml(getLocaleTr(data, locale).body)}</p>
{/* highlights */}
<div className="grid grid-cols-3 md:w-80 xl:w-96 divide-x rtl:divide-x-reverse divide-[var(--light-brand-color)] max-md:px-2 py-3 rounded-lg mt-3 rtl:md:-mr-10 ltr:md:-ml-10 md:mt-5">
{highlights.map(x => (
<Link key={x.id} href={`/billboard/${data.id}/${x.link}`} className="reactive-button flex flex-col items-center space-y-3 px-2">
{x.icon}
<span className="inline-block text-xl font-extrabold text-gray-500">{x.value}</span>
<span className="inline-block text-xs text-gray-500 capitalize">{x.title}</span>
</Link>
))}
</div>
</div>
</div>
<div className="flex justify-between md:mt-2 px-2 md:space-x-8 md:rtl:space-x-reverse">
<div className="flex flex-col md:max-w-lg">
{/* desktop category */}
<span className="hidden md:flex items-center justify-center w-max py-[6px] px-3 mt-5 mb-4 max-md:mx-auto rounded-lg text-xs text-[var(--brand-color)] bg-[var(--light-brand-color)] capitalize">
{getBillboardCatIcon({ catId: Number(data.billboard_categories[0].billboard_categories_id.id), billboard: data, catIconClass: "inline-block size-4 fill-current text-current shrink-0 rtl:ml-2 ltr:mr-2" })}
{getLocaleTr(data.billboard_categories[0].billboard_categories_id, locale).name}
</span>
{/* desktop description */}
<p className="max-md:hidden w-full text-xs/7 line-clamp-2 first-letter:capitalize">{stripHtml(getLocaleTr(data, locale).body)}</p>
</div>
{/* CTAs */}
<div className="grid grid-cols-2 md:grid-cols-1 gap-4 w-full py-3 mt-3 md:pb-0 md:w-64 xl:w-80 shrink-0">
<Button
type="button"
text={translate("edit")}
className="rounded-xl bg-[var(--brand-color)] text-white text-sm py-2 px-2 shadow-none capitalize"
leftIcon={<PenSolid className="inline-block size-4 fill-current rtl:ml-3 ltr:mr-3" />}
/>
<Button
type="button"
text={translate("archive")}
className="rounded-xl bg-white ring-1 ring-[var(--brand-color)] text-[var(--brand-color)] text-sm py-2 px-2 shadow-none capitalize"
leftIcon={<BoxArchiveSolid className="inline-block size-4 fill-current rtl:ml-3 ltr:mr-3" />}
/>
</div>
</div>
</div>
</section>
)
}
export default BillboardOverview

View File

@ -0,0 +1,155 @@
import { hasValue, useGetRouter } from 'services/general/general';
import useTranslate from 'services/translation/translation';
import { CircleQuestionSolid, DropletSolid, PaletteSolid, RotateRightSolid, ShopLockSolid } from 'components/icons';
import { useEffect, useState } from 'react';
import Button from 'components/button/button';
import BillboardSection from './section';
import Modal from 'components/modal/modal';
import { updateBillboard } from 'services/queries/directus/billboard';
import Input from 'components/input/text';
import Label from 'components/label/label';
interface BrandColorProps {
themeColor: {
light: string;
full: string;
};
brandColor: string;
}
const BrandColor: React.FunctionComponent<BrandColorProps> = ({ themeColor, brandColor }) => {
const defaultColor = hasValue(brandColor) ? brandColor : "#516ec2";
// states
const [updating, setUpdating] = useState(false);
const [selectedColor, setSelectedColor] = useState(defaultColor);
const [customColor, setCustomColor] = useState("");
const [showColorError, setShowColorError] = useState(false);
// variables
const { locale, query, router } = useGetRouter();
const translate = useTranslate();
const colors = [
{ id: 1, value: "#4f46e5" },
{ id: 2, value: "#7939ef" },
{ id: 3, value: "#165ef0" },
{ id: 4, value: "#2e90fb" },
{ id: 5, value: "#006F46" },
{ id: 6, value: "#009688" },
{ id: 7, value: "#ed3224" },
{ id: 8, value: "#e91e63" },
{ id: 9, value: "#f26722" },
{ id: 10, value: "#f0bb4b" },
{ id: 11, value: "#000000" },
{ id: 12, value: "#4d4d4d" },
{ id: 13, value: "#795548" },
];
const payload = `{
brand_color: "${selectedColor}"
}`
// methods
const validateHexColor = (input: string): boolean => {
const hexColorPattern = /^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/;
return hexColorPattern.test(input);
};
const handleCustomColorCode = (code: string) => {
if (validateHexColor(code)) {
setCustomColor(code);
setSelectedColor(code);
setShowColorError(false);
} else {
(showColorError && code.length === 0) ? setShowColorError(false) : setShowColorError(true);
}
}
const handleColorSelection = async () => {
setUpdating(true);
try {
const result = await updateBillboard({ id: query.id, payload: payload });
setUpdating(false);
setCustomColor("");
router.reload();
} catch (error) {
console.error("Update failed:", error);
setUpdating(false);
}
}
// useEffects
useEffect(() => {
(hasValue(brandColor) && brandColor !== selectedColor) && setSelectedColor(brandColor);
}, [brandColor]);
// return
return (
<BillboardSection
title={translate("dashboard-billboard-page-brand-color-title")}
icon={<PaletteSolid className="inline-block size-4 text-[var(--brand-color)] fill-current" />}
style={{
"--selected-color": selectedColor
} as React.CSSProperties}
wrapperClass="md:col-span-2"
>
<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">
<div className="grid grid-cols-7 gap-x-2 gap-y-4 w-80 max-w-full mt-3 relative mb-6 shrink-0">
{colors.map(x => (
<span
key={x.id}
style={{ "--item-color": x.value } as React.CSSProperties}
className={`reactive-button inline-block size-9 shrink-0 bg-[var(--item-color)] border-[3px] border-white ring-[3px] ${selectedColor === x.value ? "ring-[var(--item-color)]" : "ring-white"} lg:hover:ring-[var(--item-color)] rounded-full`}
onClick={() => selectedColor === x.value ? setSelectedColor(defaultColor) : setSelectedColor(x.value)}
></span>
))}
<span
key={14}
className={`reactive-button flex items-center justify-center size-9 shrink-0 bg-[#516ec2] border-[3px] border-white ring-[3px] ${selectedColor === "#516ec2" ? "ring-[#516ec2]" : "ring-white"} rounded-full`}
onClick={() => selectedColor === "#516ec2" ? setSelectedColor(defaultColor) : setSelectedColor("#516ec2")}
>
<RotateRightSolid className="inline-block size-4 fill-current text-white" />
</span>
</div>
<div className="flex flex-col md:space-y-6">
<div className="flex items-center max-md:w-full space-x-4 rtl:space-x-reverse shrink-0">
<Label forId="custom-brand-color-code" className="text-sm font-semibold px-1">{translate("dashboard-billboard-page-brand-color-custom-color-code")} :</Label>
<Input
id="custom-brand-color-code"
type="text"
placeholder="#000000"
value={customColor}
className="block w-24 bg-neutral-50 shadow-inner py-[6px] px-2 rounded-xl [direction:ltr] outline-none text-center"
onInput={handleCustomColorCode}
/>
</div>
{showColorError && <span className="block max-md:w-full text-xs/6 py-1 px-4 rounded-lg bg-error-bg text-error-text mt-4 shrink-0">- {translate("dashboard-billboard-page-brand-color-error")}</span>}
<div className="flex items-center max-md:w-full space-x-4 rtl:space-x-reverse max-md:mt-3 max-md:pt-3 shrink-0">
<span className="text-sm font-semibold px-1">{translate("dashboard-billboard-page-brand-color-selected-color")} :</span>
<span className="inline-block size-8 rounded-full bg-[var(--selected-color)] border-[3px] border-white ring-[3px] ring-[var(--selected-color)]"></span>
</div>
</div>
</div>
<div className="grid grid-cols-2 gap-4 w-full border-t border-dashed border-gray-200/75 pt-4 mt-4">
<Button
type="button"
text={translate("apply-changes")}
className="rounded-xl bg-[var(--brand-color)] text-white text-sm py-2 px-2 shadow-none capitalize"
leftIcon={<DropletSolid className="inline-block size-4 fill-current rtl:ml-3 ltr:mr-3" />}
loading={updating}
onClick={handleColorSelection}
/>
<Button
type="button"
text={translate("reset")}
className="rounded-xl bg-white ring-1 ring-[var(--brand-color)] text-[var(--brand-color)] text-sm py-2 px-2 shadow-none capitalize"
leftIcon={<RotateRightSolid className="inline-block size-4 fill-current rtl:ml-3 ltr:mr-3" />}
onClick={() => setSelectedColor(defaultColor)}
/>
</div>
</BillboardSection>
)
}
export default BrandColor

View File

@ -0,0 +1,77 @@
import { useGetRouter } from 'services/general/general';
import useTranslate from 'services/translation/translation';
import { BlockBrickSolid, BrushSolid, ShopLockSolid } from 'components/icons';
import { useEffect, useState } from 'react';
import Button from 'components/button/button';
import BillboardSection from './section';
import Modal from 'components/modal/modal';
import { updateBillboard } from 'services/queries/directus/billboard';
import Select from 'components/select/select';
type Colors = "brand" | "system" | "custom";
interface DashboardColorProps {
dashboardTheme: Colors;
themeColor: {
light: string;
full: string;
};
}
const DashboardColor: React.FunctionComponent<DashboardColorProps> = ({ themeColor, dashboardTheme }) => {
const translate = useTranslate();
const dashboardThemeOptions = [
{ code: "brand", label: translate("dashboard-billboard-page-dashboard-color-brand-label") },
{ code: "system", label: translate("dashboard-billboard-page-dashboard-color-system-label") }
];
const getTheme = dashboardThemeOptions.filter(x => x.code === dashboardTheme)[0].label;
// states
const [updating, setUpdating] = useState(false);
const [dashboardColor, setDashboardColor] = useState(getTheme);
// variables
const { locale, query } = useGetRouter();
// methods
const handleThemeSelection = async (theme: Colors) => {
const payload = `{
dashboard_theme: "${dashboardThemeOptions.filter(x => x.label === theme)[0].code}"
}`
try {
const result = await updateBillboard({ id: query.id, payload: payload });
setDashboardColor(theme);
} catch (error) {
console.error("Update failed:", error);
}
}
// useEffects
useEffect(() => {
getTheme !== dashboardColor && setDashboardColor(getTheme);
}, [dashboardTheme]);
// return
return (
<BillboardSection
title={translate("dashboard-billboard-page-dashboard-color-title")}
icon={<BrushSolid className="inline-block size-4 text-[var(--brand-color)] fill-current" />}
>
<p className="block text-xs/6">{translate("dashboard-billboard-page-dashboard-color-text")}</p>
<Select
id="news-cats-list"
noLabel
label=""
value={dashboardColor}
options={dashboardThemeOptions.map(x => x.label)}
onChange={(theme) => handleThemeSelection(theme as any)}
labelClass="text-sm px-1 font-semibold text-secondary-light capitalize"
className="inline-block w-full px-2 rounded-lg bg-white capitalize text-sm py-1"
wrapperClass="block w-52 bg-gray-100/75 rounded-xl py-2 px-2 mt-4"
/>
</BillboardSection>
)
}
export default DashboardColor

View File

@ -0,0 +1,53 @@
import { useGetRouter } from 'services/general/general';
import useTranslate from 'services/translation/translation';
import { ArrowLeftSolid, BellSolid, BoxOpenSolid, CircleDollarSolid, CircleQuestionSolid, MessagesSolid, UserTieHairSolid} from 'components/icons';
import Link from 'components/link/link';
interface BillboardProductsNewsProps {
}
const BillboardProductsNews: React.FunctionComponent<BillboardProductsNewsProps> = ({ }) => {
// states
// const [value, setValue] = useState<string[]>([]);
// variables
const { locale, query } = useGetRouter();
const translate = useTranslate();
const highlighIconClass = "inline-block size-9 fill-current text-[var(--brand-color)] shrink-0 bg-[var(--light-brand-color)] rounded-xl p-2";
const parts = [
{ id: 1, title: translate("dashboard-billboard-page-products-management"), icon: <BoxOpenSolid className={highlighIconClass} />, value: `/dashboard/billboards/${query.id}/products` },
{ id: 2, title: translate("dashboard-billboard-page-news-management"), icon: <BellSolid className={highlighIconClass} />, value: `/dashboard/billboards/${query.id}/news` },
{ id: 3, title: translate("dashboard-billboard-page-faq-management"), icon: <CircleQuestionSolid className={highlighIconClass} />, value: `/dashboard/billboards/${query.id}/faqs` },
{ id: 4, title: translate("dashboard-billboard-page-service-providers-management"), icon: <UserTieHairSolid className={highlighIconClass} />, value: `/dashboard/billboards/${query.id}/service-providers` },
{ id: 5, title: translate("dashboard-billboard-page-billing-management"), icon: <CircleDollarSolid className={highlighIconClass} />, value: `/dashboard/billboards/${query.id}/billing` },
{ id: 6, title: translate("dashboard-billboard-page-reviews-management"), icon: <MessagesSolid className={highlighIconClass} />, value: `/dashboard/billboards/${query.id}/reviews` },
]
// methods
// useEffects
// useEffect(() => {
// }, []);
// 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">
{/* products & news management CTAs */}
{parts.map(x => (
<Link key={x.id} href={x.value} className="reactive-button bg-white flex items-center justify-between px-4 py-3 shadow-bot rounded-xl">
<div className="flex items-center space-x-4 rtl:space-x-reverse">
{x.icon}
<span className="inline-block text-sm text-text-color capitalize">{x.title}</span>
</div>
<ArrowLeftSolid className="inline-block size-4 text-[var(--brand-color)] fill-current ltr:rotate-180" />
</Link>
))}
</section>
)
}
export default BillboardProductsNews

View File

@ -0,0 +1,94 @@
import { adminAccessToken, adminFetchPrivateData, hasValue, mtd, useGetRouter } from 'services/general/general';
import useTranslate from 'services/translation/translation';
import { ArrowLeftSolid, BagShoppingSolid, BasketShoppingSolid, BlockBrickSolid, ReceiptSolid, ShopLockSolid } from 'components/icons';
import { useEffect, useState } from 'react';
import Button from 'components/button/button';
import BillboardSection from './section';
import { BillboardOrdersItem } from 'common/types/billboard';
import useSWR from 'swr';
import { billboardOrdersQuery } from 'services/queries/directus/billboard';
import InnerLoading from 'components/loading/inner-loading';
import { UUID } from 'crypto';
interface SalesProps {
}
const Sales: React.FunctionComponent<SalesProps> = ({ }) => {
// states
const [updating, setUpdating] = useState(false);
// 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));
ordersError && console.log(ordersError);
const ordersList: BillboardOrdersItem[] = orders?.data.billboard_orders;
let customers: UUID[] = [];
ordersList?.map(x => x.user_id).map(x => customers.indexOf(x) === -1 && customers.push(x));
const highlights = [
{ id: 1, label: translate("dashboard-billboard-page-sales-total-sales"), value: hasValue(ordersList) ? ordersList.length : "-" },
{ id: 2, label: translate("dashboard-billboard-page-sales-total-earnings"), value: hasValue(ordersList) && ordersList.length > 0 ? ordersList.map(x => x.total_price).reduceRight((total, x) => (total + x)).toFixed(2) : "0" },
{ id: 3, label: translate("dashboard-billboard-page-sales-total-customers"), value: hasValue(ordersList) ? customers.length : "-" }
]
// methods
// useEffects
// useEffect(() => {
// getTheme !== Sales && setSales(getTheme);
// }, [dashboardTheme]);
// return
return (
<BillboardSection
title={translate("dashboard-billboard-page-sales-title")}
icon={<BagShoppingSolid className="inline-block size-4 text-[var(--brand-color)] fill-current" />}
>
{ordersList ?
<>
<p className="block text-xs/6 mb-4">{translate("dashboard-billboard-page-sales-text")}</p>
<div className="grid grid-cols-3 w-full divide-x rtl:divide-x-reverse divide-[var(--light-brand-color)]">
{highlights.map(x => (
<div key={x.id} className="flex flex-col items-center space-y-2">
<span className="inline-block text-center text-xl text-[var(--brand-color)] font-extrabold">{x.value}</span>
<span className="inline-block text-center text-xs text-gray-500">{x.label}</span>
</div>
))}
</div>
<ul className="block w-full pt-4 mt-4 border-t border-dashed border-gray-200/75">
<li className="text-sm font-semibold px-1 pb-4">{translate("dashboard-billboard-page-sales-latest-orders")}</li>
{ordersList.length > 0 ?
ordersList.slice(0, 3).map(x => (
<li key={x.id} className="flex items-center justify-between w-full py-2 px-1 space-x-2 rtl:space-x-reverse border-b border-dashed border-neutral-100">
{/* <span className="text-xs text-center text-text-gray">-</span> */}
<span className="text-[0.7rem]/4 font-semibold w-60 rtl:pr-1 ltr:pl-1 line-clamp-1">- {(locale === "fa" ? x.ordered_products[0].persian_title : x.ordered_products[0].english_title)}</span>
<span className="text-[0.7rem]/4 w-14 text-center font-extrabold py-1 px-2 rounded-md bg-success-bg text-success-text">${x.total_price}</span>
{/* <span className="text-[0.7rem]/4 py-1 px-2 rounded-md bg-warning-bg text-warning-text">{translate(`billboard-orders-status-${x.order_status}`)}</span> */}
</li>
))
:
<div className="">
<span className="block text-xs/6 mb-4 px-1">{translate("dashboard-billboard-page-no-sales-text")}</span>
<img src="/pics/billboard/no-data.svg" alt="no data to show" className="block w-3/5 md:w-2/5 mx-auto mb-2" />
</div>
}
</ul>
<Button
type="button"
text={translate("dashboard-billboard-page-sales-see-all")}
className="block w-full py-2 px-4 rounded-xl text-sm bg-[var(--brand-color)] text-white mt-4"
rightIcon={<ArrowLeftSolid className="inline-block size-4 text-white fill-current rtl:mr-3 ltr:ml-3" />}
/>
</>
:
<InnerLoading loadingText={""} width={"200"} height={"100"} />
}
</BillboardSection>
)
}
export default Sales

View File

@ -0,0 +1,46 @@
import { useGetRouter } from 'services/general/general';
import useTranslate from 'services/translation/translation';
import { ArrowLeftSolid } from 'components/icons';
import Link from 'components/link/link';
interface BillboardSectionProps {
title: string;
icon: React.ReactNode;
linkText?: string;
linkAddress?: string;
showLink?: boolean;
wrapperClass?: string;
contentClass?: string;
style?: React.CSSProperties;
children: React.ReactNode;
}
const BillboardSection: React.FunctionComponent<BillboardSectionProps> = ({ title, icon, linkText, linkAddress, showLink = true, children, wrapperClass, contentClass, style }) => {
// states
// variables
const { locale, query } = useGetRouter();
const translate = useTranslate();
// methods
// useEffects
// return
return (
<section style={style} className={`block w-[calc(100%_-_2rem)] md:w-full pt-2 mx-auto shadow-bot bg-white rounded-xl ${wrapperClass}`}>
<div className="flex items-center justify-between w-[calc(100%_-_1rem)] mx-auto py-2 px-3 rounded-t-lg rounded-b-sm bg-neutral-50">
<div className="flex items-center space-x-2 rtl:space-x-reverse">
{icon}
<span className="text-sm font-semibold text-text-color capitalize">{title}</span>
</div>
{linkAddress && linkText && showLink && <Link href={linkAddress} className="">{linkText}</Link>}
</div>
<div className={`block w-full px-4 pt-2 pb-4 ${contentClass}`}>
{children}
</div>
</section>
)
}
export default BillboardSection

View File

@ -0,0 +1,126 @@
import { adminAccessToken, adminFetchPrivateData, 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 Link from 'components/link/link';
import BillboardSection from './section';
import useSWR from 'swr';
import { billboardOrdersQuery } from 'services/queries/directus/billboard';
import { BillboardOrdersItem } from 'common/types/billboard';
import { Line } from 'react-chartjs-2';
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';
interface StatisticsOverviewProps {
themeColor: {
light: string;
full: string;
}
visits: number[];
ratings: any;
}
const StatisticsOverview: React.FunctionComponent<StatisticsOverviewProps> = ({ themeColor, visits, ratings }) => {
// states
// const [value, setValue] = useState<string[]>([]);
// variables
const { locale, query } = useGetRouter();
const translate = useTranslate();
const today = new Date();
const oneDay = 86400000; // 24 * 60 * 60 * 1000 milliseconds in a day
const numberOfDaysToShow = 30 + 7; // 7 here is to count one week before the chart as well for better representation
const pastMonthWeeks = getWeeksBetween(new Date(today.getTime() - (numberOfDaysToShow * oneDay)), today);
const pastMonthWeeksLabels = pastMonthWeeks.map(x => mtd(x.getTime()).slice(5)).slice(1);
const visitsAggregated = () => {
let agrVisits = [];
for (let i = 0; i < pastMonthWeeks.length - 1; i++) {
agrVisits.push(visits?.filter(y => new Date(mtd(y)).getTime() > new Date(mtd(pastMonthWeeks[i].getTime())).getTime() && new Date(mtd(y)).getTime() <= new Date(mtd(pastMonthWeeks[i + 1].getTime())).getTime()));
}
return agrVisits.map(x => x.length);
}
const { data: orders, error: ordersError } = useSWR([billboardOrdersQuery(query.id), "", adminAccessToken], ([query, route, token]) => adminFetchPrivateData(route, token, 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";
const highlights = [
{ id: 1, icon: <EyeSolid className={highlighIconClass} />, label: translate("dashboard-billboard-page-statistics-total-visits"), value: hasValue(visits) ? visits.length : "0" },
{ id: 2, icon: <StarSolid className={`${highlighIconClass}`} />, label: translate("dashboard-billboard-page-statistics-total-ratings"), value: hasValue(ratings) ? `${ratings.totalRating} / 5` : "-" },
{ id: 3, icon: <SackDollarSolid className={highlighIconClass} />, label: translate("dashboard-billboard-page-statistics-total-sales"), value: hasValue(ordersList) ? ordersList.length : "-" },
]
ChartJS.register(LineElement, PointElement, LinearScale, CategoryScale, Title, Tooltip, Legend);
const data = {
labels: pastMonthWeeksLabels,
datasets: [
{
label: "",
data: hasValue(visits) ? visitsAggregated() : pastMonthWeeksLabels.map(x => 0),
fill: false,
backgroundColor: themeColor.full,
borderColor: themeColor.full,
tension: 0.1,
}
],
};
const options: ChartOptions<'line'> = {
responsive: true,
plugins: {
legend: {
display: false,
position: 'top' as const, // Ensure TypeScript understands this is a literal
},
title: {
display: false,
text: 'Line Chart Example',
},
},
};
// methods
// useEffects
// useEffect(() => {
// }, []);
// return
return (
<BillboardSection
title={translate("dashboard-billboard-page-statistics-title")}
icon={<ChartSimpleSolid className="inline-block size-4 text-[var(--brand-color)] fill-current" />}
>
{hasValue(ordersList) ?
<>
<div className="block space-y-3 mt-2">
{highlights.map(x => (
<div key={x.id} className="flex items-center space-x-2 rtl:space-x-reverse px-1">
<CircleSolid className={highlighIconClass} />
<span className="text-sm">{x.label} :</span>
<span className="text-sm font-extrabold [direction:ltr]">{x.value}</span>
</div>
))}
</div>
<div className="block pt-2 pb-4 mt-6 border-t border-dashed border-gray-200/75">
<span className="block text-xs px-1 text-center py-2">{translate("dashboard-billboard-page-statistics-last-months-visits")}</span>
<Line data={data} options={options} />
</div>
<Button
type="button"
text={translate("dashboard-billboard-page-statistics-see-all")}
className="block w-full py-2 px-4 rounded-xl text-sm bg-[var(--brand-color)] text-white mt-4"
rightIcon={<ArrowLeftSolid className="inline-block size-4 text-white fill-current rtl:mr-3 ltr:ml-3" />}
/>
</>
:
<InnerLoading loadingText={""} width={"200"} height={"100"} color={themeColor.full} />
}
</BillboardSection>
)
}
export default StatisticsOverview

View File

@ -0,0 +1,109 @@
import { useGetRouter } from 'services/general/general';
import useTranslate from 'services/translation/translation';
import { ShopLockSolid } from 'components/icons';
import { useEffect, useState } from 'react';
import Button from 'components/button/button';
import BillboardSection from './section';
import Modal from 'components/modal/modal';
import { updateBillboard } from 'services/queries/directus/billboard';
interface TemporarilyClosedProps {
isClosed: boolean;
themeColor: {
light: string;
full: string;
};
}
const TemporarilyClosed: React.FunctionComponent<TemporarilyClosedProps> = ({ isClosed = false, themeColor }) => {
// states
const [closed, setClosed] = useState(isClosed);
const [updating, setUpdating] = useState(false);
const [popupOpen, setPopupOpen] = useState(false);
// variables
const { locale, query } = useGetRouter();
const translate = useTranslate();
const payload = `{
temporarily_closed: ${!isClosed}
}`
// methods
const handleTemporarilyClose = () => {
setPopupOpen(true);
}
const handleCloseModal = () => {
setPopupOpen(false);
}
const handleTemporailyClose = async () => {
setUpdating(true);
try {
const result = await updateBillboard({ id: query.id, payload: payload });
setClosed(!closed);
setUpdating(false);
setPopupOpen(false);
} catch (error) {
console.error("Update failed:", error);
setUpdating(false);
}
}
// useEffects
useEffect(() => {
isClosed !== closed && setClosed(isClosed);
}, [isClosed]);
// return
return (
<BillboardSection
title={translate("dashboard-billboard-page-temporarily-closed-title")}
icon={<ShopLockSolid className="inline-block size-4 text-[var(--brand-color)] fill-current" />}
>
<Modal
header={true}
wrapperId={`temporarily-closed-modal`}
open={popupOpen}
onClose={handleCloseModal}
title={translate("dashboard-billboard-page-temporarily-closed-title")}
className="flex flex-col bg-white w-[90vw] h-auto max-h-[80vh] lg:w-auto lg:h-auto lg:min-w-[500px] lg:max-w-[90vw] lg:max-h-[90vh] rounded"
childrenClass="lg:flierland-scrollbar"
headerClass="!h-12"
titleClass="text-base"
closeClass="!size-6"
style={{
"--brand-color": themeColor.full,
"--light-brand-color": themeColor.light,
} as React.CSSProperties}
>
<div className="block w-full sm:w-[500px] p-gi">
<p className="text-sm/7 sm:text-base/8">{closed ? translate("dashboard-billboard-page-temporarily-closed-modal-open-text") : translate("dashboard-billboard-page-temporarily-closed-modal-close-text")}</p>
<div className="grid grid-cols-2 gap-4 px-2 pt-8">
<Button
type="button"
text={closed ? translate("dashboard-billboard-page-temporarily-closed-false-label") : translate("dashboard-billboard-page-temporarily-closed-title")}
className="bg-[var(--brand-color)] text-white text-sm md:text-base capitalize shadow-none w-full px-[10px] pt-[6px] pb-[6px] md:px-3 md:py-2 inline-flex justify-center items-center rounded-xl rtl:ml-2 ltr:mr-2 rtl:lg:ml-3 ltr:lg:mr-3"
onClick={handleTemporailyClose}
loading={updating}
/>
<Button
type="button"
text={translate("cancel")}
className="bg-white text-[var(--brand-color)] ring-1 ring-[var(--brand-color)] text-sm md:text-base capitalize shadow-none w-full px-[10px] pt-[6px] pb-[6px] md:px-3 md:py-2 inline-flex justify-center items-center rounded-xl"
onClick={handleCloseModal}
/>
</div>
</div>
</Modal>
<p className="block text-xs/6">{translate("dashboard-billboard-page-temporarily-closed-text")}</p>
<div className="flex items-center justify-between mt-5">
<div onClick={handleTemporarilyClose} className={`h-8 w-20 rounded-full p-1 transition duration-200 ease-in-out xl:cursor-pointer shadow-inner ${!closed ? "bg-neutral-100/75" : "bg-[var(--brand-color)]"} `}>
<span className={`h-6 w-6 rounded-full inline-block pointer-events-none bg-white shadow-sm ring-1 ring-neutral-400/10 transition duration-200 ease-in-out ${!closed ? "rtl:-translate-x-12 ltr:translate-x-12" : "translate-x-0"}`}></span>
</div>
<span className={`text-sm font-semibold 2xl:text-base capitalize py-1 px-3 rounded-lg ltr:mr-2 rtl:ml-2 ${closed ? "bg-error-bg text-error-text" : "bg-success-bg text-success-text"}`}>{closed ? translate("dashboard-billboard-page-temporarily-closed-true-label") : translate("dashboard-billboard-page-temporarily-closed-false-label")}</span>
</div>
</BillboardSection>
)
}
export default TemporarilyClosed

View File

@ -0,0 +1,29 @@
import { hasValue } from "services/general/general";
interface ThemeColorProps {
brandColor: string;
dashboardTheme: "brand" | "system" | "custom";
dashboardColor: string;
}
export const ThemeColor = ({ brandColor, dashboardTheme, dashboardColor }: ThemeColorProps) => {
let finalColor = { light: "", full: "" };
switch (dashboardTheme) {
case "brand":
finalColor = { light: (hasValue(brandColor) ? `${brandColor}10` : "#f3f5fb"), full: (hasValue(brandColor) ? brandColor : "#516ec2") };
break;
case "system":
finalColor = { light: "#f3f5fb", full: "#516ec2" };
break;
case "custom":
finalColor = { light: `${dashboardColor}10`, full: dashboardColor };
break;
default:
break;
}
return finalColor
}
export default ThemeColor

View File

@ -1,31 +0,0 @@
import { useAppSelector, useAppDispatch } from '../../../redux/hooks';
import { getAreFiltersReset, getFuelType, setMarketFilters } from "common/redux/slices/market";
interface Billboards {
}
const Billboards: React.FunctionComponent<Billboards> = ({ }) => {
// states
// const [value, setValue] = useState<string[]>([]);
// variables
const dispatch = useAppDispatch();
const areFiltersReset = useAppSelector(getAreFiltersReset);
// methods
// useEffects
// useEffect(() => {
// }, []);
// return
return (
<section className="block w-full px-6 py-6">
Billboards
</section>
)
}
export default Billboards

View File

@ -6,19 +6,22 @@ import Header from "./header";
import { setUserData, userData, userIsDataReady } from "common/redux/slices/user";
import { userObject } from "services/user/user";
import Loading from "components/loading/loading";
import { adminAccessToken, adminFetchPrivateData, hasValue, useGetRouter } from "services/general/general";
import { adminAccessToken, adminFetchPrivateData, fetchPublicData, hasValue, useGetRouter } from "services/general/general";
import { getDictionary, setDictionary, setMenu } from "common/redux/slices/global";
import useSWR from "swr";
import { useAuth } from "services/user/AuthContext";
import ProtectedRoute from "components/protected-route";
import { useCachedGlobalData } from "services/general/global-data";
import { globalDataQuery, useCachedGlobalData } from "services/general/global-data";
import useSWRImmutable from "swr";
interface GeneralLayout {
children: any,
style?: React.CSSProperties;
bodyClass?: string;
}
// general layout
const GeneralLayout: React.FunctionComponent<GeneralLayout> = ({ children }) => {
const GeneralLayout: React.FunctionComponent<GeneralLayout> = ({ children, bodyClass, style }) => {
const [navOpen, setNavOpen] = useState(false);
@ -40,7 +43,7 @@ const GeneralLayout: React.FunctionComponent<GeneralLayout> = ({ children }) =>
const isUserDataAvailable = useAppSelector(userIsDataReady);
const { data: messagesData, error: messageFetchError } = useSWR(isUserDataAvailable ? [adminAccessToken, messagesDataQuery, ""] : null, ([token, query, route]) => adminFetchPrivateData(route, token, query));
const { data: globalData, error: globalDataError } = useCachedGlobalData();
// const { data: translations, error: error } = useSWRImmutable(["", translationsQuery], ([route, query]) => fetchPublicData(route, query));
// 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) {
@ -95,17 +98,18 @@ const GeneralLayout: React.FunctionComponent<GeneralLayout> = ({ children }) =>
return (
<ProtectedRoute>
{trData.length > 0 ?
<div className="flex w-full bg-market-input relative 2xl:px-[calc((100%_-_var(--max-width))/2)]">
<div style={style} className="flex w-full bg-market-input relative 2xl:px-[calc((100%_-_var(--max-width))/2)]">
<SideNavigation open={navOpen} onClose={toggleNav} />
<div className="block w-full h-screen lg:w-[calc(100%_-_300px)] bg-white lg:bg-white">
<Header open={navOpen} onClose={toggleNav} />
<main className="block overflow-y-scroll h-[calc(100vh_-_74px)] lg:h-[calc(100vh_-_100px)] hide-scrollbar bg-gray-50">
<main className={`block overflow-y-scroll h-[calc(100vh_-_74px)] lg:h-[calc(100vh_-_100px)] hide-scrollbar bg-gray-50 ${bodyClass}`}>
{children}
</main>
</div>
</div>
:
<Loading loadingText={loadingText} />
// <Loading loadingText={loadingText} />
""
}
</ProtectedRoute>
)

View File

@ -13,18 +13,22 @@ interface Header {
const Header: React.FunctionComponent<Header> = ({ open, onClose }) => {
const user = useAppSelector(userData);
const { asPath, router } = useGetRouter();
const { asPath, router, pathname } = useGetRouter();
const translate = useTranslate();
const isSubPage = asPath.lastIndexOf("/") > 10
const pageTitle = () => {
let title = "";
// ads
// dashboard
if (asPath === "/dashboard") title = translate("user-profile-menu-dashboard");
// ads
if (asPath === "/dashboard/ads") title = translate("user-profile-menu-ads");
if (asPath === "/dashboard/ads/new-ad") title = translate("profile-create-new-ad");
if (asPath.includes("/dashboard/ads/new-ad?id=")) title = translate("edit-ad");
// billboards
if (asPath === "/dashboard/billboards") title = translate("user-profile-menu-billboards");
if (pathname === "/dashboard/billboards/[id]") title = translate("dashboard-billboard-page-title");
// account
if (asPath === "/dashboard/account") title = translate("user-profile-menu-account");
if (asPath.includes("/dashboard/account/edit-details?id=")) title = translate("edit-user-details");

View File

@ -7,7 +7,7 @@ import Skeleton from "components/skeleton/skeleton";
import { resetUserData, userData } from "common/redux/slices/user";
import { basePath, hasValue, useGetRouter } from "services/general/general";
import LanguageSelection from "common/templates/general/header/language-selection";
import { AdvertiseLight, AdvertiseSolid, Grid2Light, Grid2Solid, InboxFullSolid, InboxLight, SignOutLight, UserVneckHairLight, UserVneckHairSolid, WalletLight, WalletSolid } from "components/icons";
import { AdvertiseLight, AdvertiseSolid, Grid2Light, Grid2Solid, InboxFullSolid, InboxLight, SignOutLight, SignsPostLight, SignsPostSolid, UserVneckHairLight, UserVneckHairSolid, WalletLight, WalletSolid } from "components/icons";
import { useAuth } from "services/user/AuthContext";
interface SideNavigation {
@ -56,6 +56,12 @@ const MenuItem: React.FunctionComponent<MenuItem> = ({ href, icon, activeIcon, t
case "AdvertiseSolid":
finalIcon = <AdvertiseSolid className={getIconClass} />
break;
case "SignsPostLight":
finalIcon = <SignsPostLight className={getIconClass} />
break;
case "SignsPostSolid":
finalIcon = <SignsPostSolid className={getIconClass} />
break;
case "UserVneckHairLight":
finalIcon = <UserVneckHairLight className={getIconClass} />
break;
@ -144,10 +150,9 @@ const SideNavigation: React.FunctionComponent<SideNavigation> = ({ open, onClose
<nav className="block w-full pt-0 pb-3 lg:py-4">
<MenuItem href="" icon="Grid2Light" activeIcon="Grid2Solid" text="user-profile-menu-dashboard" />
<MenuItem href="/ads" icon="AdvertiseLight" activeIcon="AdvertiseSolid" text="user-profile-menu-ads" />
{/* <MenuItem href="/billboards" icon="SignsPostLight" activeIcon="SignsPostSolid" text="user-profile-menu-billboards" /> */}
<MenuItem href="/account" icon="UserVneckHairLight" activeIcon="UserVneckHairSolid" text="user-profile-menu-account" />
<MenuItem href="/wallet" icon="WalletLight" activeIcon="WalletSolid" text="user-profile-menu-wallet" />
{/* <MenuItem href="/billboards" icon="signs_post_light" activeIcon="signs_post_solid" text="user-profile-menu-billboards" />
<MenuItem href="/events" icon="calendar_star_light" activeIcon="calendar_star_solid" text="user-profile-menu-events" /> */}
<MenuItem href="/inbox" icon="InboxLight" activeIcon="InboxFullSolid" text="user-profile-menu-inbox" notifications={user.inbox.new_messages} />
{/* <MenuItem href="/comments" icon="messages_light" activeIcon="messages_solid" text="user-profile-menu-comments" /> */}
</nav>

View File

@ -1,3 +1,4 @@
import { UUID } from "crypto";
import { currencies } from "./general";
import { PaymentData } from "./payments";
import { ServiceProvider } from "./service-provider";
@ -5,6 +6,8 @@ import { UserProps } from "./user";
export type Billboard = {
id: string;
date_created: string;
status: "draft" | "published" | "archived";
translations: {
languages_code: {
code: string;
@ -32,8 +35,11 @@ export type Billboard = {
type: string;
}
brand_color: string;
dashboard_theme: "brand" | "system" | "custom";
dashboard_color: string;
billboard_categories: {
billboard_categories_id: {
id: string;
translations: {
languages_code: {
code: string;
@ -114,6 +120,7 @@ export type Billboard = {
seats_numbers: string[];
}[];
}[];
visits: number[];
}
export type BillboardReview = {
@ -541,4 +548,42 @@ export type BillboardNewsItem = {
pinned: boolean;
likes: number;
seen_by: number;
}
export type BillboardOrdersItem = {
id: string;
status: "draft" | "published" | "archived";
date_created: string;
order_status: "pending" | "confirmed" | "processing" | "on-hold" | "shipped" | "delivered" | "completed" | "canceled" | "failed" | "returned" | "refunded";
billboard_id: number;
billboard_order_id: number;
user_id: UUID;
payment_id: UUID;
reservation_id: number;
ordered_products: {
id: number;
product_id: number;
persian_title: string;
english_title: string;
original_price: number;
final_price: number;
price_unit: string;
ordered_amount: string;
ordered_size: string;
ordered_color: {
key: number,
collection: "Colors"
}
currency: {
key: number;
collection: "currencies";
}
}[];
total_price: number;
translations: {
languages_code: {
code: string;
};
note: string;
}[];
}

View File

@ -0,0 +1,25 @@
import { adminDataFetch } from 'common/data/apollo-client';
import { NextApiRequest, NextApiResponse } from 'next';
import { adminAccessToken } from 'services/general/general';
import { updateBillboardItem } from 'services/queries/directus/billboard';
export default async (req: NextApiRequest, res: NextApiResponse) => {
if (req.method === 'POST') {
let id = req.body.id;
let payload = req.body.payload;
try {
const { data } = await adminDataFetch("", adminAccessToken).mutate({
mutation: updateBillboardItem(id, payload),
});
res.status(200).json(data);
} catch (error) {
return res.status(400).json(error);
}
} else {
res.setHeader('Allow', 'POST');
res.status(405).end('Method Not Allowed');
}
};

View File

@ -4,7 +4,7 @@ import client, { adminDataFetch } from "../../../common/data/apollo-client";
import { useDispatch } from 'react-redux'
import Parser from "components/parser/parser";
import Meta from "components/meta/meta"
import { url, basePath, getLocaleTr, useGetRouter, adminAccessToken, getVisitedPages, addVisitedPage, AdvancedURIDecoder, hasValue } from 'services/general/general'
import { url, basePath, getLocaleTr, useGetRouter, adminAccessToken, getVisitedPages, addVisitedPage, AdvancedURIDecoder, hasValue, mtd, oneDay } from 'services/general/general'
import useTranslate from 'services/translation/translation'
import { BillboardSchema } from "components/schema/schema";
import Gallery from "components/gallery/gallery";
@ -34,7 +34,6 @@ import { updateBillboardViews } from "services/queries/directus/item-viewed";
import ProductPlaceHolder from "common/templates/billboard/page/products/placeholder";
const BillboardContact = dynamic(() => import("common/templates/billboard/page/contact"), { ssr: false })
const BillboardPage = ({ data }: any) => {
// states
@ -84,7 +83,14 @@ const BillboardPage = ({ data }: any) => {
}
}
const [updateViews, { data: billboardViewsMutationData, error: billboardViewsMutationErrors, loading: mutating }] = useMutation(updateBillboardViews(billboard.views_count, billboard.id), {
const userViews = () => {
let localUserViews: string[] = hasValue(billboard.visits) ? billboard.visits.map((x: string) => x) : [];
localUserViews.push(`${Date.now()}`);
return localUserViews.map(x => String(x));
};
const [updateViews, { data: billboardViewsMutationData, error: billboardViewsMutationErrors, loading: mutating }] = useMutation(updateBillboardViews(userViews(), billboard.id), {
client: adminDataFetch("", adminAccessToken),
onError: (err) => {
console.log(err.message);
@ -129,7 +135,7 @@ const BillboardPage = ({ data }: any) => {
const pageType: any = "billboards";
const visitedPages = getVisitedPages();
if (!visitedPages[pageType].includes(billboard.id)) {
if (!(visitedPages["billboards"]).some((x: { id: string, time: number }) => (x.id === billboard.id) && (x.time > (Date.now() - oneDay().milliseconds * 30)) )) {
// If not, update the page views count and mark the page as visited
addVisitedPage(billboard.id, pageType);
updateViews();
@ -453,7 +459,7 @@ export async function getStaticProps({ locale, params }: any) {
}
Billboards(filter: { status: { _eq: "published" }, id: { _eq: "${params.id}" } }) {
id
views_count
visits
translations {
languages_code {
code

View File

@ -86,8 +86,6 @@ const Ads: React.FunctionComponent<Ads> = ({ }) => {
`
const { data: userAds, error: userDataError } = useSWR([dataQuery, ""], ([query, route]) => fetchPublicData(route, query));
// variables
// methods
// const handleArchive = () => {

View File

@ -1,22 +0,0 @@
import GeneralLayout from "common/templates/dashboard/navigation/general-layout";
interface Billboards {
}
const Billboards: React.FunctionComponent<Billboards> = ({ }) => {
// states
// variables
// methods
// useEffects
// return
return <GeneralLayout>
ads page!
</GeneralLayout>
}
export default Billboards;

View File

@ -0,0 +1,95 @@
import { useAppSelector } from "common/redux/hooks";
import { userData } from "common/redux/slices/user";
import BillboardOverview from "common/templates/dashboard/billboards/billboard-page/billboard-overview";
import BillboardProductsNews from "common/templates/dashboard/billboards/billboard-page/products-news";
import TemporarilyClosed from "common/templates/dashboard/billboards/billboard-page/temporarily-closed";
import ThemeColor from "common/templates/dashboard/billboards/billboard-page/theme-color";
import GeneralLayout from "common/templates/dashboard/navigation/general-layout";
import { Billboard } from "common/types/billboard";
import InnerLoading from "components/loading/inner-loading";
import Link from "next/link";
import { fetchPublicData, useGetRouter } from "services/general/general";
import { billboardNewsAggregatedQuery, billboardProductsAggregatedQuery, getBillboardFullDataQuery, getBillboardRatingsQuery } from "services/queries/directus/billboard";
import useTranslate from "services/translation/translation";
import useSWR from "swr";
import BrandColor from "common/templates/dashboard/billboards/billboard-page/brand-color";
import DashboardColor from "common/templates/dashboard/billboards/billboard-page/dashboard-color";
import Sales from "common/templates/dashboard/billboards/billboard-page/sales";
import { UUID } from "crypto";
import StatisticsOverview from "common/templates/dashboard/billboards/billboard-page/statistics";
import { useMemo } from "react";
interface BillboardPageProps {
}
const BillboardPage: React.FunctionComponent<BillboardPageProps> = ({ }) => {
// states
// variables
const { locale, query } = useGetRouter();
const translate = useTranslate();
const user = useAppSelector(userData);
const { data: billboardData, error: billboardDataError } = useSWR([getBillboardFullDataQuery(query.id, user.generalDetails.id), ""], ([query, route]) => fetchPublicData(route, query));
const { data: billboardRatings, error: billboardRatingsError } = useSWR([getBillboardRatingsQuery(query.id), ""], ([query, route]) => fetchPublicData(route, query));
const { data: billboardGeneral, error: billboardGeneralError } = useSWR(
[`${
billboardProductsAggregatedQuery(query.id) +
billboardNewsAggregatedQuery(query.id)
}`, ""],
([query, route]) => fetchPublicData(route, query)
);
billboardDataError && console.log(billboardDataError);
billboardRatingsError && console.log(billboardRatingsError);
billboardGeneralError && console.log(billboardGeneralError);
const billboard: Billboard = billboardData?.data.Billboards[0];
const overallRatings: any= billboardRatings?.data.billboard_ratings_aggregated;
const billboardGeneralData: any = billboardGeneral?.data;
const theme_color = billboard ? ThemeColor({ brandColor: billboard.brand_color, dashboardTheme: billboard.dashboard_theme, dashboardColor: billboard.dashboard_color }) : { light: "#f3f5fb", full: "#516ec2" };
const getRating = useMemo(() => {
const serviceQuality = overallRatings ? overallRatings[0].avg.service_quality : 0;
const totalRating = (serviceQuality) / 1;
return { totalRating: totalRating, count: overallRatings ? overallRatings[0].countDistinct.id : 0, serviceQuality: serviceQuality };
}, [overallRatings]);
// methods
// useEffects
// return
return <GeneralLayout
style={{
"--brand-color": theme_color.full,
"--light-brand-color": theme_color.light,
} as React.CSSProperties}
bodyClass=""
>
{(billboard && overallRatings && billboardGeneralData) ?
<div className="block pb-8">
<BillboardOverview
data={billboard}
ratings={getRating}
products={billboardGeneralData.billboard_products_aggregated[0].countDistinct.id}
news={billboardGeneralData.billboard_news_aggregated[0].countDistinct.id}
/>
<BillboardProductsNews />
<div className="grid grid-cols-1 md:grid-cols-2 gap-y-2 md:gap-y-4 gap-x-4 mt-4 md:px-4">
<StatisticsOverview visits={billboard.visits} themeColor={theme_color} ratings={getRating} />
<Sales />
<BrandColor themeColor={theme_color} brandColor={billboard.brand_color} />
<DashboardColor themeColor={theme_color} dashboardTheme={billboard.dashboard_theme} />
<TemporarilyClosed isClosed={billboard.temporarily_closed} themeColor={theme_color} />
</div>
</div>
:
<InnerLoading loadingText={""} width={"200"} height={"100"} />
}
</GeneralLayout>
}
export default BillboardPage;

View File

@ -0,0 +1,95 @@
import { useAppSelector } from "common/redux/hooks";
import { userData } from "common/redux/slices/user";
import GeneralLayout from "common/templates/dashboard/navigation/general-layout";
import { Billboard } from "common/types/billboard";
import Link from "next/link";
import { fetchPublicData, getLocaleTr, useGetRouter } from "services/general/general";
import useTranslate from "services/translation/translation";
import useSWR from "swr";
interface Billboards {
}
const Billboards: React.FunctionComponent<Billboards> = ({ }) => {
// states
// variables
const { locale } = useGetRouter();
const translate = useTranslate();
const user = useAppSelector(userData);
const userBillboardsQuery = `
Billboards (
filter: {
user_created: { id: {_eq: "${user.generalDetails.id}"} },
},
sort: ["sort", "-date_created"],
limit: -1
)
{
id
translations {
languages_code {
code
}
title
body
meta_description
slogan
}
city {
name
}
address
phone_number
email
website
instagram
facebook
twitter
youtube
working_hours
billboard_categories {
billboard_categories_id {
id
translations {
languages_code {
code
}
name
}
}
}
gallery {
directus_files_id {
id
description
width
height
}
}
}
`
const { data: userBillboards, error: userBillboardsError } = useSWR([userBillboardsQuery, ""], ([query, route]) => fetchPublicData(route, query));
userBillboardsError && console.log(userBillboardsError);
const billboards: Billboard[] = userBillboards?.data.Billboards;
// methods
// useEffects
// return
return <GeneralLayout>
{billboards && <ul className="block w-full px-4">
{billboards.map(x => (
<li key={x.id}>
<Link href={`/dashboard/billboards/${x.id}`} className="block w-full py-2">{getLocaleTr(x, locale).title}</Link>
</li>
))}
</ul>}
</GeneralLayout>
}
export default Billboards;

View File

@ -9,7 +9,7 @@ import Button from "components/button/button"
import Popover from "components/popover/popover"
import Breadcrumb from "components/breadcrumb/breadcrumb"
import Meta from "components/meta/meta"
import { url, basePath, getVisitedPages, addVisitedPage, getLocaleTr, useGetRouter, AdvancedURIDecoder, adminAccessToken, getCatTr } from 'services/general/general'
import { url, basePath, getVisitedPages, addVisitedPage, getLocaleTr, useGetRouter, AdvancedURIDecoder, adminAccessToken, getCatTr, oneDay } from 'services/general/general'
import useTranslate from 'services/translation/translation'
import { ArticleSchema } from "components/schema/schema";
import Layout from "common/templates/general/layout/layout";
@ -136,8 +136,8 @@ const ArticlePage = ({ data }: any) => {
// Check if the current page has been visited
const pageType: any = "articles";
const visitedPages = getVisitedPages();
if (!visitedPages[pageType].includes(articleRouterId)) {
if (!(visitedPages["articles"]).some((x: { id: string, time: number }) => (x.id === articleRouterId) && (x.time > (Date.now() - oneDay().milliseconds * 30)))) {
// If not, update the page views count and mark the page as visited
addVisitedPage(articleRouterId, pageType);
updateViews();

View File

@ -5,7 +5,7 @@ import { useDispatch } from 'react-redux'
import { getGlobalMenu } from '../../../common/redux/slices/global'
import Breadcrumb from "components/breadcrumb/breadcrumb"
import Meta from "components/meta/meta"
import { url, basePath, hasValue, getCatTr, useGetRouter, stripHtmlWithSpace, getVisitedPages, addVisitedPage, adminAccessToken, AdvancedURIDecoder } from 'services/general/general'
import { url, basePath, hasValue, getCatTr, useGetRouter, stripHtmlWithSpace, getVisitedPages, addVisitedPage, adminAccessToken, AdvancedURIDecoder, oneDay } from 'services/general/general'
import PicsGallery from "common/templates/market/ad-page/gallery";
import Overview from "common/templates/market/ad-page/overview";
import Contract from "common/templates/market/ad-page/contract";
@ -93,8 +93,8 @@ const AdPage = ({data}:any) => {
// Check if the current page has been visited
const pageType:any = "ads";
const visitedPages = getVisitedPages();
if (!visitedPages[pageType].includes(ad.id)) {
if (!(visitedPages["ads"]).some((x: { id: string, time: number }) => (x.id === ad.id) && (x.time > (Date.now() - oneDay().milliseconds * 30)))) {
// If not, update the page views count and mark the page as visited
addVisitedPage(ad.id, pageType);
updateViews();

View File

@ -3,7 +3,7 @@ import { gql, useMutation } from "@apollo/client";
import client, { adminDataFetch } from "../../../common/data/apollo-client";
import { useDispatch } from 'react-redux'
import Meta from "components/meta/meta"
import { url, basePath, hasValue, useGetRouter, URIDecoder, stripHtmlWithSpace, getVisitedPages, addVisitedPage, adminAccessToken, getLocaleTr } from 'services/general/general'
import { url, basePath, hasValue, useGetRouter, URIDecoder, stripHtmlWithSpace, getVisitedPages, addVisitedPage, adminAccessToken, getLocaleTr, oneDay } from 'services/general/general'
import Layout from "common/templates/general/layout/layout";
import { WikiSchema } from "components/schema/schema";
import InnerLoading from "components/loading/inner-loading";
@ -49,7 +49,7 @@ const GuidePage = ({data}:any) => {
const pageType:any = "wiki";
const visitedPages = getVisitedPages();
if (hasValue(visitedPages[pageType]) && !visitedPages[pageType].includes(wiki.id)) {
if (hasValue(visitedPages["wiki"]) && !(visitedPages["wiki"]).some((x: { id: string, time: number }) => (x.id === wiki.id) && (x.time > (Date.now() - oneDay().milliseconds * 30)))) {
// If not, update the page views count and mark the page as visited
addVisitedPage(wiki.id, pageType);
updateViews();

View File

@ -32,6 +32,8 @@ module.exports = {
'light-gray': '#f5f7fa',
'dark-gray': '#e6e6ea',
'cool-gray': '#b8b5c3',
'title-gray': '#7e849c',
'text-gray': '#8789a2',
'wheat': 'hsl(34deg 78% 91%)',
'market-primary': '#2A2E3C',
'market-sup': 'hsl(0deg 0% 95%)',