Compare commits

...

12 Commits

Author SHA1 Message Date
hamid zarghami 83c8c3a121 box ai
Build and Deploy Docker Images / build_and_deploy (push) Waiting to run
2026-07-29 12:00:53 +03:30
hamid zarghami 3d2de6fd4b problem solved
Build and Deploy Docker Images / build_and_deploy (push) Waiting to run
2026-07-29 09:41:41 +03:30
hamid zarghami 2248845467 show ai link in header
Build and Deploy Docker Images / build_and_deploy (push) Waiting to run
2026-07-29 09:41:09 +03:30
hamid zarghami b19c1cd3ca navigate back in ai [age
Build and Deploy Docker Images / build_and_deploy (push) Has been cancelled
2026-07-28 09:32:30 +03:30
hamid zarghami 2465ae1020 fix
Build and Deploy Docker Images / build_and_deploy (push) Has been cancelled
2026-07-26 15:50:30 +03:30
hamid zarghami 0cb119629c badge and show ai button
Build and Deploy Docker Images / build_and_deploy (push) Has been cancelled
2026-07-26 14:40:33 +03:30
hamid zarghami da42e42b7c base url
Build and Deploy Docker Images / build_and_deploy (push) Has been cancelled
2026-07-26 11:39:05 +03:30
hamid zarghami 6849062f28 offer food
Build and Deploy Docker Images / build_and_deploy (push) Has been cancelled
2026-07-26 10:37:59 +03:30
hamid zarghami 3409b8114e base url
Build and Deploy Docker Images / build_and_deploy (push) Has been cancelled
2026-07-23 11:16:24 +03:30
hamid zarghami 4c32f06da7 merge
Build and Deploy Docker Images / build_and_deploy (push) Has been cancelled
2026-07-23 11:14:36 +03:30
hamid zarghami a9fc7c4ff7 compeleted ai 2026-07-23 11:14:07 +03:30
hamid zarghami e10177a788 ui ai page 2026-07-23 10:03:37 +03:30
18 changed files with 1065 additions and 699 deletions
+101
View File
@@ -0,0 +1,101 @@
<svg width="299" height="97" viewBox="0 0 299 97" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M148.337 7.64759C148.337 11.8811 148.474 16.1145 148.337 20.348C148.2 23.489 146.147 25.8106 143.683 27.7225C143.273 27.9956 142.862 28.2687 142.178 28.815C143.41 29.6344 144.368 30.3172 145.463 31.1366H118.5C118.911 31 119.184 30.8634 119.321 30.8634C123.29 30.0441 127.533 29.7709 131.365 28.1322C140.399 24.3084 146.01 17.6168 148.2 8.05728L148.337 7.64759Z" fill="url(#paint0_linear_13943_20144)"/>
<path d="M152.169 23.3524C151.759 23.8987 151.211 24.4449 150.937 24.8546C150.527 25.4009 150.39 25.9471 149.979 26.63V0C149.979 0 149.979 0 150.116 0C150.527 2.04846 150.801 3.96035 151.211 6.00881C152.99 16.3877 158.739 23.7621 168.456 27.859C170.099 28.5419 172.015 28.9515 173.794 29.6344C173.657 29.6344 173.52 29.6344 173.384 29.6344C169.962 29.6344 166.677 29.6344 163.255 29.6344C158.739 29.7709 155.454 27.7225 152.854 24.0352C152.854 23.8987 152.58 23.6256 152.443 23.489C152.443 23.489 152.443 23.489 152.169 23.3524Z" fill="url(#paint1_linear_13943_20144)"/>
<path d="M126.986 32.3656C130.681 32.3656 134.514 32.3656 138.209 32.3656C141.768 32.3656 144.368 34.4141 146.421 37.1454C146.695 37.5551 146.969 37.9648 147.516 38.6476C148.337 37.5551 149.022 36.4626 149.843 35.5066V62C149.432 59.815 149.158 57.7665 148.748 55.5815C146.558 43.5639 139.578 35.9163 127.67 32.6388C127.533 32.6388 127.26 32.6388 127.123 32.5022C127.123 32.5022 127.123 32.5022 127.123 32.3656H126.986Z" fill="url(#paint2_linear_13943_20144)"/>
<path d="M151.347 53.8062C151.347 49.8458 151.211 45.8855 151.347 41.9251C151.484 38.6476 153.4 36.1894 156.001 34.2775C156.411 34.0044 156.822 33.7313 157.506 33.185C156.411 32.3656 155.317 31.6828 154.222 31H180.5C178.721 31.2731 177.078 31.6828 175.299 31.9559C168.866 32.9119 163.255 35.5066 158.738 40.1498C155.18 43.7004 152.853 47.9339 151.621 52.8502C151.621 53.1234 151.484 53.3965 151.347 53.8062Z" fill="url(#paint3_linear_13943_20144)"/>
<path d="M148.337 7.64759C148.337 11.8811 148.474 16.1145 148.337 20.348C148.2 23.489 146.147 25.8106 143.683 27.7225C143.273 27.9956 142.862 28.2687 142.178 28.815C143.41 29.6344 144.368 30.3172 145.463 31.1366H118.5C118.911 31 119.184 30.8634 119.321 30.8634C123.29 30.0441 127.533 29.7709 131.365 28.1322C140.399 24.3084 146.01 17.6168 148.2 8.05728L148.337 7.64759Z" fill="url(#paint4_linear_13943_20144)"/>
<path d="M152.169 23.3524C151.759 23.8987 151.211 24.4449 150.937 24.8546C150.527 25.4009 150.39 25.9471 149.979 26.63V0C149.979 0 149.979 0 150.116 0C150.527 2.04846 150.801 3.96035 151.211 6.00881C152.99 16.3877 158.739 23.7621 168.456 27.859C170.099 28.5419 172.015 28.9515 173.794 29.6344C173.657 29.6344 173.52 29.6344 173.384 29.6344C169.962 29.6344 166.677 29.6344 163.255 29.6344C158.739 29.7709 155.454 27.7225 152.854 24.0352C152.854 23.8987 152.58 23.6256 152.443 23.489C152.443 23.489 152.443 23.489 152.169 23.3524Z" fill="url(#paint5_linear_13943_20144)"/>
<path d="M126.986 32.3656C130.681 32.3656 134.514 32.3656 138.209 32.3656C141.768 32.3656 144.368 34.4141 146.421 37.1454C146.695 37.5551 146.969 37.9648 147.516 38.6476C148.337 37.5551 149.022 36.4626 149.843 35.5066V62C149.432 59.815 149.158 57.7665 148.748 55.5815C146.558 43.5639 139.578 35.9163 127.67 32.6388C127.533 32.6388 127.26 32.6388 127.123 32.5022C127.123 32.5022 127.123 32.5022 127.123 32.3656H126.986Z" fill="url(#paint6_linear_13943_20144)"/>
<path d="M151.347 53.8062C151.347 49.8458 151.211 45.8855 151.347 41.9251C151.484 38.6476 153.4 36.1894 156.001 34.2775C156.411 34.0044 156.822 33.7313 157.506 33.185C156.411 32.3656 155.317 31.6828 154.222 31H180.5C178.721 31.2731 177.078 31.6828 175.299 31.9559C168.866 32.9119 163.255 35.5066 158.738 40.1498C155.18 43.7004 152.853 47.9339 151.621 52.8502C151.621 53.1234 151.484 53.3965 151.347 53.8062Z" fill="url(#paint7_linear_13943_20144)"/>
<path d="M148.337 7.64759C148.337 11.8811 148.474 16.1145 148.337 20.348C148.2 23.489 146.147 25.8106 143.683 27.7225C143.273 27.9956 142.862 28.2687 142.178 28.815C143.41 29.6344 144.368 30.3172 145.463 31.1366H118.5C118.911 31 119.184 30.8634 119.321 30.8634C123.29 30.0441 127.533 29.7709 131.365 28.1322C140.399 24.3084 146.01 17.6168 148.2 8.05728L148.337 7.64759Z" fill="url(#paint8_linear_13943_20144)"/>
<path d="M152.169 23.3524C151.759 23.8987 151.211 24.4449 150.937 24.8546C150.527 25.4009 150.39 25.9471 149.979 26.63V0C149.979 0 149.979 0 150.116 0C150.527 2.04846 150.801 3.96035 151.211 6.00881C152.99 16.3877 158.739 23.7621 168.456 27.859C170.099 28.5419 172.015 28.9515 173.794 29.6344C173.657 29.6344 173.52 29.6344 173.384 29.6344C169.962 29.6344 166.677 29.6344 163.255 29.6344C158.739 29.7709 155.454 27.7225 152.854 24.0352C152.854 23.8987 152.58 23.6256 152.443 23.489C152.443 23.489 152.443 23.489 152.169 23.3524Z" fill="url(#paint9_linear_13943_20144)"/>
<path d="M126.986 32.3656C130.681 32.3656 134.514 32.3656 138.209 32.3656C141.768 32.3656 144.368 34.4141 146.421 37.1454C146.695 37.5551 146.969 37.9648 147.516 38.6476C148.337 37.5551 149.022 36.4626 149.843 35.5066V62C149.432 59.815 149.158 57.7665 148.748 55.5815C146.558 43.5639 139.578 35.9163 127.67 32.6388C127.533 32.6388 127.26 32.6388 127.123 32.5022C127.123 32.5022 127.123 32.5022 127.123 32.3656H126.986Z" fill="url(#paint10_linear_13943_20144)"/>
<path d="M151.347 53.8062C151.347 49.8458 151.211 45.8855 151.347 41.9251C151.484 38.6476 153.4 36.1894 156.001 34.2775C156.411 34.0044 156.822 33.7313 157.506 33.185C156.411 32.3656 155.317 31.6828 154.222 31H180.5C178.721 31.2731 177.078 31.6828 175.299 31.9559C168.866 32.9119 163.255 35.5066 158.738 40.1498C155.18 43.7004 152.853 47.9339 151.621 52.8502C151.621 53.1234 151.484 53.3965 151.347 53.8062Z" fill="url(#paint11_linear_13943_20144)"/>
<path d="M148.337 7.64759C148.337 11.8811 148.474 16.1145 148.337 20.348C148.2 23.489 146.147 25.8106 143.683 27.7225C143.273 27.9956 142.862 28.2687 142.178 28.815C143.41 29.6344 144.368 30.3172 145.463 31.1366H118.5C118.911 31 119.184 30.8634 119.321 30.8634C123.29 30.0441 127.533 29.7709 131.365 28.1322C140.399 24.3084 146.01 17.6168 148.2 8.05728L148.337 7.64759Z" fill="url(#paint12_linear_13943_20144)"/>
<path d="M152.169 23.3524C151.759 23.8987 151.211 24.4449 150.937 24.8546C150.527 25.4009 150.39 25.9471 149.979 26.63V0C149.979 0 149.979 0 150.116 0C150.527 2.04846 150.801 3.96035 151.211 6.00881C152.99 16.3877 158.739 23.7621 168.456 27.859C170.099 28.5419 172.015 28.9515 173.794 29.6344C173.657 29.6344 173.52 29.6344 173.384 29.6344C169.962 29.6344 166.677 29.6344 163.255 29.6344C158.739 29.7709 155.454 27.7225 152.854 24.0352C152.854 23.8987 152.58 23.6256 152.443 23.489C152.443 23.489 152.443 23.489 152.169 23.3524Z" fill="url(#paint13_linear_13943_20144)"/>
<path d="M126.986 32.3656C130.681 32.3656 134.514 32.3656 138.209 32.3656C141.768 32.3656 144.368 34.4141 146.421 37.1454C146.695 37.5551 146.969 37.9648 147.516 38.6476C148.337 37.5551 149.022 36.4626 149.843 35.5066V62C149.432 59.815 149.158 57.7665 148.748 55.5815C146.558 43.5639 139.578 35.9163 127.67 32.6388C127.533 32.6388 127.26 32.6388 127.123 32.5022C127.123 32.5022 127.123 32.5022 127.123 32.3656H126.986Z" fill="url(#paint14_linear_13943_20144)"/>
<path d="M151.347 53.8062C151.347 49.8458 151.211 45.8855 151.347 41.9251C151.484 38.6476 153.4 36.1894 156.001 34.2775C156.411 34.0044 156.822 33.7313 157.506 33.185C156.411 32.3656 155.317 31.6828 154.222 31H180.5C178.721 31.2731 177.078 31.6828 175.299 31.9559C168.866 32.9119 163.255 35.5066 158.738 40.1498C155.18 43.7004 152.853 47.9339 151.621 52.8502C151.621 53.1234 151.484 53.3965 151.347 53.8062Z" fill="url(#paint15_linear_13943_20144)"/>
<path d="M97.6982 83.8669V69H101.869C103.147 69 104.291 69.2691 105.3 69.8745C106.309 70.4127 107.049 71.2872 107.587 72.2963C108.125 73.3726 108.394 74.5835 108.394 75.9289V76.8707C108.394 78.2834 108.125 79.4943 107.587 80.5706C107.049 81.647 106.309 82.4542 105.3 82.9924C104.291 83.5305 103.147 83.8669 101.802 83.8669H97.6982ZM99.6491 70.6145V82.2524H101.735C103.214 82.2524 104.425 81.7815 105.233 80.8397C106.04 79.8979 106.511 78.5525 106.511 76.8034V75.9289C106.511 74.2471 106.107 72.9017 105.3 72.0272C104.493 71.0854 103.416 70.6145 101.936 70.6145H99.6491Z" fill="#1D1D1A"/>
<path d="M154.881 83.8669H152.863L145.396 72.4308V83.8669H143.445V69H145.396L152.863 80.5033V69H154.814V83.8669H154.881Z" fill="#1D1D1A"/>
<path d="M193.7 76.938L191.883 78.8216V83.8669H189.933V69H191.883V76.3325L198.476 69H200.831L194.978 75.5253L201.301 83.7996H198.947L193.7 76.938Z" fill="#1D1D1A"/>
<path d="M125.92 71.4218L130.494 83.8669H132.445L126.794 69H125.113L119.395 83.8669H121.413L125.92 71.4218Z" fill="#1D1D1A"/>
<path d="M172.407 71.4218L176.914 83.8669H178.932L173.282 69H171.533L165.882 83.8669H167.9L172.407 71.4218Z" fill="#1D1D1A"/>
<path d="M98.3216 96.7115H97.1582V90.8667H98.3216V96.7115Z" fill="#231F20"/>
<path d="M108.503 96.7115H107.339L104.985 92.778V96.7115H103.821V90.8667H104.985L107.339 94.8002V90.8667H108.503V96.7115Z" fill="#231F20"/>
<path d="M118.463 91.7808H116.801V96.7392H115.637V91.7808H114.003V90.8667H118.49V91.7808H118.463Z" fill="#231F20"/>
<path d="M127.314 94.1354H125.154V95.7974H127.73V96.6838H123.99V90.8667H127.73V91.7808H125.154V93.2489H127.314V94.1631V94.1354Z" fill="#231F20"/>
<path d="M134.393 95.8251H137.024V96.7115H133.229V90.8667H134.393V95.8251Z" fill="#231F20"/>
<path d="M143.688 95.8251H146.319V96.7115H142.524V90.8667H143.688V95.8251Z" fill="#231F20"/>
<path d="M152.983 96.7115H151.819V90.8667H152.983V96.7115Z" fill="#231F20"/>
<path d="M163.081 95.9913C162.887 96.2406 162.638 96.4345 162.277 96.6007C161.917 96.7669 161.446 96.8777 160.892 96.8777C160.338 96.8777 159.618 96.6561 159.147 96.2129C158.704 95.7697 158.482 95.188 158.482 94.4678V93.2489C158.482 92.5287 158.704 91.9747 159.147 91.5315C159.59 91.0883 160.144 90.8667 160.837 90.8667C161.529 90.8667 162.111 91.0329 162.471 91.3653C162.859 91.6977 163.025 92.1686 163.025 92.7226H161.917C161.917 92.4179 161.834 92.1686 161.64 92.0024C161.474 91.8362 161.197 91.7531 160.865 91.7531C160.532 91.7531 160.228 91.8916 160.006 92.1686C159.784 92.4456 159.674 92.8057 159.674 93.2489V94.4678C159.674 94.911 159.784 95.2711 160.006 95.5481C160.228 95.8251 160.532 95.9636 160.92 95.9636C161.308 95.9636 161.419 95.9359 161.585 95.8805C161.751 95.8251 161.862 95.742 161.973 95.6589V94.5232H160.837V93.7199H163.136V95.9913H163.081Z" fill="#231F20"/>
<path d="M171.96 94.1354H169.799V95.7974H172.375V96.6838H168.636V90.8667H172.375V91.7808H169.799V93.2489H171.96V94.1631V94.1354Z" fill="#231F20"/>
<path d="M182.556 96.7115H181.393L179.038 92.778V96.7115H177.875V90.8667H179.038L181.393 94.8002V90.8667H182.556V96.7115Z" fill="#231F20"/>
<path d="M192.572 94.8556C192.572 95.4927 192.406 95.9913 191.99 96.3514C191.602 96.7115 191.048 96.8777 190.383 96.8777C189.719 96.8777 189.109 96.6561 188.694 96.2129C188.251 95.7697 188.057 95.188 188.057 94.4678V93.3043C188.057 92.5841 188.278 92.0024 188.694 91.5592C189.109 91.116 189.663 90.8667 190.356 90.8667C191.048 90.8667 191.63 91.0329 192.018 91.393C192.433 91.7531 192.627 92.224 192.6 92.8611H191.464C191.464 92.4733 191.381 92.1963 191.187 92.0301C190.993 91.8362 190.716 91.7531 190.356 91.7531C189.996 91.7531 189.719 91.8916 189.525 92.1686C189.331 92.4456 189.22 92.8057 189.22 93.2766V94.4401C189.22 94.8833 189.331 95.2434 189.525 95.5481C189.719 95.8251 190.023 95.9636 190.383 95.9636C190.744 95.9636 190.993 95.8805 191.159 95.6866C191.325 95.4927 191.408 95.2157 191.408 94.8279H192.544L192.572 94.8556Z" fill="#231F20"/>
<path d="M201.427 94.1354H199.266V95.7974H201.842V96.6838H198.103V90.8667H201.842V91.7808H199.266V93.2489H201.427V94.1631V94.1354Z" fill="#231F20"/>
<defs>
<linearGradient id="paint0_linear_13943_20144" x1="134.924" y1="6.14538" x2="176.565" y2="53.3515" gradientUnits="userSpaceOnUse">
<stop stop-color="#B52B9D"/>
<stop offset="1" stop-color="#8393CF"/>
</linearGradient>
<linearGradient id="paint1_linear_13943_20144" x1="134.924" y1="6.14538" x2="176.565" y2="53.3515" gradientUnits="userSpaceOnUse">
<stop stop-color="#B52B9D"/>
<stop offset="1" stop-color="#8393CF"/>
</linearGradient>
<linearGradient id="paint2_linear_13943_20144" x1="134.924" y1="6.14538" x2="176.565" y2="53.3515" gradientUnits="userSpaceOnUse">
<stop stop-color="#B52B9D"/>
<stop offset="1" stop-color="#8393CF"/>
</linearGradient>
<linearGradient id="paint3_linear_13943_20144" x1="134.924" y1="6.14538" x2="176.565" y2="53.3515" gradientUnits="userSpaceOnUse">
<stop stop-color="#B52B9D"/>
<stop offset="1" stop-color="#8393CF"/>
</linearGradient>
<linearGradient id="paint4_linear_13943_20144" x1="134.924" y1="6.14538" x2="176.565" y2="53.3515" gradientUnits="userSpaceOnUse">
<stop stop-color="#B52B9D"/>
<stop offset="1" stop-color="#8393CF"/>
</linearGradient>
<linearGradient id="paint5_linear_13943_20144" x1="134.924" y1="6.14538" x2="176.565" y2="53.3515" gradientUnits="userSpaceOnUse">
<stop stop-color="#B52B9D"/>
<stop offset="1" stop-color="#8393CF"/>
</linearGradient>
<linearGradient id="paint6_linear_13943_20144" x1="134.924" y1="6.14538" x2="176.565" y2="53.3515" gradientUnits="userSpaceOnUse">
<stop stop-color="#B52B9D"/>
<stop offset="1" stop-color="#8393CF"/>
</linearGradient>
<linearGradient id="paint7_linear_13943_20144" x1="134.924" y1="6.14538" x2="176.565" y2="53.3515" gradientUnits="userSpaceOnUse">
<stop stop-color="#B52B9D"/>
<stop offset="1" stop-color="#8393CF"/>
</linearGradient>
<linearGradient id="paint8_linear_13943_20144" x1="134.924" y1="6.14538" x2="176.565" y2="53.3515" gradientUnits="userSpaceOnUse">
<stop stop-color="#B52B9D"/>
<stop offset="1" stop-color="#8393CF"/>
</linearGradient>
<linearGradient id="paint9_linear_13943_20144" x1="134.924" y1="6.14538" x2="176.565" y2="53.3515" gradientUnits="userSpaceOnUse">
<stop stop-color="#B52B9D"/>
<stop offset="1" stop-color="#8393CF"/>
</linearGradient>
<linearGradient id="paint10_linear_13943_20144" x1="134.924" y1="6.14538" x2="176.565" y2="53.3515" gradientUnits="userSpaceOnUse">
<stop stop-color="#B52B9D"/>
<stop offset="1" stop-color="#8393CF"/>
</linearGradient>
<linearGradient id="paint11_linear_13943_20144" x1="134.924" y1="6.14538" x2="176.565" y2="53.3515" gradientUnits="userSpaceOnUse">
<stop stop-color="#B52B9D"/>
<stop offset="1" stop-color="#8393CF"/>
</linearGradient>
<linearGradient id="paint12_linear_13943_20144" x1="134.924" y1="6.14538" x2="176.565" y2="53.3515" gradientUnits="userSpaceOnUse">
<stop stop-color="#B52B9D"/>
<stop offset="1" stop-color="#8393CF"/>
</linearGradient>
<linearGradient id="paint13_linear_13943_20144" x1="134.924" y1="6.14538" x2="176.565" y2="53.3515" gradientUnits="userSpaceOnUse">
<stop stop-color="#B52B9D"/>
<stop offset="1" stop-color="#8393CF"/>
</linearGradient>
<linearGradient id="paint14_linear_13943_20144" x1="134.924" y1="6.14538" x2="176.565" y2="53.3515" gradientUnits="userSpaceOnUse">
<stop stop-color="#B52B9D"/>
<stop offset="1" stop-color="#8393CF"/>
</linearGradient>
<linearGradient id="paint15_linear_13943_20144" x1="134.924" y1="6.14538" x2="176.565" y2="53.3515" gradientUnits="userSpaceOnUse">
<stop stop-color="#B52B9D"/>
<stop offset="1" stop-color="#8393CF"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 15 KiB

+84
View File
@@ -0,0 +1,84 @@
<svg width="62" height="62" viewBox="0 0 62 62" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M29.837 7.64759C29.837 11.8811 29.9738 16.1145 29.837 20.348C29.7001 23.489 27.6471 25.8106 25.1835 27.7225C24.7729 27.9956 24.3623 28.2687 23.678 28.815C24.9098 29.6344 25.8678 30.3172 26.9628 31.1366H0C0.4106 31 0.684334 30.8634 0.821201 30.8634C4.79034 30.0441 9.03321 29.7709 12.8655 28.1322C21.8987 24.3084 27.5102 17.6168 29.7001 8.05728L29.837 7.64759Z" fill="url(#paint0_linear_13943_20143)"/>
<path d="M33.6692 23.3524C33.2586 23.8987 32.7112 24.4449 32.4374 24.8546C32.0268 25.4009 31.89 25.9471 31.4794 26.63V0C31.4794 0 31.4794 0 31.6162 0C32.0268 2.04846 32.3006 3.96035 32.7112 6.00881C34.4904 16.3877 40.2388 23.7621 49.9564 27.859C51.5988 28.5419 53.5149 28.9515 55.2942 29.6344C55.1573 29.6344 55.0204 29.6344 54.8836 29.6344C51.4619 29.6344 48.1771 29.6344 44.7554 29.6344C40.2388 29.7709 36.954 27.7225 34.3536 24.0352C34.3536 23.8987 34.0798 23.6256 33.943 23.489C33.943 23.489 33.943 23.489 33.6692 23.3524Z" fill="url(#paint1_linear_13943_20143)"/>
<path d="M8.48601 32.3656C12.1814 32.3656 16.0137 32.3656 19.7091 32.3656C23.2676 32.3656 25.8681 34.4141 27.9211 37.1454C28.1948 37.5551 28.4686 37.9648 29.016 38.6476C29.8372 37.5551 30.5216 36.4626 31.3428 35.5066V62C30.9322 59.815 30.6584 57.7665 30.2478 55.5815C28.058 43.5639 21.0778 35.9163 9.17034 32.6388C9.03347 32.6388 8.75974 32.6388 8.62288 32.5022C8.62288 32.5022 8.62288 32.5022 8.62288 32.3656H8.48601Z" fill="url(#paint2_linear_13943_20143)"/>
<path d="M32.8474 53.8062C32.8474 49.8458 32.7105 45.8855 32.8474 41.9251C32.9842 38.6476 34.9004 36.1894 37.5008 34.2775C37.9114 34.0044 38.322 33.7313 39.0064 33.185C37.9114 32.3656 36.8165 31.6828 35.7216 31H62C60.2207 31.2731 58.5783 31.6828 56.7991 31.9559C50.3663 32.9119 44.7548 35.5066 40.2382 40.1498C36.6796 43.7004 34.3529 47.9339 33.1211 52.8502C33.1211 53.1234 32.9842 53.3965 32.8474 53.8062Z" fill="url(#paint3_linear_13943_20143)"/>
<path d="M29.837 7.64759C29.837 11.8811 29.9738 16.1145 29.837 20.348C29.7001 23.489 27.6471 25.8106 25.1835 27.7225C24.7729 27.9956 24.3623 28.2687 23.678 28.815C24.9098 29.6344 25.8678 30.3172 26.9628 31.1366H0C0.4106 31 0.684334 30.8634 0.821201 30.8634C4.79034 30.0441 9.03321 29.7709 12.8655 28.1322C21.8987 24.3084 27.5102 17.6168 29.7001 8.05728L29.837 7.64759Z" fill="url(#paint4_linear_13943_20143)"/>
<path d="M33.6692 23.3524C33.2586 23.8987 32.7112 24.4449 32.4374 24.8546C32.0268 25.4009 31.89 25.9471 31.4794 26.63V0C31.4794 0 31.4794 0 31.6162 0C32.0268 2.04846 32.3006 3.96035 32.7112 6.00881C34.4904 16.3877 40.2388 23.7621 49.9564 27.859C51.5988 28.5419 53.5149 28.9515 55.2942 29.6344C55.1573 29.6344 55.0204 29.6344 54.8836 29.6344C51.4619 29.6344 48.1771 29.6344 44.7554 29.6344C40.2388 29.7709 36.954 27.7225 34.3536 24.0352C34.3536 23.8987 34.0798 23.6256 33.943 23.489C33.943 23.489 33.943 23.489 33.6692 23.3524Z" fill="url(#paint5_linear_13943_20143)"/>
<path d="M8.48601 32.3656C12.1814 32.3656 16.0137 32.3656 19.7091 32.3656C23.2676 32.3656 25.8681 34.4141 27.9211 37.1454C28.1948 37.5551 28.4686 37.9648 29.016 38.6476C29.8372 37.5551 30.5216 36.4626 31.3428 35.5066V62C30.9322 59.815 30.6584 57.7665 30.2478 55.5815C28.058 43.5639 21.0778 35.9163 9.17034 32.6388C9.03347 32.6388 8.75974 32.6388 8.62288 32.5022C8.62288 32.5022 8.62288 32.5022 8.62288 32.3656H8.48601Z" fill="url(#paint6_linear_13943_20143)"/>
<path d="M32.8474 53.8062C32.8474 49.8458 32.7105 45.8855 32.8474 41.9251C32.9842 38.6476 34.9004 36.1894 37.5008 34.2775C37.9114 34.0044 38.322 33.7313 39.0064 33.185C37.9114 32.3656 36.8165 31.6828 35.7216 31H62C60.2207 31.2731 58.5783 31.6828 56.7991 31.9559C50.3663 32.9119 44.7548 35.5066 40.2382 40.1498C36.6796 43.7004 34.3529 47.9339 33.1211 52.8502C33.1211 53.1234 32.9842 53.3965 32.8474 53.8062Z" fill="url(#paint7_linear_13943_20143)"/>
<path d="M29.837 7.64759C29.837 11.8811 29.9738 16.1145 29.837 20.348C29.7001 23.489 27.6471 25.8106 25.1835 27.7225C24.7729 27.9956 24.3623 28.2687 23.678 28.815C24.9098 29.6344 25.8678 30.3172 26.9628 31.1366H0C0.4106 31 0.684334 30.8634 0.821201 30.8634C4.79034 30.0441 9.03321 29.7709 12.8655 28.1322C21.8987 24.3084 27.5102 17.6168 29.7001 8.05728L29.837 7.64759Z" fill="url(#paint8_linear_13943_20143)"/>
<path d="M33.6692 23.3524C33.2586 23.8987 32.7112 24.4449 32.4374 24.8546C32.0268 25.4009 31.89 25.9471 31.4794 26.63V0C31.4794 0 31.4794 0 31.6162 0C32.0268 2.04846 32.3006 3.96035 32.7112 6.00881C34.4904 16.3877 40.2388 23.7621 49.9564 27.859C51.5988 28.5419 53.5149 28.9515 55.2942 29.6344C55.1573 29.6344 55.0204 29.6344 54.8836 29.6344C51.4619 29.6344 48.1771 29.6344 44.7554 29.6344C40.2388 29.7709 36.954 27.7225 34.3536 24.0352C34.3536 23.8987 34.0798 23.6256 33.943 23.489C33.943 23.489 33.943 23.489 33.6692 23.3524Z" fill="url(#paint9_linear_13943_20143)"/>
<path d="M8.48601 32.3656C12.1814 32.3656 16.0137 32.3656 19.7091 32.3656C23.2676 32.3656 25.8681 34.4141 27.9211 37.1454C28.1948 37.5551 28.4686 37.9648 29.016 38.6476C29.8372 37.5551 30.5216 36.4626 31.3428 35.5066V62C30.9322 59.815 30.6584 57.7665 30.2478 55.5815C28.058 43.5639 21.0778 35.9163 9.17034 32.6388C9.03347 32.6388 8.75974 32.6388 8.62288 32.5022C8.62288 32.5022 8.62288 32.5022 8.62288 32.3656H8.48601Z" fill="url(#paint10_linear_13943_20143)"/>
<path d="M32.8474 53.8062C32.8474 49.8458 32.7105 45.8855 32.8474 41.9251C32.9842 38.6476 34.9004 36.1894 37.5008 34.2775C37.9114 34.0044 38.322 33.7313 39.0064 33.185C37.9114 32.3656 36.8165 31.6828 35.7216 31H62C60.2207 31.2731 58.5783 31.6828 56.7991 31.9559C50.3663 32.9119 44.7548 35.5066 40.2382 40.1498C36.6796 43.7004 34.3529 47.9339 33.1211 52.8502C33.1211 53.1234 32.9842 53.3965 32.8474 53.8062Z" fill="url(#paint11_linear_13943_20143)"/>
<path d="M29.837 7.64759C29.837 11.8811 29.9738 16.1145 29.837 20.348C29.7001 23.489 27.6471 25.8106 25.1835 27.7225C24.7729 27.9956 24.3623 28.2687 23.678 28.815C24.9098 29.6344 25.8678 30.3172 26.9628 31.1366H0C0.4106 31 0.684334 30.8634 0.821201 30.8634C4.79034 30.0441 9.03321 29.7709 12.8655 28.1322C21.8987 24.3084 27.5102 17.6168 29.7001 8.05728L29.837 7.64759Z" fill="url(#paint12_linear_13943_20143)"/>
<path d="M33.6692 23.3524C33.2586 23.8987 32.7112 24.4449 32.4374 24.8546C32.0268 25.4009 31.89 25.9471 31.4794 26.63V0C31.4794 0 31.4794 0 31.6162 0C32.0268 2.04846 32.3006 3.96035 32.7112 6.00881C34.4904 16.3877 40.2388 23.7621 49.9564 27.859C51.5988 28.5419 53.5149 28.9515 55.2942 29.6344C55.1573 29.6344 55.0204 29.6344 54.8836 29.6344C51.4619 29.6344 48.1771 29.6344 44.7554 29.6344C40.2388 29.7709 36.954 27.7225 34.3536 24.0352C34.3536 23.8987 34.0798 23.6256 33.943 23.489C33.943 23.489 33.943 23.489 33.6692 23.3524Z" fill="url(#paint13_linear_13943_20143)"/>
<path d="M8.48601 32.3656C12.1814 32.3656 16.0137 32.3656 19.7091 32.3656C23.2676 32.3656 25.8681 34.4141 27.9211 37.1454C28.1948 37.5551 28.4686 37.9648 29.016 38.6476C29.8372 37.5551 30.5216 36.4626 31.3428 35.5066V62C30.9322 59.815 30.6584 57.7665 30.2478 55.5815C28.058 43.5639 21.0778 35.9163 9.17034 32.6388C9.03347 32.6388 8.75974 32.6388 8.62288 32.5022C8.62288 32.5022 8.62288 32.5022 8.62288 32.3656H8.48601Z" fill="url(#paint14_linear_13943_20143)"/>
<path d="M32.8474 53.8062C32.8474 49.8458 32.7105 45.8855 32.8474 41.9251C32.9842 38.6476 34.9004 36.1894 37.5008 34.2775C37.9114 34.0044 38.322 33.7313 39.0064 33.185C37.9114 32.3656 36.8165 31.6828 35.7216 31H62C60.2207 31.2731 58.5783 31.6828 56.7991 31.9559C50.3663 32.9119 44.7548 35.5066 40.2382 40.1498C36.6796 43.7004 34.3529 47.9339 33.1211 52.8502C33.1211 53.1234 32.9842 53.3965 32.8474 53.8062Z" fill="url(#paint15_linear_13943_20143)"/>
<defs>
<linearGradient id="paint0_linear_13943_20143" x1="16.424" y1="6.14538" x2="58.0646" y2="53.3515" gradientUnits="userSpaceOnUse">
<stop stop-color="#B52B9D"/>
<stop offset="1" stop-color="#8393CF"/>
</linearGradient>
<linearGradient id="paint1_linear_13943_20143" x1="16.424" y1="6.14538" x2="58.0646" y2="53.3515" gradientUnits="userSpaceOnUse">
<stop stop-color="#B52B9D"/>
<stop offset="1" stop-color="#8393CF"/>
</linearGradient>
<linearGradient id="paint2_linear_13943_20143" x1="16.424" y1="6.14538" x2="58.0646" y2="53.3515" gradientUnits="userSpaceOnUse">
<stop stop-color="#B52B9D"/>
<stop offset="1" stop-color="#8393CF"/>
</linearGradient>
<linearGradient id="paint3_linear_13943_20143" x1="16.424" y1="6.14538" x2="58.0646" y2="53.3515" gradientUnits="userSpaceOnUse">
<stop stop-color="#B52B9D"/>
<stop offset="1" stop-color="#8393CF"/>
</linearGradient>
<linearGradient id="paint4_linear_13943_20143" x1="16.424" y1="6.14538" x2="58.0646" y2="53.3515" gradientUnits="userSpaceOnUse">
<stop stop-color="#B52B9D"/>
<stop offset="1" stop-color="#8393CF"/>
</linearGradient>
<linearGradient id="paint5_linear_13943_20143" x1="16.424" y1="6.14538" x2="58.0646" y2="53.3515" gradientUnits="userSpaceOnUse">
<stop stop-color="#B52B9D"/>
<stop offset="1" stop-color="#8393CF"/>
</linearGradient>
<linearGradient id="paint6_linear_13943_20143" x1="16.424" y1="6.14538" x2="58.0646" y2="53.3515" gradientUnits="userSpaceOnUse">
<stop stop-color="#B52B9D"/>
<stop offset="1" stop-color="#8393CF"/>
</linearGradient>
<linearGradient id="paint7_linear_13943_20143" x1="16.424" y1="6.14538" x2="58.0646" y2="53.3515" gradientUnits="userSpaceOnUse">
<stop stop-color="#B52B9D"/>
<stop offset="1" stop-color="#8393CF"/>
</linearGradient>
<linearGradient id="paint8_linear_13943_20143" x1="16.424" y1="6.14538" x2="58.0646" y2="53.3515" gradientUnits="userSpaceOnUse">
<stop stop-color="#B52B9D"/>
<stop offset="1" stop-color="#8393CF"/>
</linearGradient>
<linearGradient id="paint9_linear_13943_20143" x1="16.424" y1="6.14538" x2="58.0646" y2="53.3515" gradientUnits="userSpaceOnUse">
<stop stop-color="#B52B9D"/>
<stop offset="1" stop-color="#8393CF"/>
</linearGradient>
<linearGradient id="paint10_linear_13943_20143" x1="16.424" y1="6.14538" x2="58.0646" y2="53.3515" gradientUnits="userSpaceOnUse">
<stop stop-color="#B52B9D"/>
<stop offset="1" stop-color="#8393CF"/>
</linearGradient>
<linearGradient id="paint11_linear_13943_20143" x1="16.424" y1="6.14538" x2="58.0646" y2="53.3515" gradientUnits="userSpaceOnUse">
<stop stop-color="#B52B9D"/>
<stop offset="1" stop-color="#8393CF"/>
</linearGradient>
<linearGradient id="paint12_linear_13943_20143" x1="16.424" y1="6.14538" x2="58.0646" y2="53.3515" gradientUnits="userSpaceOnUse">
<stop stop-color="#B52B9D"/>
<stop offset="1" stop-color="#8393CF"/>
</linearGradient>
<linearGradient id="paint13_linear_13943_20143" x1="16.424" y1="6.14538" x2="58.0646" y2="53.3515" gradientUnits="userSpaceOnUse">
<stop stop-color="#B52B9D"/>
<stop offset="1" stop-color="#8393CF"/>
</linearGradient>
<linearGradient id="paint14_linear_13943_20143" x1="16.424" y1="6.14538" x2="58.0646" y2="53.3515" gradientUnits="userSpaceOnUse">
<stop stop-color="#B52B9D"/>
<stop offset="1" stop-color="#8393CF"/>
</linearGradient>
<linearGradient id="paint15_linear_13943_20143" x1="16.424" y1="6.14538" x2="58.0646" y2="53.3515" gradientUnits="userSpaceOnUse">
<stop stop-color="#B52B9D"/>
<stop offset="1" stop-color="#8393CF"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 11 KiB

+109 -150
View File
@@ -1,41 +1,32 @@
'use client'
import AnimatedBottomSheet from '@/components/bottomsheet/AnimatedBottomSheet';
import EqualizerIcon from '@/components/icons/EqualizerIcon';
import TelegramIcon from '@/components/icons/TelegramIcon';
import TabContainer from '@/components/tab/TabContainer';
import { TabHeader } from '@/components/tab/TabHeader';
import Comment from '@/components/utils/Comment';
import RateBar from '@/components/utils/RateBar';
import useToggle from '@/hooks/helpers/useToggle';
import { ArrowDown2, CallCalling, Clock, Gallery, InfoCircle, Instagram, Location, Star1, Whatsapp } from 'iconsax-react';
import { useQueryState } from 'next-usequerystate';
import Image from 'next/image';
import React from 'react'
import { useGetAbout, useGetReviews, useGetSchedules } from './hooks/useAboutData';
import { isReviewApproved } from './types/Types';
import AboutSkeleton from './components/AboutSkeleton';
import { glassSurfaceCard, glassSurfaceCardFlat, glassSurfaceFlat } from '@/lib/styles/glassSurface';
"use client";
import AnimatedBottomSheet from "@/components/bottomsheet/AnimatedBottomSheet";
import EqualizerIcon from "@/components/icons/EqualizerIcon";
import TelegramIcon from "@/components/icons/TelegramIcon";
import TabContainer from "@/components/tab/TabContainer";
import { TabHeader } from "@/components/tab/TabHeader";
import Comment from "@/components/utils/Comment";
import RateBar from "@/components/utils/RateBar";
import useToggle from "@/hooks/helpers/useToggle";
import { glassSurfaceCard, glassSurfaceCardFlat, glassSurfaceFlat } from "@/lib/styles/glassSurface";
import { ArrowDown2, CallCalling, Clock, Gallery, InfoCircle, Instagram, Location, Star1, Whatsapp } from "iconsax-react";
import { useQueryState } from "next-usequerystate";
import Image from "next/image";
import AboutSkeleton from "./components/AboutSkeleton";
import { useGetAbout, useGetReviews, useGetSchedules } from "./hooks/useAboutData";
import { isReviewApproved } from "./types/Types";
const sortings = [
'جدیدترین',
'قدیمی ترین'
]
const sortings = ["جدیدترین", "قدیمی ترین"];
function AboutPage() {
const { data: aboutData, isLoading: aboutLoading } = useGetAbout();
const { data: reviewsData, isLoading: reviewsLoading } = useGetReviews();
const { data: schedulesData, isLoading: schedulesLoading } = useGetSchedules();
const [sorting, setSorting] = useQueryState('sortBy', { defaultValue: '0' });
const [sorting, setSorting] = useQueryState("sortBy", { defaultValue: "0" });
const { state: sortingModal, toggle: toggleSortingModal, set: setSortingModal } = useToggle();
const restaurant = aboutData?.data;
const schedules = schedulesData?.data || [];
const phoneList = Array.isArray(restaurant?.phones)
? restaurant.phones.filter((phone): phone is string => Boolean(phone))
: restaurant?.phone
? [restaurant.phone]
: [];
const phoneList = Array.isArray(restaurant?.phones) ? restaurant.phones.filter((phone): phone is string => Boolean(phone)) : restaurant?.phone ? [restaurant.phone] : [];
const isLoading = aboutLoading || reviewsLoading || schedulesLoading;
@@ -46,22 +37,22 @@ function AboutPage() {
const changeSorting = (index: number) => {
setSorting(() => String(index));
setSortingModal(false);
}
};
const formatTime = (time: string): string => {
return time.substring(0, 5);
};
const getDayName = (weekDay: number): string => {
const days = ['یکشنبه', 'دوشنبه', 'سه‌شنبه', 'چهارشنبه', 'پنج‌شنبه', 'جمعه', 'شنبه'];
return days[weekDay] || '';
const days = ["یکشنبه", "دوشنبه", "سه‌شنبه", "چهارشنبه", "پنج‌شنبه", "جمعه", "شنبه"];
return days[weekDay] || "";
};
const groupSchedulesByDay = () => {
const grouped: Record<number, typeof schedules> = {};
schedules
.filter(schedule => schedule.isActive)
.forEach(schedule => {
.filter((schedule) => schedule.isActive)
.forEach((schedule) => {
if (!grouped[schedule.weekDay]) {
grouped[schedule.weekDay] = [];
}
@@ -72,9 +63,7 @@ function AboutPage() {
const getTodaySchedule = () => {
const today = new Date().getDay();
const todaySchedules = schedules.filter(schedule =>
schedule.isActive && schedule.weekDay === today
);
const todaySchedules = schedules.filter((schedule) => schedule.isActive && schedule.weekDay === today);
if (todaySchedules.length > 0) {
const firstSchedule = todaySchedules[0];
return `${formatTime(firstSchedule.openTime)} تا ${formatTime(firstSchedule.closeTime)}`;
@@ -86,42 +75,30 @@ function AboutPage() {
if (!restaurant) return null;
return (
<section aria-labelledby="about-title" className='py-4'>
<section
className={glassSurfaceCard('p-4')}>
<div className="flex justify-between items-center border-b-[1.5px] border-gray-200 pb-[25px]">
<section aria-labelledby="about-title" className="py-4">
<section className={glassSurfaceCard("p-4")}>
<div className="flex justify-between items-center border-b-[1.5px] border-gray-200 pb-6.25">
<div className="">
<h2 className='text-sm2 font-bold leading-5'>{restaurant.name}</h2>
{restaurant.establishedYear && (
<p className="text-sm2 leading-5 mt-4">تاسیس: {restaurant.establishedYear}</p>
)}
{restaurant.tagNames && restaurant.tagNames.length > 0 && (
<p className="text-sm2 leading-5 mt-2">نوع محصولات: {restaurant.tagNames.join('، ')}</p>
)}
<h2 className="text-sm2 font-bold leading-5">{restaurant.name}</h2>
{restaurant.establishedYear && <p className="text-sm2 leading-5 mt-4">تاسیس: {restaurant.establishedYear}</p>}
{restaurant.tagNames && restaurant.tagNames.length > 0 && <p className="text-sm2 leading-5 mt-2">نوع محصولات: {restaurant.tagNames.join("، ")}</p>}
</div>
{restaurant.logo && (
<div className="rounded-normal overflow-clip">
<Image
alt='logo'
src={restaurant.logo}
width={88}
height={88}
unoptimized
priority
/>
<Image alt="logo" src={restaurant.logo} width={88} height={88} unoptimized priority />
</div>
)}
</div>
{restaurant.description && (
<div className="mt-[23px]">
<div className="mt-5.75">
<h3 className="text-sm2 font-bold leading-5">درباره مجموعه</h3>
<p className="text-sm2 leading-5 mt-3 border-spacing-48" style={{ wordSpacing: '0.01em' }}>
<p className="text-sm2 leading-5 mt-3 border-spacing-48" style={{ wordSpacing: "0.01em" }}>
{restaurant.description}
</p>
{restaurant.images && restaurant.images.length > 0 && (
<div className='inline-flex gap-2 mt-[23px] items-center'>
<Gallery size={20} className='stroke-disabled-text' />
<span className='text-sm2 text-disabled-text font-medium pt-0.5'>عکس های رستوران</span>
<div className="inline-flex gap-2 mt-5.75 items-center">
<Gallery size={20} className="stroke-disabled-text" />
<span className="text-sm2 text-disabled-text font-medium pt-0.5">عکس های رستوران</span>
</div>
)}
</div>
@@ -129,60 +106,59 @@ function AboutPage() {
</section>
{(phoneList.length > 0 || restaurant.telegram || restaurant.whatsapp || restaurant.instagram) && (
<section
className={glassSurfaceCard('pt-3 pb-3.5 px-[16px] mt-4 grid grid-cols-3 items-center')}>
<h2 className='text-sm2 font-medium leading-5'>ارتباط</h2>
<div className='col-span-2 text-center flex justify-center flex-wrap gap-2'>
<section className={glassSurfaceCard("pt-3 pb-3.5 px-4 mt-4 grid grid-cols-3 items-center")}>
<h2 className="text-sm2 font-medium leading-5">ارتباط</h2>
<div className="col-span-2 text-center flex justify-center flex-wrap gap-2">
{phoneList.map((phone) => (
<a key={phone} href={`tel://${phone}`} className='bg-[#EAEDF5] dark:bg-neutral-700 px-2 py-1.5 rounded-normal inline-flex items-center gap-1.5'>
<CallCalling className='stroke-foreground' size={24} />
<span dir='ltr' className='text-xs leading-5'>{phone}</span>
<a key={phone} href={`tel://${phone}`} className="bg-[#EAEDF5] dark:bg-neutral-700 px-2 py-1.5 rounded-normal inline-flex items-center gap-1.5">
<CallCalling className="stroke-foreground" size={24} />
<span dir="ltr" className="text-xs leading-5">
{phone}
</span>
</a>
))}
{restaurant.telegram &&
<a href={`https://t.me/${restaurant.telegram}`} className='bg-[#EAEDF5] dark:bg-neutral-700 p-2 rounded-normal'>
<TelegramIcon className='stroke-foreground' width={24} height={24} />
{restaurant.telegram && (
<a href={`https://t.me/${restaurant.telegram}`} className="bg-[#EAEDF5] dark:bg-neutral-700 p-2 rounded-normal">
<TelegramIcon className="stroke-foreground" width={24} height={24} />
</a>
}
{restaurant.whatsapp &&
<a href={`https://whatsapp.com/?phone=${restaurant.whatsapp}`} className='bg-[#EAEDF5] dark:bg-neutral-700 p-2 rounded-normal'>
<Whatsapp className='stroke-foreground' size={24} />
)}
{restaurant.whatsapp && (
<a href={`https://whatsapp.com/?phone=${restaurant.whatsapp}`} className="bg-[#EAEDF5] dark:bg-neutral-700 p-2 rounded-normal">
<Whatsapp className="stroke-foreground" size={24} />
</a>
}
{restaurant.instagram &&
<a href={`https://instagram.com/${restaurant.instagram}`} className='bg-[#EAEDF5] dark:bg-neutral-700 p-2 rounded-normal'>
<Instagram className='stroke-foreground' size={24} />
)}
{restaurant.instagram && (
<a href={`https://instagram.com/${restaurant.instagram}`} className="bg-[#EAEDF5] dark:bg-neutral-700 p-2 rounded-normal">
<Instagram className="stroke-foreground" size={24} />
</a>
}
)}
</div>
</section>
)}
{restaurant.address && (
<section className={glassSurfaceCard('px-4 pt-6 pb-6 mt-4')}>
<h2 className='text-sm2 font-medium leading-5'>آدرس</h2>
<p className='text-sm2 mt-[9px] leading-5'>{restaurant.address}</p>
<section className={glassSurfaceCard("px-4 pt-6 pb-6 mt-4")}>
<h2 className="text-sm2 font-medium leading-5">آدرس</h2>
<p className="text-sm2 mt-2.25 leading-5">{restaurant.address}</p>
{restaurant.latitude && restaurant.longitude && (
<div className='inline-flex gap-2 mt-[23px] items-center '>
<Location size={20} className='stroke-disabled-text' />
<span className='text-sm2 text-disabled-text '>موقعیت روی نقشه</span>
<div className="inline-flex gap-2 mt-5.75 items-center ">
<Location size={20} className="stroke-disabled-text" />
<span className="text-sm2 text-disabled-text ">موقعیت روی نقشه</span>
</div>
)}
</section>
)}
{schedules.length > 0 && getTodaySchedule() && (
<section
aria-label='ساعات کاری'
className={glassSurfaceCard('py-6 px-4 mt-4 flex justify-between items-center')}>
<section aria-label="ساعات کاری" className={glassSurfaceCard("py-6 px-4 mt-4 flex justify-between items-center")}>
<div className="flex items-center gap-2 justify-start">
<Clock size={16} className='stroke-disabled-text' />
<div className='text-sm2 font-medium leading-5 mt-0.5 text-[#8C90A3]'>باز</div>
<div className='size-1.5 bg-[#D9D9D9] rounded-full'></div>
<div className='text-sm2 mt-0.5'>امروز از ساعت {getTodaySchedule()}</div>
<Clock size={16} className="stroke-disabled-text" />
<div className="text-sm2 font-medium leading-5 mt-0.5 text-[#8C90A3]">باز</div>
<div className="size-1.5 bg-[#D9D9D9] rounded-full"></div>
<div className="text-sm2 mt-0.5">امروز از ساعت {getTodaySchedule()}</div>
</div>
<div className="">
<ArrowDown2 size={16} className='stroke-[#292D32]' />
<ArrowDown2 size={16} className="stroke-[#292D32]" />
</div>
</section>
)}
@@ -192,17 +168,13 @@ function AboutPage() {
{Object.entries(groupSchedulesByDay())
.sort(([a], [b]) => Number(a) - Number(b))
.map(([weekDay, daySchedules]) => (
<div
key={weekDay}
className={glassSurfaceCardFlat('leading-5 py-6 px-4 mt-4 flex justify-between items-center')}>
<div className="text-sm2">
{getDayName(Number(weekDay))}
</div>
<div className='text-sm2 font-light'>
<div key={weekDay} className={glassSurfaceCardFlat("leading-5 py-6 px-4 mt-4 flex justify-between items-center")}>
<div className="text-sm2">{getDayName(Number(weekDay))}</div>
<div className="text-sm2 font-light">
{daySchedules.map((schedule, index) => (
<span key={schedule.id}>
{formatTime(schedule.openTime)} - {formatTime(schedule.closeTime)}
{index < daySchedules.length - 1 && '، '}
{index < daySchedules.length - 1 && "، "}
</span>
))}
</div>
@@ -211,15 +183,15 @@ function AboutPage() {
</section>
)}
</section>
)
}
);
};
const formatDate = (dateString: string): string => {
const date = new Date(dateString);
return date.toLocaleDateString('fa-IR', {
year: 'numeric',
month: 'long',
day: 'numeric'
return date.toLocaleDateString("fa-IR", {
year: "numeric",
month: "long",
day: "numeric",
});
};
@@ -227,7 +199,7 @@ function AboutPage() {
const reviews = (reviewsData?.data || []).filter(isReviewApproved);
const sortedReviews = [...reviews].sort((a, b) => {
if (sorting === '0') {
if (sorting === "0") {
// جدیدترین
return new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime();
} else {
@@ -236,16 +208,14 @@ function AboutPage() {
}
});
const averageRating = reviews.length > 0
? reviews.reduce((sum, review) => sum + review.rating, 0) / reviews.length
: 0;
const averageRating = reviews.length > 0 ? reviews.reduce((sum, review) => sum + review.rating, 0) / reviews.length : 0;
const ratingCounts = {
5: reviews.filter(r => r.rating === 5).length,
4: reviews.filter(r => r.rating === 4).length,
3: reviews.filter(r => r.rating === 3).length,
2: reviews.filter(r => r.rating === 2).length,
1: reviews.filter(r => r.rating === 1).length,
5: reviews.filter((r) => r.rating === 5).length,
4: reviews.filter((r) => r.rating === 4).length,
3: reviews.filter((r) => r.rating === 3).length,
2: reviews.filter((r) => r.rating === 2).length,
1: reviews.filter((r) => r.rating === 1).length,
};
const totalReviews = reviews.length;
@@ -258,27 +228,23 @@ function AboutPage() {
};
return (
<section aria-labelledby="reviews-title" className='py-4'>
<section
aria-label='امتیاز کاربران'
className={glassSurfaceCard('p-4 py-6 grid grid-cols-2 items-center')}>
<div className="text-center font-bold text-5xl">
{averageRating.toFixed(1)}
</div>
<section aria-labelledby="reviews-title" className="py-4">
<section aria-label="امتیاز کاربران" className={glassSurfaceCard("p-4 py-6 grid grid-cols-2 items-center")}>
<div className="text-center font-bold text-5xl">{averageRating.toFixed(1)}</div>
<div className="flex flex-col w-fit items-end">
<RateBar className='mt-1' content='5' percentage={String(ratingPercentages[5])} />
<RateBar className='mt-1' content='4' percentage={String(ratingPercentages[4])} />
<RateBar className='mt-1' content='3' percentage={String(ratingPercentages[3])} />
<RateBar className='mt-1' content='2' percentage={String(ratingPercentages[2])} />
<RateBar className='mt-1' content='1' percentage={String(ratingPercentages[1])} />
<RateBar className="mt-1" content="5" percentage={String(ratingPercentages[5])} />
<RateBar className="mt-1" content="4" percentage={String(ratingPercentages[4])} />
<RateBar className="mt-1" content="3" percentage={String(ratingPercentages[3])} />
<RateBar className="mt-1" content="2" percentage={String(ratingPercentages[2])} />
<RateBar className="mt-1" content="1" percentage={String(ratingPercentages[1])} />
</div>
</section>
<section className={glassSurfaceCard('pt-3 pb-3.5 px-[16px] mt-4')}>
<section className={glassSurfaceCard("pt-3 pb-3.5 px-4 mt-4")}>
<div className="flex justify-between items-center border-b-[1.5px] border-border pb-2">
<h2 className='text-sm2 font-medium leading-5'>نظرات کاربران</h2>
<button onClick={toggleSortingModal} className={glassSurfaceFlat('rounded-xl h-8 pattern-secondary-bg ps-4 pe-2 py-1.5 inline-flex items-center justify-between gap-[7px]')}>
<EqualizerIcon className='dark:text-white' />
<h2 className="text-sm2 font-medium leading-5">نظرات کاربران</h2>
<button onClick={toggleSortingModal} className={glassSurfaceFlat("rounded-xl h-8 pattern-secondary-bg ps-4 pe-2 py-1.5 inline-flex items-center justify-between gap-1.75")}>
<EqualizerIcon className="dark:text-white" />
<span className="text-xs leading-5 font-medium">{sortings[+sorting]}</span>
</button>
</div>
@@ -287,8 +253,8 @@ function AboutPage() {
sortedReviews.map((review) => (
<article key={review.id}>
<Comment
className='pt-8'
user={review.user?.firstName + ' ' + review.user?.lastName}
className="pt-8"
user={review.user?.firstName + " " + review.user?.lastName}
rating={review.rating}
date={formatDate(review.createdAt)}
text={review.comment}
@@ -299,7 +265,7 @@ function AboutPage() {
</article>
))
) : (
<p className='text-sm2 text-center py-8 text-disabled-text'>هنوز نظری ثبت نشده است</p>
<p className="text-sm2 text-center py-8 text-disabled-text">هنوز نظری ثبت نشده است</p>
)}
</div>
</section>
@@ -313,29 +279,22 @@ function AboutPage() {
</div>
{i < sortings.length - 1 && <hr className="border-white/40 dark:border-border mb-4 mt-4" />}
</div>
)
);
})}
</div>
</AnimatedBottomSheet>
</section>
)
}
);
};
return (
<div className='pt-8 h-full overflow-y-auto noscrollbar'>
<div className="pt-8 h-full overflow-y-auto noscrollbar">
<TabContainer>
<TabHeader
viewRenderer={firstTab()}
title='درباره ما' icon={<InfoCircle size={24} />}>
</TabHeader>
<TabHeader
viewRenderer={secondTab()}
title='امتیازات و نظرات' icon={<Star1 size={24} />}>
</TabHeader>
<TabHeader viewRenderer={firstTab()} title="درباره ما" icon={<InfoCircle size={24} />}></TabHeader>
<TabHeader viewRenderer={secondTab()} title="امتیازات و نظرات" icon={<Star1 size={24} />}></TabHeader>
</TabContainer>
</div>
)
);
}
export default AboutPage
export default AboutPage;
@@ -46,6 +46,7 @@ export interface Restaurant {
domain: string;
score: Score;
plan: "base" | "premium";
enableAiChat?: boolean;
bgType?: "pattern" | "custom" | "color";
bgBlur?: number;
bgOpacity?: number;
+201
View File
@@ -0,0 +1,201 @@
"use client";
import { useGetAbout } from "@/app/[name]/(Main)/about/hooks/useAboutData";
import type { Food } from "@/app/[name]/(Main)/types/Types";
import { useGetProfile } from "@/app/[name]/(Profile)/profile/hooks/userProfileData";
import MenuItem from "@/components/listview/MenuItem";
import MenuItemRenderer from "@/components/listview/MenuItemRenderer";
import { toast } from "@/components/Toast";
import { extractErrorMessage } from "@/lib/func";
import { glassSurfaceFlat } from "@/lib/styles/glassSurface";
import { ArrowLeft, Send2 } from "iconsax-react";
import Image from "next/image";
import { useRouter } from "next/navigation";
import { FormEvent, useEffect, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { useChat } from "./hooks/useAiData";
import { AI_SUGGESTIONS } from "./suggestions";
import type { ChatFood } from "./types/Types";
type Message = {
id: string;
role: "user" | "assistant";
content: string;
foods?: ChatFood[];
};
function getGreetingKey() {
const hour = new Date().getHours();
if (hour >= 5 && hour < 12) return "morning";
if (hour >= 12 && hour < 14) return "noon";
if (hour >= 14 && hour < 17) return "afternoon";
return "evening";
}
function toFood(food: ChatFood): Food {
return {
id: food.id,
category: "",
title: food.title,
desc: food.desc,
content: food.content ?? [],
price: food.price,
prepareTime: 0,
isActive: true,
images: food.images ?? [],
inPlaceServe: true,
pickupServe: true,
discount: food.discount ?? 0,
isSpecialOffer: food.isSpecialOffer ?? false,
isAvailable: true,
unavailabilityMessage: null,
};
}
function AiPage() {
const { t } = useTranslation("common", { keyPrefix: "AI" });
const router = useRouter();
const { data: about } = useGetAbout();
const { data: profile, isSuccess } = useGetProfile();
const { mutate: chat, isPending } = useChat();
const [input, setInput] = useState("");
const [messages, setMessages] = useState<Message[]>([]);
const listRef = useRef<HTMLUListElement>(null);
const restaurant = about?.data;
const brandName = restaurant?.name || t("BrandFallback");
const userName = isSuccess && profile?.data?.firstName ? profile.data.firstName : t("GuestName");
useEffect(() => {
listRef.current?.scrollTo({ top: listRef.current.scrollHeight, behavior: "smooth" });
}, [messages, isPending]);
const sendMessage = (text: string) => {
const content = text.trim();
if (!content || isPending) return;
const userMessage: Message = { id: `u-${Date.now()}`, role: "user", content };
setMessages((prev) => [...prev, userMessage]);
setInput("");
chat(
{ question: content },
{
onSuccess: (res) => {
setMessages((prev) => [
...prev,
{
id: `a-${Date.now()}`,
role: "assistant",
content: res.data.answer,
foods: res.data.foods?.length ? res.data.foods : undefined,
},
]);
},
onError: (error) => {
toast(extractErrorMessage(error, t("ErrorFallback")), "error");
},
},
);
};
const onSubmit = (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
sendMessage(input);
};
return (
<section className="flex h-[calc(100svh-6.5rem)] flex-col pt-4 xl:h-[calc(100svh-6rem)]">
<div className="grid shrink-0 grid-cols-3 items-center">
<span />
<h1 className="place-self-center text-sm2 font-medium">{t("Heading")}</h1>
<ArrowLeft className="cursor-pointer place-self-end" size="20" color="currentColor" onClick={() => router.back()} />
</div>
<div className="mt-6 flex min-h-0 flex-1 flex-col">
{messages.length > 0 ? (
<ul ref={listRef} className="noscrollbar flex-1 space-y-3 overflow-y-auto pb-4">
{messages.map((message) => {
const isUser = message.role === "user";
const hasFoods = !isUser && !!message.foods?.length;
return (
<li key={message.id} className={`flex flex-col gap-2 ${isUser ? "items-end" : "items-start"}`}>
<div
className={
isUser
? "max-w-[80%] rounded-[20px] rounded-br-none bg-primary/15 px-4 py-3 dark:bg-primary/25"
: "max-w-[80%] rounded-[20px] rounded-bl-none bg-[#EBEDF5] px-4 py-3 dark:bg-gray-600"
}
>
<p className="text-xs2 leading-5 whitespace-pre-wrap">{message.content}</p>
</div>
{hasFoods && (
<div className="flex w-full flex-col gap-2">
{message.foods!.map((food) => (
<MenuItemRenderer key={food.id}>
<MenuItem food={toFood(food)} />
</MenuItemRenderer>
))}
</div>
)}
</li>
);
})}
{isPending && (
<li className="flex justify-start">
<div className="max-w-[80%] rounded-[20px] rounded-bl-none bg-[#EBEDF5] px-4 py-3 dark:bg-gray-600">
<p className="animate-pulse text-xs2 leading-5 text-disabled-text">{t("Thinking")}</p>
</div>
</li>
)}
</ul>
) : (
<div className="flex flex-1 flex-col items-center justify-center gap-8">
<div className="flex flex-col items-center gap-2">
<Image src={"/assets/images/logo_ai.svg"} alt={brandName} width={299} height={299} className="w-74.75 h-auto" unoptimized />
</div>
<div className="flex w-full flex-col items-center gap-4">
<h2 className="text-base font-medium">{t(`Greeting.${getGreetingKey()}`, { name: userName })}</h2>
<div className="flex w-full flex-wrap gap-2">
{AI_SUGGESTIONS.map(({ labelKey, icon: Icon }) => (
<button
key={labelKey}
type="button"
disabled={isPending}
onClick={() => sendMessage(t(labelKey))}
className={glassSurfaceFlat("inline-flex items-center gap-1.5 rounded-lg px-3 py-1.5 transition-[filter] active:brightness-95 disabled:opacity-50")}
>
<Icon size={15} className="stroke-foreground" variant="Bold" />
<span className="text-xs2">{t(labelKey)}</span>
</button>
))}
</div>
</div>
</div>
)}
<form onSubmit={onSubmit} className={glassSurfaceFlat("mt-3 flex h-12 shrink-0 items-center gap-1 rounded-xl border border-border px-3")}>
<input
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder={t("InputPlaceholder")}
autoComplete="off"
disabled={isPending}
className="h-full w-full bg-transparent text-[11px]! outline-none placeholder:text-disabled-text disabled:opacity-60"
/>
<button type="submit" disabled={!input.trim() || isPending} className="rounded-full p-1.5 disabled:opacity-40">
<Send2 size={20} variant="Bold" className="fill-primary text-primary dark:fill-foreground dark:text-foreground" />
</button>
</form>
</div>
</section>
);
}
export default AiPage;
@@ -0,0 +1,8 @@
import { useMutation } from "@tanstack/react-query";
import * as api from "../service/AiService";
export const useChat = () => {
return useMutation({
mutationFn: api.chat,
});
};
+3
View File
@@ -0,0 +1,3 @@
'use client'
export { default } from './AiPage'
@@ -0,0 +1,7 @@
import { api } from "@/config/axios";
import { ChatRequest, ChatResponse } from "../types/Types";
export const chat = async (params: ChatRequest) => {
const { data } = await api.post<ChatResponse>("/public/ai/chat", params);
return data;
};
+29
View File
@@ -0,0 +1,29 @@
import type { Icon } from 'iconsax-react'
import {
Apple,
Cake,
Chart,
FavoriteChart,
Flash,
Health,
LikeTag,
People,
Timer,
} from 'iconsax-react'
export type Suggestion = {
labelKey: string
icon: Icon
}
export const AI_SUGGESTIONS: Suggestion[] = [
{ labelKey: 'Suggestions.vegetarian', icon: Apple },
{ labelKey: 'Suggestions.healthy', icon: Health },
{ labelKey: 'Suggestions.tasty', icon: LikeTag },
{ labelKey: 'Suggestions.protein', icon: FavoriteChart },
{ labelKey: 'Suggestions.bestsellers', icon: Chart },
{ labelKey: 'Suggestions.spicy', icon: Flash },
{ labelKey: 'Suggestions.quick', icon: Timer },
{ labelKey: 'Suggestions.dessert', icon: Cake },
{ labelKey: 'Suggestions.group', icon: People },
]
+27
View File
@@ -0,0 +1,27 @@
import { BaseResponse } from "@/app/[name]/(Main)/types/Types";
export type ChatRequest = {
question: string;
};
export type ChatFood = {
id: string;
title: string;
desc: string;
content: string[] | null;
price: number;
images: string[];
discount: number;
isSpecialOffer: boolean;
};
export type ChatData = {
answer: string;
foods?: ChatFood[];
consumedTokens: number;
cost: number;
promptTokens: number;
completionTokens: number;
};
export type ChatResponse = BaseResponse<ChatData>;
@@ -183,9 +183,9 @@ const MenuIndex = () => {
<section className="w-full">
<div className="flex flex-wrap gap-2 items-center relative" ref={smallCategoriesRef}>
<span className="sm:text-base text-sm font-medium max-w-[113px] sm:max-w-auto">{selectedCategory === "0" ? "" : categories.find((c) => c.id === selectedCategory)?.title || ""}</span>
<div className="flex min-w-[247px] flex-1 gap-2 justify-end items-center">
<button onClick={toggleFilterModal} className={glassSurfaceFlat("rounded-xl h-8 ps-4 pe-2 py-1.5 inline-flex items-center justify-between gap-[7px]")}>
<span className="sm:text-base text-sm font-medium max-w-28.25 sm:max-w-auto">{selectedCategory === "0" ? "" : categories.find((c) => c.id === selectedCategory)?.title || ""}</span>
<div className="flex min-w-61.75 flex-1 gap-2 justify-end items-center">
<button onClick={toggleFilterModal} className={glassSurfaceFlat("rounded-xl h-8 ps-4 pe-2 py-1.5 inline-flex items-center justify-between gap-1.75")}>
<Candle2 className="stroke-foreground" size={16} />
<span className="text-xs leading-5 font-medium whitespace-nowrap">{tMenu("MenuFilterDrawer.Label")}</span>
</button>
+32 -30
View File
@@ -1,38 +1,40 @@
'use client'
"use client";
import React from 'react'
import SearchIcon from '../icons/SearchIcon'
import { glassSurfaceFlat } from '@/lib/styles/glassSurface'
import { useGetAbout } from "@/app/[name]/(Main)/about/hooks/useAboutData";
import { glassSurfaceFlat } from "@/lib/styles/glassSurface";
import Link from "next/link";
import { useParams } from "next/navigation";
import React from "react";
import SearchIcon from "../icons/SearchIcon";
interface SearchboxProps
extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'results'> {
placeholder?: string
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void
interface SearchboxProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, "results"> {
placeholder?: string;
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
}
export default function SearchBox({
className,
value,
placeholder = '',
onChange,
...props
}: SearchboxProps) {
export default function SearchBox({ className, value, placeholder = "", onChange, ...props }: SearchboxProps) {
const { name } = useParams<{ name?: string }>();
const { data: about } = useGetAbout();
const showAiLink = !!name && !!about?.data?.enableAiChat;
return (
<div
className={glassSurfaceFlat(
'inline-flex rounded-xl px-4 h-10 w-full items-center content-center',
className,
<div className="flex gap-4 items-center">
<div className={glassSurfaceFlat("inline-flex rounded-xl px-4 h-10 w-full items-center content-center", className)}>
<SearchIcon stroke="#8C90A3" />
<input
onChange={onChange}
value={value}
placeholder={placeholder}
className="text-[#8C90A3] pt-1 block outline-none border-none focus:ring-0 focus:outline-none px-1.75 h-full items-center w-full text-xs!"
type="search"
{...props}
/>
</div>
{showAiLink && (
<Link href={`/${name}/ai`} aria-label="AI" className={glassSurfaceFlat("inline-flex items-center justify-center rounded-xl w-10 h-10 shrink-0")}>
<img src={"/assets/images/logo_ai2.svg"} alt="logo_ai" className="w-5" />
</Link>
)}
>
<SearchIcon stroke='#8C90A3' />
<input
onChange={onChange}
value={value}
placeholder={placeholder}
className='text-[#8C90A3] pt-1 block outline-none border-none focus:ring-0 focus:outline-none px-[7px] h-full items-center w-full text-xs!'
type='search'
{...props}
/>
</div>
)
);
}
+15 -61
View File
@@ -51,9 +51,7 @@ const MenuItem = ({ food }: MenuItemProps) => {
if (!content) return "";
if (Array.isArray(content)) {
return content
.filter((item) => item && typeof item === "string")
.join("، ");
return content.filter((item) => item && typeof item === "string").join("، ");
}
return "";
@@ -74,15 +72,9 @@ const MenuItem = ({ food }: MenuItemProps) => {
return basePrice;
}, [food.price, food.discount]);
const formattedPrice = useMemo(
() => (finalPrice ? finalPrice.toLocaleString("fa-IR") : "0"),
[finalPrice],
);
const formattedPrice = useMemo(() => (finalPrice ? finalPrice.toLocaleString("fa-IR") : "0"), [finalPrice]);
const formattedOriginalPrice = useMemo(
() => (food.price ? food.price.toLocaleString("fa-IR") : "0"),
[food.price],
);
const formattedOriginalPrice = useMemo(() => (food.price ? food.price.toLocaleString("fa-IR") : "0"), [food.price]);
const hasDiscount = useMemo(() => (food.discount || 0) > 0, [food.discount]);
@@ -102,63 +94,34 @@ const MenuItem = ({ food }: MenuItemProps) => {
return (
<div className="flex gap-4 w-full h-full items-center">
<Link href={foodDetailUrl} className="cursor-pointer">
<img
className="rounded-xl bg-[#F2F2F9] min-w-28 w-28 max-h-28 object-cover"
src={imageSrc}
height={112}
width={112}
alt={foodName}
loading="lazy"
onError={() => setImageSrc(fallbackImage)}
/>
<img className="rounded-xl bg-[#F2F2F9] min-w-28 w-28 max-h-28 object-cover" src={imageSrc} height={112} width={112} alt={foodName} loading="lazy" onError={() => setImageSrc(fallbackImage)} />
</Link>
<div className="w-full inline-flex flex-col justify-between min-w-0">
<Link href={foodDetailUrl} className="cursor-pointer min-w-0">
<div className="min-w-0">
<div className="text-sm2 font-normal text-black dark:text-white wrap-break-word">
{foodName}
</div>
{(foodContent || foodDescription) && (
<div className="text-[#7F7F7F] line-clamp-2 text-xs leading-5 font-normal mt-2 wrap-break-word overflow-hidden">
{foodContent || foodDescription}
</div>
)}
<div className="text-sm2 font-normal text-black dark:text-white wrap-break-word">{foodName}</div>
{(foodContent || foodDescription) && <div className="text-[#7F7F7F] line-clamp-2 text-xs leading-5 font-normal mt-2 wrap-break-word overflow-hidden">{foodContent || foodDescription}</div>}
</div>
</Link>
<div className="inline-flex mt-2 gap-2 justify-between w-full items-start">
<div
className="w-full flex flex-col gap-1 items-end"
dir="ltr"
>
<div className="w-full flex flex-col gap-1 items-end" dir="ltr">
{hasDiscount ? (
<>
<span className="text-xs text-disabled-text line-through">
{formattedOriginalPrice} T
</span>
<span className="text-sm font-medium text-black dark:text-white">
{formattedPrice} T
</span>
<span className="text-xs text-disabled-text line-through">{formattedOriginalPrice} T</span>
<span className="text-sm font-medium text-black dark:text-white">{formattedPrice} T</span>
</>
) : (
<span className="text-sm text-right">
{formattedPrice} T
</span>
<span className="text-sm text-right">{formattedPrice} T</span>
)}
</div>
<motion.div
whileTap={isAvailable ? { scale: 1.05 } : undefined}
className="bg-background pattern-secondary-bg active:drop-shadow-xs max-w-[115px] rounded-md w-full h-8 inline-flex p-1 justify-between items-center gap-2 relative overflow-hidden"
className="bg-background pattern-secondary-bg active:drop-shadow-xs max-w-28.75 rounded-md w-full h-8 inline-flex p-1 justify-between items-center gap-2 relative overflow-hidden"
>
{quantity <= 0 ? (
<button
onClick={handleAddToCart}
disabled={!isAvailable}
className="inline-flex w-full justify-center items-center gap-2 disabled:pointer-events-none"
>
<button onClick={handleAddToCart} disabled={!isAvailable} className="inline-flex w-full justify-center items-center gap-2 disabled:pointer-events-none">
{isAvailable && <PlusIcon />}
<div className="text-sm2 pt-0.5 font-normal text-foreground">
{isAvailable ? "افزودن" : "عدم موجودی"}
</div>
<div className="text-sm2 pt-0.5 font-normal text-foreground">{isAvailable ? "افزودن" : "عدم موجودی"}</div>
</button>
) : (
<>
@@ -169,19 +132,10 @@ const MenuItem = ({ food }: MenuItemProps) => {
>
<PlusIcon className="text-foreground" />
</button>
<motion.div
key={quantity}
initial={{ scale: 1 }}
animate={{ scale: [1.2, 0.95, 1] }}
transition={{ duration: 0.3 }}
className="text-sm2 pt-0.5 font-semibold"
>
<motion.div key={quantity} initial={{ scale: 1 }} animate={{ scale: [1.2, 0.95, 1] }} transition={{ duration: 0.3 }} className="text-sm2 pt-0.5 font-semibold">
{quantity}
</motion.div>
<button
onClick={handleRemoveFromCart}
className="bg-container hover:bg-container/60 active:bg-container/30 rounded-sm p-2 transition-colors"
>
<button onClick={handleRemoveFromCart} className="bg-container hover:bg-container/60 active:bg-container/30 rounded-sm p-2 transition-colors">
<MinusIcon className="text-foreground" />
</button>
</>
+386 -424
View File
@@ -1,466 +1,428 @@
'use client';
"use client";
import React, { useMemo, useRef, useState, useEffect } from 'react';
import { motion, Variants } from 'framer-motion';
import SideMenuItem from './SideMenuItem';
import NoteBoardIcon from '../icons/NoteBoardIcon';
import BlurredOverlayContainer from '../overlays/BlurredOverlayContainer';
import NightModeSwitch from '../button/NightModeSwitch';
import Button from '../button/PrimaryButton';
import { useParams, usePathname, useRouter } from 'next/navigation';
import clsx from 'clsx';
import { useTranslation } from 'react-i18next';
import { CalendarSearch, Cup, DirectboxReceive, DirectInbox, DocumentCopy, Game, Icon, Instagram, Like1, Login, LogoutCurve, Notification, Receipt1, Setting2, Share, TicketDiscount, Whatsapp } from 'iconsax-react';
import TelegramIcon from '../icons/TelegramIcon';
import Modal from '../utils/Modal';
import LogoutPrompt from '@/features/general/LogoutPrompt';
import useToggle from '@/hooks/helpers/useToggle';
import { useGetAbout } from '@/app/[name]/(Main)/about/hooks/useAboutData';
import Image from 'next/image';
import { useGetProfile } from '@/app/[name]/(Profile)/profile/hooks/userProfileData';
import { toast, toastLoginRequired } from '../Toast';
import { glassSurface } from '@/lib/styles/glassSurface';
import { useGetAbout } from "@/app/[name]/(Main)/about/hooks/useAboutData";
import { useGetProfile } from "@/app/[name]/(Profile)/profile/hooks/userProfileData";
import LogoutPrompt from "@/features/general/LogoutPrompt";
import useToggle from "@/hooks/helpers/useToggle";
import { glassSurface } from "@/lib/styles/glassSurface";
import clsx from "clsx";
import { motion, Variants } from "framer-motion";
import {
CalendarSearch,
Cup,
DirectboxReceive,
DirectInbox,
DocumentCopy,
Game,
Icon,
Instagram,
Like1,
Login,
LogoutCurve,
MessageProgramming,
Notification,
Receipt1,
Setting2,
Share,
TicketDiscount,
Whatsapp,
} from "iconsax-react";
import Image from "next/image";
import { useParams, usePathname, useRouter } from "next/navigation";
import React, { useEffect, useMemo, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import NightModeSwitch from "../button/NightModeSwitch";
import Button from "../button/PrimaryButton";
import NoteBoardIcon from "../icons/NoteBoardIcon";
import TelegramIcon from "../icons/TelegramIcon";
import BlurredOverlayContainer from "../overlays/BlurredOverlayContainer";
import { toast, toastLoginRequired } from "../Toast";
import Modal from "../utils/Modal";
import SideMenuItem from "./SideMenuItem";
type MenuItemType = {
href: string | undefined;
title: string;
icon: Icon;
auth?: boolean;
guestOnly?: boolean;
premiumOnly?: boolean;
href: string | undefined;
title: string;
icon: Icon;
badge?: string;
auth?: boolean;
guestOnly?: boolean;
premiumOnly?: boolean;
};
const menuItems: Array<Array<MenuItemType>> = [
[
{ href: 'notifications', title: 'Notifications', icon: Notification },
{ href: 'transactions/discount/club', title: 'CustomerClub', icon: Cup, premiumOnly: true },
{ href: 'transactions/discount', title: 'Discounts', icon: TicketDiscount, premiumOnly: true },
{ href: 'order/history', title: 'Orders', icon: CalendarSearch, premiumOnly: true },
{ href: 'transactions', title: 'Transactions', icon: Receipt1, premiumOnly: true },
{ href: 'game', title: 'Games', icon: Game },
{ href: '?share', title: 'ShareWithFriends', icon: Like1 },
{ href: 'report', title: 'ReportProblem', icon: NoteBoardIcon },
{ href: '?installpwa', title: 'InstallApp', icon: DirectboxReceive }
],
[],
[
{ auth: true, href: 'profile/settings', title: 'Preferences', icon: Setting2 },
{ auth: true, href: '?logout', title: 'Logout', icon: LogoutCurve },
{ guestOnly: true, href: 'auth', title: 'LoginSignup', icon: Login }
]
[
{ href: "notifications", title: "Notifications", icon: Notification },
{ href: "transactions/discount/club", title: "CustomerClub", icon: Cup, premiumOnly: true },
{ href: "transactions/discount", title: "Discounts", icon: TicketDiscount, premiumOnly: true },
{ href: "order/history", title: "Orders", icon: CalendarSearch, premiumOnly: true },
{ href: "transactions", title: "Transactions", icon: Receipt1, premiumOnly: true },
{ href: "game", title: "Games", icon: Game },
{ href: "ai", title: "AI", icon: MessageProgramming, badge: "AIBadge" },
{ href: "?share", title: "ShareWithFriends", icon: Like1 },
{ href: "report", title: "ReportProblem", icon: NoteBoardIcon },
{ href: "?installpwa", title: "InstallApp", icon: DirectboxReceive },
],
[],
[
{ auth: true, href: "profile/settings", title: "Preferences", icon: Setting2 },
{ auth: true, href: "?logout", title: "Logout", icon: LogoutCurve },
{ guestOnly: true, href: "auth", title: "LoginSignup", icon: Login },
],
];
type Props = {
menuState: boolean;
toggleMenuState: React.MouseEventHandler<HTMLDivElement> | undefined;
nightModeState: boolean;
togglenightModeState: React.MouseEventHandler<HTMLDivElement> | undefined;
menuState: boolean;
toggleMenuState: React.MouseEventHandler<HTMLDivElement> | undefined;
nightModeState: boolean;
togglenightModeState: React.MouseEventHandler<HTMLDivElement> | undefined;
};
type BeforeInstallPromptEvent = Event & {
prompt: () => Promise<void>;
userChoice: Promise<{ outcome: 'accepted' | 'dismissed' }>;
prompt: () => Promise<void>;
userChoice: Promise<{ outcome: "accepted" | "dismissed" }>;
};
const isIOS = () => {
if (typeof window === 'undefined') return false;
return /iPad|iPhone|iPod/.test(navigator.userAgent) ||
(navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1);
if (typeof window === "undefined") return false;
return /iPad|iPhone|iPod/.test(navigator.userAgent) || (navigator.platform === "MacIntel" && navigator.maxTouchPoints > 1);
};
const isStandalone = () => {
if (typeof window === 'undefined') return false;
if (typeof window === "undefined") return false;
const isIOSStandalone =
('standalone' in window.navigator) &&
(window.navigator as { standalone?: boolean }).standalone === true;
const isIOSStandalone = "standalone" in window.navigator && (window.navigator as { standalone?: boolean }).standalone === true;
const isDisplayModeStandalone =
window.matchMedia('(display-mode: standalone)').matches ||
window.matchMedia('(display-mode: fullscreen)').matches;
const isDisplayModeStandalone = window.matchMedia("(display-mode: standalone)").matches || window.matchMedia("(display-mode: fullscreen)").matches;
return isIOSStandalone || isDisplayModeStandalone;
return isIOSStandalone || isDisplayModeStandalone;
};
function SideMenu({ menuState, toggleMenuState, nightModeState, togglenightModeState }: Props) {
const menuStateMemo = useMemo(() => menuState, [menuState]);
const closeRef = useRef<HTMLDivElement>(null);
const { state: logoutModal, toggle: toggleLogoutModal } = useToggle();
const { state: shareModal, toggle: toggleShareModal } = useToggle();
const { state: installPwaModal, toggle: toggleInstallPwaModal } = useToggle();
const [deferredPrompt, setDeferredPrompt] = useState<BeforeInstallPromptEvent | null>(null);
const [isIOSDevice, setIsIOSDevice] = useState(false);
const [isPWAInstalled, setIsPWAInstalled] = useState(false);
const { data: aboutData } = useGetAbout();
const { data: profile, isSuccess } = useGetProfile();
const profileData = profile?.data;
const isLoggedIn = isSuccess && profileData;
const isPremium = aboutData?.data?.plan === "premium";
const router = useRouter();
const menuStateMemo = useMemo(() => menuState, [menuState]);
const closeRef = useRef<HTMLDivElement>(null);
const { state: logoutModal, toggle: toggleLogoutModal } = useToggle();
const { state: shareModal, toggle: toggleShareModal } = useToggle();
const { state: installPwaModal, toggle: toggleInstallPwaModal } = useToggle();
const [deferredPrompt, setDeferredPrompt] = useState<BeforeInstallPromptEvent | null>(null);
const [isIOSDevice, setIsIOSDevice] = useState(false);
const [isPWAInstalled, setIsPWAInstalled] = useState(false);
const { data: aboutData } = useGetAbout();
const { data: profile, isSuccess } = useGetProfile();
const profileData = profile?.data;
const isLoggedIn = isSuccess && profileData;
const isPremium = aboutData?.data?.plan === 'premium';
const router = useRouter();
const params = useParams();
const { name } = params;
const pathname = usePathname();
const { t: tMenu } = useTranslation("common", {
keyPrefix: "SideMenu",
});
const { t: tShareModal } = useTranslation("common", {
keyPrefix: "ShareModal",
});
const { t: tInstallPwaModal } = useTranslation("common", {
keyPrefix: "InstallPwaModal",
});
const variants: Variants = {
hidden: { x: "100%", transition: { duration: 0.1, ease: "easeInOut" } },
visible: { x: 0, transition: { delay: 0.075, duration: 0.1, ease: "easeInOut" } },
};
const params = useParams();
const { name } = params;
const pathname = usePathname();
const { t: tMenu } = useTranslation('common', {
keyPrefix: 'SideMenu'
});
const { t: tShareModal } = useTranslation('common', {
keyPrefix: 'ShareModal'
});
const { t: tInstallPwaModal } = useTranslation('common', {
keyPrefix: 'InstallPwaModal'
});
useEffect(() => {
const ios = isIOS();
const standalone = isStandalone();
setIsIOSDevice(ios);
setIsPWAInstalled(standalone);
const variants: Variants = {
hidden: { x: '100%', transition: { duration: 0.1, ease: 'easeInOut' } },
visible: { x: 0, transition: { delay: 0.075, duration: 0.1, ease: 'easeInOut' } }
const handleBeforeInstallPrompt = (e: Event) => {
e.preventDefault();
setDeferredPrompt(e as BeforeInstallPromptEvent);
};
useEffect(() => {
const ios = isIOS();
const standalone = isStandalone();
setIsIOSDevice(ios);
setIsPWAInstalled(standalone);
const handleBeforeInstallPrompt = (e: Event) => {
e.preventDefault();
setDeferredPrompt(e as BeforeInstallPromptEvent);
};
const handleAppInstalled = () => {
setDeferredPrompt(null);
setIsPWAInstalled(true);
};
window.addEventListener('beforeinstallprompt', handleBeforeInstallPrompt);
window.addEventListener('appinstalled', handleAppInstalled);
return () => {
window.removeEventListener('beforeinstallprompt', handleBeforeInstallPrompt);
window.removeEventListener('appinstalled', handleAppInstalled);
};
}, []);
const handleInstallPWA = async () => {
if (!deferredPrompt) {
return;
}
deferredPrompt.prompt();
const { outcome } = await deferredPrompt.userChoice;
if (outcome === 'accepted') {
setDeferredPrompt(null);
toggleInstallPwaModal();
}
const handleAppInstalled = () => {
setDeferredPrompt(null);
setIsPWAInstalled(true);
};
const getShareUrl = () => {
if (typeof window === 'undefined') return '';
return window.location.href;
window.addEventListener("beforeinstallprompt", handleBeforeInstallPrompt);
window.addEventListener("appinstalled", handleAppInstalled);
return () => {
window.removeEventListener("beforeinstallprompt", handleBeforeInstallPrompt);
window.removeEventListener("appinstalled", handleAppInstalled);
};
}, []);
const copyToClipboard = async (showToast: boolean = true) => {
try {
const url = getShareUrl();
await navigator.clipboard.writeText(url);
if (showToast) {
toast('لینک با موفقیت کپی شد', 'success');
}
return true;
} catch {
if (showToast) {
toast('خطا در کپی کردن لینک', 'error');
}
return false;
}
};
const handleCopyLink = async () => {
await copyToClipboard();
toggleShareModal();
};
const handleShareTelegram = () => {
const url = encodeURIComponent(getShareUrl());
window.open(`https://t.me/share/url?url=${url}`, '_blank');
};
const handleShareWhatsApp = () => {
const url = encodeURIComponent(getShareUrl());
const text = encodeURIComponent('اپلیکیشن ما را ببینید!');
window.open(`https://wa.me/?text=${text}%20${url}`, '_blank');
};
const handleShareInstagram = async () => {
const success = await copyToClipboard(false);
if (success) {
toast('لینک کپی شد. لطفاً آن را در اینستاگرام به اشتراک بگذارید', 'info');
}
};
const handleNotificationClick = (e: React.MouseEvent) => {
e.preventDefault();
if (!isLoggedIn) {
toastLoginRequired(() => router.push(`/${name}/auth`), 'info');
} else {
router.push(`/${name}/notifications`);
}
};
const hrefOnClicks = [
{ href: '?logout', handler: toggleLogoutModal },
{ href: '?share', handler: toggleShareModal },
{ href: '?installpwa', handler: toggleInstallPwaModal },
{ href: 'notifications', handler: handleNotificationClick }
];
const renderMenu = () => {
return (
<section className="flex-1 overflow-y-scroll noscrollbar pt-6 pb-8 flex flex-col justify-between md:w-[200px] xl:w-[250px]">
{
aboutData?.data?.logo && (
<div >
<Image className='mx-auto' src={aboutData?.data?.logo} alt='logo' width={110} height={100} unoptimized />
</div>
)
}
<div className='mt-4'>
<header className="px-5 md:px-6 xl:px-12">
<h6 className="text-start font-bold text-sm text-menu-header mt-2 mb-[19px] dark:text-disabled-text">منو</h6>
</header>
<nav aria-label={tMenu('NavAriaLabel')}>
<ul>
{menuItems[0]
.filter(item => {
if (item.auth && !isSuccess) return false;
if (item.guestOnly && isSuccess) return false;
if (item.href === '?installpwa' && isPWAInstalled) return false;
if (item.premiumOnly && !isPremium) return false;
return true;
})
.map(({ icon: Icon, ...item }, index) => {
const href = `/${name}/${item.href}`;
const isActive = pathname === href;
return (
<li key={index} className="mt-4 h-6">
<SideMenuItem
className={clsx(isActive && 'text-primary! border-primary! dark:text-neutral-200! dark:border-neutral-200!', 'border-r-4!')}
href={href ?? ''}
title={tMenu(item.title)}
onClick={
typeof item.href === 'string'
? hrefOnClicks.find((i) => i.href === item.href)?.handler
: undefined
}
icon={
<Icon
variant={isActive ? 'Bold' : 'Linear'}
className={clsx(
isActive ? 'text-primary fill-primary dark:text-neutral-200 dark:fill-neutral-200' : 'stroke-icon-deactive text-icon-deactive'
)}
size={20} width={20} height={20} />
}
/>
</li>
);
})}
</ul>
</nav>
</div>
<section aria-label={tMenu('ControlsAriaLabel')}>
<ul className="flex flex-col pt-16">
{menuItems[2]
.filter(item => {
if (item.auth && !isSuccess) return false;
if (item.guestOnly && isSuccess) return false;
return true;
})
.map(({ icon: Icon, ...item }, index) => {
const href = `/${name}/${item.href}`;
const isActive = pathname === href;
return (
<li key={index} className="mt-4 h-6 w-full">
<SideMenuItem
key={index}
href={href ?? ''}
className={clsx(isActive && 'text-primary! border-primary! dark:text-neutral-200! dark:border-neutral-200!', 'border-r-4!')}
onClick={
typeof item.href === 'string'
? hrefOnClicks.find((i) => i.href === item.href)?.handler
: undefined
}
title={tMenu(item.title)}
icon={
<Icon
variant={isActive ? 'Bold' : 'Linear'}
className={clsx(
isActive ? 'text-primary fill-primary dark:text-neutral-200 dark:fill-neutral-200' : 'stroke-icon-deactive text-icon-deactive'
)}
size={20} width={20} height={20} />
}
/>
</li>
)
})}
<li className="mt-4 h-6">
<SideMenuItem
className='pr-7'
href={''}
title={''}
icon={<></>}>
<NightModeSwitch
checked={nightModeState}
onClick={togglenightModeState}
/>
</SideMenuItem>
</li>
</ul>
</section>
</section>
)
const handleInstallPWA = async () => {
if (!deferredPrompt) {
return;
}
deferredPrompt.prompt();
const { outcome } = await deferredPrompt.userChoice;
if (outcome === "accepted") {
setDeferredPrompt(null);
toggleInstallPwaModal();
}
};
const getShareUrl = () => {
if (typeof window === "undefined") return "";
return window.location.href;
};
const copyToClipboard = async (showToast: boolean = true) => {
try {
const url = getShareUrl();
await navigator.clipboard.writeText(url);
if (showToast) {
toast("لینک با موفقیت کپی شد", "success");
}
return true;
} catch {
if (showToast) {
toast("خطا در کپی کردن لینک", "error");
}
return false;
}
};
const handleCopyLink = async () => {
await copyToClipboard();
toggleShareModal();
};
const handleShareTelegram = () => {
const url = encodeURIComponent(getShareUrl());
window.open(`https://t.me/share/url?url=${url}`, "_blank");
};
const handleShareWhatsApp = () => {
const url = encodeURIComponent(getShareUrl());
const text = encodeURIComponent("اپلیکیشن ما را ببینید!");
window.open(`https://wa.me/?text=${text}%20${url}`, "_blank");
};
const handleShareInstagram = async () => {
const success = await copyToClipboard(false);
if (success) {
toast("لینک کپی شد. لطفاً آن را در اینستاگرام به اشتراک بگذارید", "info");
}
};
const handleNotificationClick = (e: React.MouseEvent) => {
e.preventDefault();
if (!isLoggedIn) {
toastLoginRequired(() => router.push(`/${name}/auth`), "info");
} else {
router.push(`/${name}/notifications`);
}
};
const hrefOnClicks = [
{ href: "?logout", handler: toggleLogoutModal },
{ href: "?share", handler: toggleShareModal },
{ href: "?installpwa", handler: toggleInstallPwaModal },
{ href: "notifications", handler: handleNotificationClick },
];
const renderMenu = () => {
return (
<>
<div className='w-full' ref={closeRef} onClick={toggleMenuState}>
<aside aria-label={tMenu('AriaLabel')}>
<BlurredOverlayContainer className='xl:hidden!' bgOpacity={40} visible={menuStateMemo} outDelay={150} inDuration={200}>
<motion.nav
transition={{ type: "spring", stiffness: 200, damping: 20 }}
initial="hidden"
animate={menuState ? 'visible' : 'hidden'}
variants={variants}
data-visible={menuState}
data-isvisible={menuStateMemo}
onClick={(e) => { e.stopPropagation(); }}
className="fixed top-0 bottom-0 right-0 bg-container w-[200px] h-dvh flex flex-col z-40 overflow-clip not-xl:rounded-s-none!"
>
{renderMenu()}
</motion.nav>
</BlurredOverlayContainer>
<nav
className={glassSurface('hidden fixed top-4 bottom-4 right-4 xl:w-[250px] xl:flex flex-col z-40 overflow-clip xl:rounded-4xl')}
>
{renderMenu()}
</nav>
</aside>
</div>
<section className="flex-1 overflow-y-scroll noscrollbar pt-6 pb-8 flex flex-col justify-between md:w-50 xl:w-62.5">
{aboutData?.data?.logo && (
<div>
<Image className="mx-auto" src={aboutData?.data?.logo} alt="logo" width={110} height={100} unoptimized />
</div>
)}
<div className="mt-4">
<header className="px-5 md:px-6 xl:px-12">
<h6 className="text-start font-bold text-sm text-menu-header mt-2 mb-4.75 dark:text-disabled-text">منو</h6>
</header>
{/* Logout Modal */}
<LogoutPrompt
visible={logoutModal}
onClick={toggleLogoutModal}
slug={name as string}
/>
<nav aria-label={tMenu("NavAriaLabel")}>
<ul>
{menuItems[0]
.filter((item) => {
if (item.auth && !isSuccess) return false;
if (item.guestOnly && isSuccess) return false;
if (item.href === "?installpwa" && isPWAInstalled) return false;
if (item.premiumOnly && !isPremium) return false;
if (item.href === "ai" && !aboutData?.data?.enableAiChat) return false;
return true;
})
.map(({ icon: Icon, ...item }, index) => {
const href = `/${name}/${item.href}`;
const isActive = pathname === href;
return (
<li key={index} className={clsx("mt-4", item.badge ? "h-8" : "h-6")}>
<SideMenuItem
className={clsx(isActive && "text-primary! border-primary! dark:text-neutral-200! dark:border-neutral-200!", "border-r-4!")}
href={href ?? ""}
title={tMenu(item.title)}
badge={item.badge ? tMenu(item.badge) : undefined}
onClick={typeof item.href === "string" ? hrefOnClicks.find((i) => i.href === item.href)?.handler : undefined}
icon={
<Icon
variant={isActive ? "Bold" : "Linear"}
className={clsx(isActive ? "text-primary fill-primary dark:text-neutral-200 dark:fill-neutral-200" : "stroke-icon-deactive text-icon-deactive")}
size={20}
width={20}
height={20}
/>
}
/>
</li>
);
})}
</ul>
</nav>
</div>
{/* Share modal */}
<Modal
visible={shareModal}
onClick={toggleShareModal}
>
<h2 className="text-base font-medium">
<Share className="inline-block ml-2 mb-1.5 stroke-primary dark:stroke-foreground" size={24} />
{tShareModal('Heading')}
</h2>
<p className="text-xs font-medium mt-6">
{tShareModal('Description')}
</p>
<Button onClick={handleCopyLink} className="text-sm mt-8">
<DocumentCopy className="inline-block ml-2 mb-0.5 stroke-container dark:stroke-foreground" size={20} />
<span className="font-light">{tShareModal('ButtonCopy')}</span>
</Button>
<div className="grid grid-cols-3 gap-5.5 mt-6 px-12">
<button
onClick={handleShareTelegram}
className="flex flex-col items-center cursor-pointer hover:opacity-80 transition-opacity"
>
<TelegramIcon width={24} height={24} className="mb-1.5 text-primary dark:text-foreground" />
<span className='text-xs2 font-medium'>Telegram</span>
</button>
<button
onClick={handleShareWhatsApp}
className="flex flex-col items-center cursor-pointer hover:opacity-80 transition-opacity"
>
<Whatsapp size={24} className="mb-1.5 stroke-primary dark:stroke-foreground" />
<span className='text-xs2 font-medium'>WhatsApp</span>
</button>
<button
onClick={handleShareInstagram}
className="flex flex-col items-center cursor-pointer hover:opacity-80 transition-opacity"
>
<Instagram size={24} className="mb-1.5 stroke-primary dark:stroke-foreground" />
<span className='text-xs2 font-medium'>Instagram</span>
</button>
</div>
</Modal>
{/* Install PWA modal */}
<Modal
visible={installPwaModal}
onClick={toggleInstallPwaModal}
>
<h2 className="text-base font-medium">
<DirectInbox className="inline-block ml-2 mb-1.5 stroke-primary dark:stroke-foreground" size={24} />
{tInstallPwaModal('Heading')}
</h2>
{isPWAInstalled ? (
<p className="mt-6 text-xs font-medium text-center text-foreground/80 dark:text-neutral-200">
{tInstallPwaModal('AlreadyInstalled')}
</p>
) : isIOSDevice ? (
<div className="mt-5 rounded-3xl bg-primary/5 px-4 py-5 backdrop-blur-sm dark:bg-white/5">
<p className="text-xs font-medium leading-relaxed text-center text-foreground/80 dark:text-neutral-200">
{tInstallPwaModal('IOSDescription')}
</p>
<ol className="mt-5 space-y-3 text-xs font-medium rtl:text-right">
<li className="flex items-start gap-2">
<span className="mt-0.5 flex h-5 w-5 items-center justify-center rounded-full bg-primary/10 text-[11px] font-bold text-primary">
۱
</span>
<span>{tInstallPwaModal('IOSStep1')}</span>
</li>
<li className="flex items-start gap-2">
<span className="mt-0.5 flex h-5 w-5 items-center justify-center rounded-full bg-primary/10 text-[11px] font-bold text-primary">
۲
</span>
<span>{tInstallPwaModal('IOSStep2')}</span>
</li>
<li className="flex items-start gap-2">
<span className="mt-0.5 flex h-5 w-5 items-center justify-center rounded-full bg-primary/10 text-[11px] font-bold text-primary">
۳
</span>
<span>{tInstallPwaModal('IOSStep3')}</span>
</li>
</ol>
</div>
) : (
<>
<p className="mt-6 text-xs font-medium leading-relaxed text-center text-foreground/80 dark:text-neutral-200">
{tInstallPwaModal('Description')}
</p>
{deferredPrompt ? (
<Button onClick={handleInstallPWA} className="mt-8 text-sm">
<span className="font-light">{tInstallPwaModal('ButtonOk')}</span>
</Button>
) : (
<p className="mt-4 text-[11px] font-medium leading-relaxed text-center text-foreground/60 dark:text-neutral-300">
{tInstallPwaModal('ChromeFallback')}
</p>
)}
</>
)}
</Modal>
</>
<section aria-label={tMenu("ControlsAriaLabel")}>
<ul className="flex flex-col pt-16">
{menuItems[2]
.filter((item) => {
if (item.auth && !isSuccess) return false;
if (item.guestOnly && isSuccess) return false;
return true;
})
.map(({ icon: Icon, ...item }, index) => {
const href = `/${name}/${item.href}`;
const isActive = pathname === href;
return (
<li key={index} className="mt-4 h-6 w-full">
<SideMenuItem
key={index}
href={href ?? ""}
className={clsx(isActive && "text-primary! border-primary! dark:text-neutral-200! dark:border-neutral-200!", "border-r-4!")}
onClick={typeof item.href === "string" ? hrefOnClicks.find((i) => i.href === item.href)?.handler : undefined}
title={tMenu(item.title)}
icon={
<Icon
variant={isActive ? "Bold" : "Linear"}
className={clsx(isActive ? "text-primary fill-primary dark:text-neutral-200 dark:fill-neutral-200" : "stroke-icon-deactive text-icon-deactive")}
size={20}
width={20}
height={20}
/>
}
/>
</li>
);
})}
<li className="mt-4 h-6">
<SideMenuItem className="pr-7" href={""} title={""} icon={<></>}>
<NightModeSwitch checked={nightModeState} onClick={togglenightModeState} />
</SideMenuItem>
</li>
</ul>
</section>
</section>
);
};
return (
<>
<div className="w-full" ref={closeRef} onClick={toggleMenuState}>
<aside aria-label={tMenu("AriaLabel")}>
<BlurredOverlayContainer className="xl:hidden!" bgOpacity={40} visible={menuStateMemo} outDelay={150} inDuration={200}>
<motion.nav
transition={{ type: "spring", stiffness: 200, damping: 20 }}
initial="hidden"
animate={menuState ? "visible" : "hidden"}
variants={variants}
data-visible={menuState}
data-isvisible={menuStateMemo}
onClick={(e) => {
e.stopPropagation();
}}
className="fixed top-0 bottom-0 right-0 bg-container w-50 h-dvh flex flex-col z-40 overflow-clip not-xl:rounded-s-none!"
>
{renderMenu()}
</motion.nav>
</BlurredOverlayContainer>
<nav className={glassSurface("hidden fixed top-4 bottom-4 right-4 xl:w-62.5 xl:flex flex-col z-40 overflow-clip xl:rounded-4xl")}>{renderMenu()}</nav>
</aside>
</div>
{/* Logout Modal */}
<LogoutPrompt visible={logoutModal} onClick={toggleLogoutModal} slug={name as string} />
{/* Share modal */}
<Modal visible={shareModal} onClick={toggleShareModal}>
<h2 className="text-base font-medium">
<Share className="inline-block ml-2 mb-1.5 stroke-primary dark:stroke-foreground" size={24} />
{tShareModal("Heading")}
</h2>
<p className="text-xs font-medium mt-6">{tShareModal("Description")}</p>
<Button onClick={handleCopyLink} className="text-sm mt-8">
<DocumentCopy className="inline-block ml-2 mb-0.5 stroke-container dark:stroke-foreground" size={20} />
<span className="font-light">{tShareModal("ButtonCopy")}</span>
</Button>
<div className="grid grid-cols-3 gap-5.5 mt-6 px-12">
<button onClick={handleShareTelegram} className="flex flex-col items-center cursor-pointer hover:opacity-80 transition-opacity">
<TelegramIcon width={24} height={24} className="mb-1.5 text-primary dark:text-foreground" />
<span className="text-xs2 font-medium">Telegram</span>
</button>
<button onClick={handleShareWhatsApp} className="flex flex-col items-center cursor-pointer hover:opacity-80 transition-opacity">
<Whatsapp size={24} className="mb-1.5 stroke-primary dark:stroke-foreground" />
<span className="text-xs2 font-medium">WhatsApp</span>
</button>
<button onClick={handleShareInstagram} className="flex flex-col items-center cursor-pointer hover:opacity-80 transition-opacity">
<Instagram size={24} className="mb-1.5 stroke-primary dark:stroke-foreground" />
<span className="text-xs2 font-medium">Instagram</span>
</button>
</div>
</Modal>
{/* Install PWA modal */}
<Modal visible={installPwaModal} onClick={toggleInstallPwaModal}>
<h2 className="text-base font-medium">
<DirectInbox className="inline-block ml-2 mb-1.5 stroke-primary dark:stroke-foreground" size={24} />
{tInstallPwaModal("Heading")}
</h2>
{isPWAInstalled ? (
<p className="mt-6 text-xs font-medium text-center text-foreground/80 dark:text-neutral-200">{tInstallPwaModal("AlreadyInstalled")}</p>
) : isIOSDevice ? (
<div className="mt-5 rounded-3xl bg-primary/5 px-4 py-5 backdrop-blur-sm dark:bg-white/5">
<p className="text-xs font-medium leading-relaxed text-center text-foreground/80 dark:text-neutral-200">{tInstallPwaModal("IOSDescription")}</p>
<ol className="mt-5 space-y-3 text-xs font-medium rtl:text-right">
<li className="flex items-start gap-2">
<span className="mt-0.5 flex h-5 w-5 items-center justify-center rounded-full bg-primary/10 text-[11px] font-bold text-primary">۱</span>
<span>{tInstallPwaModal("IOSStep1")}</span>
</li>
<li className="flex items-start gap-2">
<span className="mt-0.5 flex h-5 w-5 items-center justify-center rounded-full bg-primary/10 text-[11px] font-bold text-primary">۲</span>
<span>{tInstallPwaModal("IOSStep2")}</span>
</li>
<li className="flex items-start gap-2">
<span className="mt-0.5 flex h-5 w-5 items-center justify-center rounded-full bg-primary/10 text-[11px] font-bold text-primary">۳</span>
<span>{tInstallPwaModal("IOSStep3")}</span>
</li>
</ol>
</div>
) : (
<>
<p className="mt-6 text-xs font-medium leading-relaxed text-center text-foreground/80 dark:text-neutral-200">{tInstallPwaModal("Description")}</p>
{deferredPrompt ? (
<Button onClick={handleInstallPWA} className="mt-8 text-sm">
<span className="font-light">{tInstallPwaModal("ButtonOk")}</span>
</Button>
) : (
<p className="mt-4 text-[11px] font-medium leading-relaxed text-center text-foreground/60 dark:text-neutral-300">{tInstallPwaModal("ChromeFallback")}</p>
)}
</>
)}
</Modal>
</>
);
}
export default SideMenu;
export default SideMenu;
+25 -19
View File
@@ -1,29 +1,35 @@
'use client';
"use client";
import Link, { LinkProps } from 'next/link'
import React from 'react'
import Link, { LinkProps } from "next/link";
import React from "react";
type Props = {
icon: React.ReactElement
title: string
} & LinkProps & React.AnchorHTMLAttributes<HTMLAnchorElement>
icon: React.ReactElement;
title: string;
badge?: string;
} & LinkProps &
React.AnchorHTMLAttributes<HTMLAnchorElement>;
export default function SideMenuItem({ href, title, icon, className, children, ...restProps }: Props) {
return (
<Link
{...restProps}
href={href}
className={`
export default function SideMenuItem({ href, title, icon, badge, className, children, ...restProps }: Props) {
return (
<Link
{...restProps}
href={href}
className={`
inline-flex gap-3 ps-6 xl:ps-9 h-full w-full items-center overflow-hidden text-nowrap
text-xs font-light text-disabled-text border-transparent
data-active:border-s-6 data-active:border-primary data-active:text-primary
dark:data-active:border-foreground dark:data-active:text-foreground
${className}
`}
>
{children}
<span className="shrink-0">{icon}</span>
<span className='mt-0.5'>{title}</span>
</Link>
)
}
>
{children}
<span className="shrink-0">{icon}</span>
<span className="flex items-start gap-2">
<span className="mt-0.5">{title}</span>
{badge && <span className="rounded-md mt-0.5 border border-primary text-primary px-1.5 py-0.5 text-[8px] leading-none font-light dark:bg-white/15 dark:text-neutral-200">{badge}</span>}
</span>
</Link>
);
}
@@ -43,7 +43,7 @@ function ClientMenuRouteWrapper({ children }: Props) {
};
const location = usePathname();
const hideBottomNav = pathname.endsWith("/cart");
const hideBottomNav = pathname.endsWith("/cart") || pathname.endsWith("/ai");
const mainRef = useRef<HTMLElement>(null);
// Refs for the scroll-driven animation — kept outside React state so scroll
@@ -75,8 +75,8 @@ function ClientMenuRouteWrapper({ children }: Props) {
}
const SCROLL_RANGE = 150; // px of scroll to reach full compact state
const LERP_FACTOR = 0.1; // per-frame smoothing (lower = smoother, higher = snappier)
const MIN_SCALE = 0.82; // scale floor (18% smaller at full scroll)
const LERP_FACTOR = 0.1; // per-frame smoothing (lower = smoother, higher = snappier)
const MIN_SCALE = 0.82; // scale floor (18% smaller at full scroll)
const tick = () => {
const diff = targetProgressRef.current - currentProgressRef.current;
@@ -106,7 +106,7 @@ function ClientMenuRouteWrapper({ children }: Props) {
return (
<div className="h-svh overflow-hidden pt-10 flex flex-col pb-4 xl:pt-12">
<div data-menu-topbar className={glassSurface("z-50 fixed right-4 left-4 xl:right-[285px] top-4 xl:h-16 h-12 flex items-center px-4 justify-between rounded-[32px] xl:w-[calc(100%-305px)]")}>
<div data-menu-topbar className={glassSurface("z-50 fixed right-4 left-4 xl:right-71.25 top-4 xl:h-16 h-12 flex items-center px-4 justify-between rounded-4xl xl:w-[calc(100%-305px)]")}>
<TopBar
profileDropState={profileDrop}
toggleProfileDropState={toggleProfileDrop}
@@ -120,12 +120,7 @@ function ClientMenuRouteWrapper({ children }: Props) {
</div>
{!hideBottomNav && (
<div
ref={navContainerRef}
data-menu-bottom-nav
className="fixed bottom-7 left-0 right-0 z-45 px-4"
style={{ willChange: "transform", transformOrigin: "center bottom" }}
>
<div ref={navContainerRef} data-menu-bottom-nav className="fixed bottom-7 left-0 right-0 z-45 px-4" style={{ willChange: "transform", transformOrigin: "center bottom" }}>
<BottomNavBar onPagerClick={togglePager} />
</div>
)}
+1 -1
View File
@@ -1,5 +1,5 @@
// export const API_BASE_URL = "https://dmenuplus-api-production.dev.danakcorp.com";
// export const API_BASE_URL = "http://192.168.1.107:2000";
// export const API_BASE_URL = "http://10.202.59.88:2000";
export const API_BASE_URL = "https://dmenu-api.danakcorp.com";
process.env.NEXT_PUBLIC_API_URL ?? (process.env.NODE_ENV === "development" ? "http://192.168.99.131:2000" : "https://dmenu-api.danakcorp.com");
export const TOKEN_NAME = "dmenu-t";
+28 -1
View File
@@ -15,6 +15,8 @@
"Orders": "لیست سفارشات",
"Transactions": "لیست تراکنش ها",
"Games": "بازی و سرگرمی ها",
"AI": "چی بخورم؟",
"AIBadge": "هوش مصنوعی",
"ShareWithFriends": "معرفی به دوستان",
"ReportProblem": "گزارش اشکال",
"InstallApp": "نصب اپلیکیشن",
@@ -59,5 +61,30 @@
"IOSStep3": "در صفحه بعدی روی دکمه «Add» در گوشه بالا بزنید.",
"ChromeFallback": "اگر دکمه نصب را نمی‌بینید، یا اپلیکیشن قبلاً نصب شده است یا مرورگر هنوز امکان نصب را فعال نکرده است. کمی بعد دوباره تلاش کنید یا از منوی مرورگر گزینه نصب در صفحه اصلی را انتخاب کنید."
},
"SearchPlaceholder": "جستجو"
"SearchPlaceholder": "جستجو",
"AI": {
"Heading": "چی بخورم؟",
"BrandFallback": "دی‌منو",
"GuestName": "مهمان",
"Greeting": {
"morning": "صبح بخیر، {{name}}",
"noon": "ظهر بخیر، {{name}}",
"afternoon": "عصر بخیر، {{name}}",
"evening": "شب بخیر، {{name}}"
},
"InputPlaceholder": "هر چی دوست داری بپرس…",
"Suggestions": {
"vegetarian": "غذای گیاهی چی دارید؟",
"healthy": "غذای سالم چی دارید؟",
"tasty": "یه غذای خوشمزه پیشنهاد بده.",
"protein": "غذای پروتئینی پیشنهاد بده",
"bestsellers": "پرفروش‌ترین غذاها چیه؟",
"spicy": "غذای تند پیشنهاد کن.",
"quick": "یه غذای سریع برای من انتخاب کن",
"dessert": "پیشنهادت برای دسر چیه؟",
"group": "برای ۴ نفر چی سفارش بدیم"
},
"Thinking": "در حال فکر کردن…",
"ErrorFallback": "متأسفانه نتونستم جواب بدم. دوباره امتحان کن."
}
}