for deploy

This commit is contained in:
ilya_zahvatkin 2025-05-01 18:29:38 +07:00
parent 9974d41bd8
commit 41c1385546
50 changed files with 2829 additions and 936 deletions

BIN
.DS_Store vendored

Binary file not shown.

View File

@ -13,7 +13,7 @@ RUN pip install --no-cache-dir -r requirements.txt
COPY backend/ .
# Копирование .env.docker в .env для использования в контейнере
COPY backend/.env.docker ./app/.env
COPY backend/.env.docker ./.env
# Создание директории для загрузок если её нет
RUN mkdir -p /app/uploads/products
@ -25,4 +25,4 @@ RUN chmod -R 777 /app/uploads
EXPOSE 8000
# Запуск приложения с Uvicorn
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]

View File

@ -8,12 +8,12 @@ COPY frontend/package*.json ./
# Установка зависимостей с флагом --legacy-peer-deps
RUN npm ci --legacy-peer-deps
# Копирование исходного кода
COPY frontend/ ./
# Копирование .env.docker в .env.local для использования в контейнере
COPY frontend/.env.docker ./.env.local
# Копирование исходного кода
COPY frontend/ ./
# Сборка приложения
RUN npm run build
@ -21,4 +21,4 @@ RUN npm run build
EXPOSE 3000
# Запуск приложения
CMD ["npm", "start"]
CMD ["sh", "-c", "npm run build && npm start"]

View File

@ -0,0 +1,56 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Creator: CorelDRAW 2021.5 -->
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" width="101.681mm" height="52.7148mm" version="1.1" style="shape-rendering:geometricPrecision; text-rendering:geometricPrecision; image-rendering:optimizeQuality; fill-rule:evenodd; clip-rule:evenodd"
viewBox="0 0 9795.77 5078.45"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:xodm="http://www.corel.com/coreldraw/odm/2003">
<defs>
<style type="text/css">
<![CDATA[
.fil0 {fill:#2B2B2A;fill-rule:nonzero}
]]>
</style>
</defs>
<g id="Слой_x0020_1">
<metadata id="CorelCorpID_0Corel-Layer"/>
<g id="_105553243841888">
<path class="fil0" d="M4908.27 805.19c54.07,19.97 154.75,32.69 273.42,7.79 134.29,-28.17 227.42,-126.87 210.47,-234.42 -15.58,-98.88 -78.5,-144.79 -183.21,-184.87 -5,-103.57 -48.42,-197.68 -117.48,-266.17 -25.62,-25.61 -54.57,-47.33 -86.3,-65.14 -19.36,-12.43 -50.13,-24.51 -52.91,-25.62 30.07,-7.24 62.93,-11.37 101.34,-12.25 160.91,-3.71 233.25,103.02 241.68,154.81l67.38 1.11c-5.58,-28.4 -59.6,-180.41 -318.52,-180.41 -47.89,0 -94.65,8.35 -136.41,22.27 -33.41,-8.91 -66.83,-13.92 -102.48,-13.92 -101.33,0 -403.71,0.56 -403.71,0.56l0 17.25c35.1,0 57.37,8.36 71.83,20.61 26.18,21.17 26.18,55.13 26.18,74.61 0,145.76 0,437.26 0,583.01 0,22.83 0,67.94 -45.67,86.87 -12.8,5.57 -30.08,9.47 -52.34,9.47l0 17.81c0,0 245.13,0 396.09,0 61.7,0 110.63,-13.36 110.63,-13.36zm-333.56 -11.14l0 0 0 -760.64 180.43 0c66.83,0 127.42,11.35 204.92,62.92 94.34,62.78 161.59,156.48 172.05,270.63 16.65,181.55 -83.66,312.64 -175.39,368.06 23.38,10.03 47.31,17.26 62.92,21.17 96.9,-61.25 165.06,-170.19 183.75,-279.54 84.09,39.54 123.05,81.3 129.2,142.55 5,52.9 -21.16,95.78 -63.49,126.95 -57.91,43.44 -148.19,64.63 -225.51,56.8 -162.62,-16.47 -250.22,-113.71 -270.64,-191.55l-70.14 0c12.85,56.55 58.58,124.7 147.55,169.83 0,0 -38.54,12.8 -97.45,12.8 -44.55,0 -178.2,0 -178.2,0z"/>
<path class="fil0" d="M5019.09 410.39c23.38,7.24 44.54,13.92 64.58,20.6 1.12,-21.71 1.69,-51.78 -3.34,-80.74 -187.66,-59.58 -275.07,-87.98 -273.39,-189.32 0.55,-23.94 12.23,-47.33 34.5,-68.49 -13.34,-5.01 -27.84,-9.47 -41.18,-12.25 -42.5,35.61 -54.6,75.9 -54.57,107.47 0.11,118.63 90.75,168.16 273.41,222.73z"/>
</g>
<g id="_105553243834656">
<g>
<path class="fil0" d="M2921.05 1914.15c14.17,-25.91 21.24,-57.16 21.24,-93.77 0,-54.25 -15.78,-95.9 -47.37,-124.97 -31.56,-29.06 -76.18,-43.59 -133.82,-43.59l-123.88 0 0 342.24 138.46 0c32.71,0 61.58,-6.84 86.58,-20.54 25.03,-13.67 44.62,-33.47 58.8,-59.37zm-98.13 10.09l0 0c-14.98,9.62 -32.76,14.44 -53.33,14.44l-60.71 0 0 -231.48 50.76 0c35.47,0 62.75,9.68 81.85,29.03 19.12,19.35 28.68,47.4 28.68,84.16 0,24.29 -4.13,45.26 -12.4,62.91 -8.25,17.66 -19.87,31.3 -34.85,40.94z"/>
<path class="fil0" d="M3167.62 1864.11l84.03 0 79.44 129.96 80.64 0 -92.53 -143.8c23.15,-5.49 41.54,-16.96 55.14,-34.36 13.59,-17.4 20.39,-38.1 20.39,-62.06 0,-32.87 -11.09,-58.09 -33.28,-75.67 -22.19,-17.56 -53.68,-26.34 -94.49,-26.34l-171 0 0 342.24 71.67 0 0 -129.96zm0 -156.67l0 0 91.8 0c21.07,0 36.85,4.12 47.37,12.39 10.53,8.26 15.79,20.57 15.79,36.93 0,16.35 -5.17,29.06 -15.55,38.13 -10.36,9.08 -25.57,13.6 -45.66,13.6l-93.75 0 0 -101.05z"/>
<polygon class="fil0" points="3833.78,1938.68 3626.36,1938.68 3626.36,1848.56 3809.02,1848.56 3809.02,1793.18 3626.36,1793.18 3626.36,1707.2 3823.83,1707.2 3823.83,1651.83 3554.7,1651.83 3554.7,1994.07 3833.78,1994.07 "/>
<path class="fil0" d="M4239.57 1829.39c-4.06,-3.88 -8.85,-7.53 -14.45,-10.94 -5.59,-3.39 -11.96,-6.64 -19.07,-9.72 -7.13,-2.91 -16.61,-5.93 -28.43,-9.1 -11.81,-3.16 -25.91,-6.51 -42.25,-10.07 -28.18,-5.99 -46.57,-10.94 -55.14,-14.83 -9.08,-4.04 -15.92,-8.89 -20.52,-14.58 -4.61,-5.66 -6.93,-12.95 -6.93,-21.85 0,-13.6 5.42,-23.81 16.28,-30.62 10.84,-6.8 27.11,-10.19 48.81,-10.19 20.4,0 36.08,3.72 47,11.17 10.92,7.46 18.17,18.55 21.74,33.28l69.46 -9.47c-6.16,-30.62 -19.87,-52.56 -41.17,-65.84 -21.28,-13.27 -53.14,-19.9 -95.58,-19.9 -44.36,0 -78.17,8.21 -101.4,24.63 -23.25,16.44 -34.86,40.13 -34.86,71.06 0,18.13 3.4,33.27 10.2,45.42 6.8,12.14 16.26,21.85 28.42,29.15 5.5,3.24 11.17,6.19 16.99,8.85 5.83,2.68 13.6,5.42 23.31,8.27 9.72,2.84 23.18,6.27 40.34,10.32 14.9,3.08 27.37,5.87 37.4,8.38 10.04,2.51 17.65,4.82 22.84,6.93 10.52,4.22 18.62,9.48 24.29,15.79 5.66,6.32 8.51,14.34 8.51,24.05 0,15.22 -6.23,26.64 -18.72,34.25 -12.47,7.6 -31.24,11.42 -56.36,11.42 -24.12,0 -42.99,-4.08 -56.59,-12.26 -13.6,-8.17 -22.75,-21.34 -27.44,-39.47l-69.23 11.41c14.25,62.68 64.7,94.01 151.32,94.01 48.1,0 84.59,-8.87 109.44,-26.6 24.84,-17.73 37.26,-43.36 37.26,-76.87 0,-14.26 -2.18,-26.86 -6.54,-37.78 -4.38,-10.92 -10.68,-20.37 -18.95,-28.28z"/>
<path class="fil0" d="M4549.65 1998.92c48.1,0 84.59,-8.87 109.44,-26.6 24.84,-17.73 37.27,-43.36 37.27,-76.87 0,-14.26 -2.19,-26.86 -6.55,-37.78 -4.38,-10.92 -10.68,-20.37 -18.95,-28.28 -4.06,-3.88 -8.85,-7.53 -14.45,-10.94 -5.59,-3.39 -11.96,-6.64 -19.07,-9.72 -7.13,-2.91 -16.61,-5.93 -28.43,-9.1 -11.81,-3.16 -25.91,-6.51 -42.25,-10.07 -28.18,-5.99 -46.57,-10.94 -55.14,-14.83 -9.08,-4.04 -15.92,-8.89 -20.52,-14.58 -4.61,-5.66 -6.93,-12.95 -6.93,-21.85 0,-13.6 5.42,-23.81 16.28,-30.62 10.84,-6.8 27.11,-10.19 48.81,-10.19 20.4,0 36.08,3.72 47,11.17 10.92,7.46 18.17,18.55 21.74,33.28l69.46 -9.47c-6.16,-30.62 -19.87,-52.56 -41.17,-65.84 -21.28,-13.27 -53.14,-19.9 -95.58,-19.9 -44.36,0 -78.17,8.21 -101.4,24.63 -23.25,16.44 -34.86,40.13 -34.86,71.06 0,18.13 3.4,33.27 10.2,45.42 6.8,12.14 16.26,21.85 28.42,29.15 5.5,3.24 11.17,6.19 16.99,8.85 5.83,2.68 13.6,5.42 23.31,8.27 9.72,2.84 23.18,6.27 40.34,10.32 14.9,3.08 27.37,5.87 37.4,8.38 10.04,2.51 17.65,4.82 22.84,6.93 10.52,4.22 18.62,9.48 24.29,15.79 5.66,6.32 8.51,14.34 8.51,24.05 0,15.22 -6.23,26.64 -18.72,34.25 -12.47,7.6 -31.24,11.42 -56.36,11.42 -24.12,0 -42.99,-4.08 -56.59,-12.26 -13.6,-8.17 -22.75,-21.34 -27.44,-39.47l-69.23 11.41c14.25,62.68 64.7,94.01 151.32,94.01z"/>
<polygon class="fil0" points="5117.7,1707.2 5117.7,1651.83 4848.57,1651.83 4848.57,1994.07 5127.66,1994.07 5127.66,1938.68 4920.23,1938.68 4920.23,1848.56 5102.89,1848.56 5102.89,1793.18 4920.23,1793.18 4920.23,1707.2 "/>
<path class="fil0" d="M5279.85 1994.07l138.47 0c32.7,0 61.57,-6.84 86.58,-20.54 25.02,-13.67 44.62,-33.47 58.8,-59.37 14.16,-25.91 21.24,-57.16 21.24,-93.77 0,-54.25 -15.78,-95.9 -47.37,-124.97 -31.57,-29.06 -76.18,-43.59 -133.82,-43.59l-123.89 0 0 342.24zm71.67 -286.87l0 0 50.76 0c35.47,0 62.75,9.68 81.85,29.03 19.12,19.35 28.67,47.4 28.67,84.16 0,24.29 -4.12,45.26 -12.39,62.91 -8.26,17.66 -19.87,31.3 -34.86,40.94 -14.97,9.62 -32.76,14.44 -53.32,14.44l-60.71 0 0 -231.48z"/>
<polygon class="fil0" points="6047.96,1868.49 6223.09,1868.49 6223.09,1813.11 6047.96,1813.11 6047.96,1707.2 6228.67,1707.2 6228.67,1651.83 5976.31,1651.83 5976.31,1994.07 6047.96,1994.07 "/>
<path class="fil0" d="M6446.68 1977.42c25.76,14.34 56.52,21.5 92.3,21.5 34.98,0 65.47,-7.37 91.46,-22.11 25.99,-14.72 46.14,-35.41 60.48,-62.05 14.34,-26.64 21.5,-57.76 21.5,-93.4 0,-36.26 -6.9,-67.45 -20.66,-93.52 -13.75,-26.07 -33.51,-46.11 -59.27,-60.12 -25.74,-14 -56.75,-21 -93.01,-21 -36.12,0 -67.1,6.91 -92.91,20.75 -25.83,13.85 -45.59,33.81 -59.27,59.89 -13.69,26.06 -20.53,57.39 -20.53,93.99 0,36.61 6.87,68.17 20.64,94.74 13.78,26.56 33.54,47 59.27,61.32zm18.47 -243.14l0 0c17.49,-20.81 42.27,-31.2 74.33,-31.2 31.57,0 56.1,10.49 73.58,31.44 17.49,20.97 26.23,49.91 26.23,86.84 0,38.71 -8.69,68.57 -26.11,89.64 -17.41,21.04 -42.14,31.56 -74.21,31.56 -20.57,0 -38.32,-4.89 -53.3,-14.7 -14.98,-9.79 -26.53,-23.76 -34.61,-41.89 -8.1,-18.13 -12.15,-39.67 -12.15,-64.61 0,-37.23 8.74,-66.27 26.23,-87.08z"/>
<path class="fil0" d="M6937.74 1864.11l84.04 0 79.43 129.96 80.64 0 -92.54 -143.8c23.15,-5.49 41.54,-16.96 55.14,-34.36 13.6,-17.4 20.4,-38.1 20.4,-62.06 0,-32.87 -11.1,-58.09 -33.29,-75.67 -22.18,-17.56 -53.68,-26.34 -94.48,-26.34l-171 0 0 342.24 71.66 0 0 -129.96zm0 -156.67l0 0 91.81 0c21.06,0 36.84,4.12 47.37,12.39 10.52,8.26 15.78,20.57 15.78,36.93 0,16.35 -5.17,29.06 -15.55,38.13 -10.36,9.08 -25.57,13.6 -45.66,13.6l-93.75 0 0 -101.05z"/>
</g>
<g>
<path class="fil0" d="M627.35 3128.5c-312.41,-99.91 -458.08,-146.06 -455.14,-313.6 1.93,-109.46 154.21,-225.53 405.68,-225.47 287.79,0.07 385.03,181.46 397.9,255.57l111.56 1.45c-9.62,-46.47 -97.83,-297.67 -524.58,-297.67 -259.34,0 -495.04,135.69 -491.46,309.29 4.01,196.05 149.13,276.7 449.94,366.97 355.51,106.69 500.8,187.23 515.93,345.01 19.96,208.26 -256.33,323.28 -475.22,301.64 -358.92,-35.49 -444.44,-293.58 -445.96,-314.95l-115.99 0c39.03,195.14 288.44,356.1 562.16,356.1 379.65,0 604.74,-178.57 573.21,-410.47 -30.07,-221.31 -214.33,-279.92 -508.03,-373.87z"/>
<path class="fil0" d="M9792.83 3502.37c-30.09,-221.31 -214.33,-279.92 -508.03,-373.87 -312.41,-99.91 -458.08,-146.06 -455.14,-313.6 1.9,-109.46 154.21,-225.53 405.68,-225.47 287.79,0.07 385,181.46 397.9,255.57l111.56 1.45c-9.62,-46.47 -97.83,-297.67 -524.58,-297.67 -259.34,0 -495.04,135.69 -491.46,309.29 4.01,196.05 149.13,276.7 449.95,366.97 355.48,106.69 500.8,187.23 515.92,345.01 19.96,208.26 -256.33,323.28 -475.22,301.64 -358.92,-35.49 -444.44,-293.58 -445.96,-314.95l-116.01 0c39.05,195.14 288.46,356.1 562.18,356.1 379.65,0 604.74,-178.57 573.21,-410.47z"/>
<path class="fil0" d="M5602.13 3400.62c-63.13,271.92 -276.29,471.98 -529.27,471.98 -303.48,0 -549.2,-287.46 -549.2,-641.93 0,-354.48 245.71,-641.94 549.2,-641.94 253.46,0 466.14,200.06 529.27,472.47l132.07 0c-75.26,-294.75 -343.3,-512.77 -661.34,-512.77 -376.82,0 -682.73,305.43 -682.73,682.24 0,377.29 305.91,682.72 682.73,682.72 318.04,0 585.59,-217.54 661.34,-512.77l-132.07 0z"/>
<path class="fil0" d="M7158.78 3127.72l54.39 0 78.2 0c-49.56,-327.76 -332.66,-579.29 -674.5,-579.29 -377.27,0 -682.7,305.43 -682.7,682.24 0,377.29 305.43,682.72 682.7,682.72 258.81,0 483.69,-143.73 599.24,-356.41l-126.28 0c-95.64,188.89 -271.91,315.62 -472.96,315.62 -303.48,0 -549.64,-287.46 -549.64,-641.93 0,-34.96 2.42,-69.44 7.25,-102.95l1084.3 0zm-541.91 -538.99l0 0c261.28,0 480.27,213.66 535.61,499.67l-1071.65 0c55.35,-286.01 274.32,-499.67 536.04,-499.67z"/>
<path class="fil0" d="M8004.3 3128.5c-312.38,-99.91 -458.08,-146.06 -455.14,-313.6 1.93,-109.46 154.21,-225.53 405.7,-225.47 287.79,0.07 385.01,181.46 397.88,255.57l111.56 1.45c-9.62,-46.47 -97.8,-297.67 -524.56,-297.67 -259.33,0 -495.03,135.69 -491.48,309.29 4.03,196.05 149.13,276.7 449.94,366.97 355.51,106.69 500.8,187.23 515.93,345.01 19.96,208.26 -256.3,323.28 -475.22,301.64 -358.9,-35.49 -444.45,-293.58 -445.96,-314.95l-115.99 0c39.03,195.14 288.46,356.1 562.16,356.1 379.64,0 604.74,-178.57 573.23,-410.47 -30.09,-221.31 -214.35,-279.92 -508.05,-373.87z"/>
<path class="fil0" d="M3529.31 2588.72c252.98,0 466.16,200.06 529.28,472.47l131.59 0c-75.26,-294.75 -342.82,-512.77 -660.87,-512.77 -377.29,0 -682.72,305.43 -682.72,682.24 0,377.29 305.43,682.72 682.72,682.72 318.05,0 585.12,-217.54 660.87,-512.77l-131.59 0c-63.12,271.92 -276.77,471.98 -529.28,471.98 -303.48,0 -549.67,-287.46 -549.67,-641.93 0,-354.48 246.18,-641.94 549.67,-641.94z"/>
<path class="fil0" d="M2453.98 2562.09l-0.19 0 0 849.19c0,254.83 -206.58,461.42 -461.41,461.42 -254.86,0 -461.44,-206.59 -461.44,-461.42l0 -849.19 -123.81 0 0 852.58c0,336.18 313.22,498.69 588.65,498.69 205.52,0 381.87,-124.37 458.21,-301.9l0 261.24 123.81 0 0 -1310.94 -123.81 0 0 0.33z"/>
</g>
</g>
<path class="fil0" d="M2863.3 4731.51c41.79,0 74.39,11.02 97.36,33.06 22.96,22.04 35.82,46.84 38.11,74.85l-43.17 0c-5.05,-21.13 -15.15,-38.11 -29.85,-50.51 -14.7,-12.4 -35.37,-18.37 -61.99,-18.37 -32.15,0 -58.32,11.02 -78.53,33.98 -19.75,22.97 -29.86,57.87 -29.86,104.71 0,38.57 9.19,69.8 27.1,94.13 17.91,23.88 45,35.82 80.36,35.82 33.06,0 58.32,-12.4 75.32,-37.65 9.18,-13.32 16.07,-30.77 20.2,-52.81l43.63 0c-3.68,34.9 -16.53,63.83 -38.57,87.25 -26.18,28.47 -61.54,42.71 -105.63,42.71 -38.57,0 -70.72,-11.95 -96.43,-34.9 -34.44,-30.77 -51.44,-78.06 -51.44,-141.9 0,-48.68 12.86,-88.63 38.57,-119.4 27.55,-33.98 66.13,-50.97 114.81,-50.97z"/>
<polygon id="_1" class="fil0" points="3146.94,4740.7 3191.94,4740.7 3191.94,4876.62 3363.23,4876.62 3363.23,4740.7 3408.23,4740.7 3408.23,5069.96 3363.23,5069.96 3363.23,4916.12 3191.94,4916.12 3191.94,5069.96 3146.94,5069.96 "/>
<polygon id="_2" class="fil0" points="3576.6,4740.7 3621.61,4740.7 3621.61,5069.96 3576.6,5069.96 "/>
<polygon id="_3" class="fil0" points="3783.39,4740.7 3827.01,4740.7 3827.01,4900.97 3987.74,4740.7 4049.27,4740.7 3911.96,4873.42 4052.94,5069.96 3995.08,5069.96 3879.82,4904.64 3827.01,4955.15 3827.01,5069.96 3783.39,5069.96 "/>
<path id="_4" class="fil0" d="M4492.31 4868.82c14.23,-10.11 23.87,-18.37 29.38,-24.8 8.73,-10.11 12.86,-21.13 12.86,-33.53 0,-10.11 -3.22,-18.37 -9.64,-25.26 -6.43,-6.89 -14.69,-10.56 -25.71,-10.56 -16.53,0 -28.02,5.51 -34.44,16.53 -3.68,5.51 -5.06,11.94 -5.06,18.82 0,8.73 2.3,17.46 7.35,26.18 5.05,8.28 13.31,19.3 25.26,32.61zm-10.57 173.12c16.53,0 30.77,-3.67 42.71,-11.02 11.95,-7.8 21.59,-16.53 28.02,-25.71l-74.85 -90.92c-21.12,14.24 -34.44,24.8 -40.87,32.15 -10.11,11.47 -15.15,25.25 -15.15,41.33 0,17.45 6.43,30.76 19.29,40.4 12.86,9.19 26.64,13.78 40.87,13.78zm-26.18 -155.21c-14.23,-16.07 -23.42,-29.39 -28.01,-40.41 -4.6,-11.02 -7.35,-21.58 -7.35,-31.69 0,-21.13 7.35,-38.57 21.58,-52.81 14.7,-13.78 33.53,-20.66 57.86,-20.66 22.97,0 40.41,6.44 53.28,19.29 12.85,12.86 19.29,28.48 19.29,46.84 0,21.13 -6.44,39.5 -19.75,55.12 -7.8,9.64 -20.66,20.2 -39.04,32.14l60.16 71.64c4.13,-11.94 6.89,-20.66 8.27,-26.63 1.83,-5.97 3.21,-14.24 5.05,-24.8l38.12 0c-2.3,21.12 -7.35,41.33 -15.15,60.62 -7.81,19.29 -11.48,27.09 -11.48,23.42l58.78 71.17 -52.35 0 -30.77 -37.66c-12.4,13.32 -23.42,22.97 -33.52,28.93 -17.91,11.02 -38.57,16.53 -61.54,16.53 -34.44,0 -59.24,-9.18 -74.85,-28.01 -15.62,-18.37 -23.42,-39.04 -23.42,-62.46 0,-24.8 7.8,-45.46 22.96,-62.46 9.18,-10.1 26.64,-22.96 51.89,-38.11z"/>
<path id="_5" class="fil0" d="M5152.26 4934.95l-50.06 -145.58 -52.8 145.58 102.86 0zm-73.01 -194.25l50.51 0 119.4 329.26 -49.13 0 -33.07 -98.73 -130.41 0 -35.82 98.73 -45.46 0 123.99 -329.26z"/>
<path id="_6" class="fil0" d="M5517.55 4731.51c41.79,0 74.39,11.02 97.36,33.06 22.96,22.04 35.82,46.84 38.11,74.85l-43.17 0c-5.05,-21.13 -15.15,-38.11 -29.85,-50.51 -14.7,-12.4 -35.37,-18.37 -61.99,-18.37 -32.15,0 -58.32,11.02 -78.53,33.98 -19.75,22.97 -29.86,57.87 -29.86,104.71 0,38.57 9.19,69.8 27.1,94.13 17.91,23.88 45,35.82 80.36,35.82 33.06,0 58.32,-12.4 75.32,-37.65 9.18,-13.32 16.07,-30.77 20.2,-52.81l43.63 0c-3.68,34.9 -16.53,63.83 -38.57,87.25 -26.18,28.47 -61.54,42.71 -105.63,42.71 -38.57,0 -70.72,-11.95 -96.43,-34.9 -34.44,-30.77 -51.44,-78.06 -51.44,-141.9 0,-48.68 12.86,-88.63 38.57,-119.4 27.55,-33.98 66.13,-50.97 114.81,-50.97z"/>
<polygon id="_7" class="fil0" points="6039.52,4740.7 6039.52,4779.73 5928.39,4779.73 5928.39,5069.96 5883.39,5069.96 5883.39,4779.73 5772.26,4779.73 5772.26,4740.7 "/>
<polygon id="_8" class="fil0" points="6179.72,4740.7 6224.73,4740.7 6224.73,5069.96 6179.72,5069.96 "/>
<polygon id="_9" class="fil0" points="6412.69,4740.7 6507.28,5021.28 6600.51,4740.7 6650.56,4740.7 6530.7,5069.96 6483.4,5069.96 6363.09,4740.7 "/>
<polygon id="_10" class="fil0" points="6786.25,4740.7 7026.42,4740.7 7026.42,4781.1 6829.87,4781.1 6829.87,4880.76 7011.72,4880.76 7011.72,4918.88 6829.87,4918.88 6829.87,5030.92 7030.09,5030.92 7030.09,5069.96 6786.25,5069.96 "/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 15 KiB

View File

@ -0,0 +1,55 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Creator: CorelDRAW 2021.5 -->
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" width="101.681mm" height="52.664mm" version="1.1" style="shape-rendering:geometricPrecision; text-rendering:geometricPrecision; image-rendering:optimizeQuality; fill-rule:evenodd; clip-rule:evenodd"
viewBox="0 0 9785.87 5068.43"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:xodm="http://www.corel.com/coreldraw/odm/2003">
<defs>
<style type="text/css">
<![CDATA[
.fil0 {fill:#2B2B2A;fill-rule:nonzero}
]]>
</style>
</defs>
<g id="Слой_x0020_1">
<metadata id="CorelCorpID_0Corel-Layer"/>
<g id="_105553243821280">
<path class="fil0" d="M4903.31 804.37c54.01,19.95 154.59,32.65 273.14,7.79 134.15,-28.14 227.19,-126.74 210.26,-234.18 -15.56,-98.78 -78.42,-144.64 -183.02,-184.69 -4.99,-103.47 -48.37,-197.48 -117.37,-265.9 -25.59,-25.58 -54.51,-47.28 -86.21,-65.08 -19.34,-12.42 -50.08,-24.48 -52.86,-25.59 30.04,-7.23 62.86,-11.36 101.24,-12.24 160.75,-3.71 233.02,102.92 241.44,154.65l67.31 1.11c-5.57,-28.37 -59.54,-180.23 -318.2,-180.23 -47.84,0 -94.56,8.34 -136.28,22.25 -33.38,-8.9 -66.76,-13.91 -102.37,-13.91 -101.23,0 -403.3,0.56 -403.3,0.56l0 17.24c35.06,0 57.31,8.35 71.76,20.59 26.16,21.14 26.16,55.08 26.16,74.54 0,145.61 0,436.82 0,582.42 0,22.81 0,67.87 -45.63,86.78 -12.79,5.56 -30.05,9.46 -52.29,9.46l0 17.79c0,0 244.88,0 395.69,0 61.64,0 110.52,-13.35 110.52,-13.35zm-333.22 -11.13l0 0 0 -759.87 180.25 0c66.76,0 127.29,11.34 204.71,62.85 94.25,62.72 161.42,156.32 171.88,270.36 16.63,181.37 -83.58,312.32 -175.22,367.69 23.36,10.02 47.26,17.25 62.85,21.14 96.8,-61.19 164.89,-170.02 183.56,-279.25 84.01,39.5 122.93,81.22 129.07,142.41 4.99,52.85 -21.13,95.68 -63.42,126.83 -57.85,43.4 -148.04,64.57 -225.28,56.74 -162.45,-16.46 -249.97,-113.59 -270.37,-191.36l-70.07 0c12.84,56.49 58.52,124.57 147.4,169.66 0,0 -38.51,12.79 -97.35,12.79 -44.5,0 -178.02,0 -178.02,0z"/>
<path class="fil0" d="M5014.02 409.98c23.36,7.23 44.49,13.91 64.51,20.58 1.12,-21.69 1.68,-51.73 -3.34,-80.66 -187.47,-59.52 -274.8,-87.89 -273.11,-189.13 0.55,-23.92 12.22,-47.28 34.46,-68.42 -13.33,-5 -27.81,-9.46 -41.14,-12.24 -42.46,35.57 -54.55,75.83 -54.51,107.37 0.11,118.51 90.66,167.99 273.13,222.51z"/>
</g>
<g id="_105553243768096">
<g>
<path class="fil0" d="M2918.1 1912.22c14.16,-25.88 21.22,-57.1 21.22,-93.67 0,-54.19 -15.76,-95.8 -47.32,-124.84 -31.53,-29.03 -76.11,-43.55 -133.69,-43.55l-123.76 0 0 341.9 138.32 0c32.67,0 61.52,-6.83 86.49,-20.52 25,-13.66 44.58,-33.43 58.74,-59.31zm-98.03 10.08l0 0c-14.97,9.61 -32.73,14.43 -53.28,14.43l-60.65 0 0 -231.25 50.71 0c35.44,0 62.69,9.67 81.77,29 19.1,19.33 28.65,47.35 28.65,84.08 0,24.26 -4.13,45.21 -12.39,62.85 -8.24,17.64 -19.85,31.27 -34.81,40.9z"/>
<path class="fil0" d="M3164.42 1862.22l83.94 0 79.36 129.83 80.55 0 -92.44 -143.66c23.13,-5.49 41.5,-16.94 55.09,-34.33 13.58,-17.38 20.37,-38.06 20.37,-62 0,-32.84 -11.08,-58.03 -33.25,-75.6 -22.16,-17.54 -53.63,-26.31 -94.39,-26.31l-170.83 0 0 341.9 71.59 0 0 -129.83zm0 -156.51l0 0 91.71 0c21.05,0 36.81,4.12 47.32,12.38 10.52,8.25 15.77,20.55 15.77,36.89 0,16.33 -5.17,29.03 -15.53,38.09 -10.35,9.07 -25.54,13.59 -45.62,13.59l-93.65 0 0 -100.95z"/>
<polygon class="fil0" points="3829.91,1936.72 3622.69,1936.72 3622.69,1846.69 3805.17,1846.69 3805.17,1791.37 3622.69,1791.37 3622.69,1705.48 3819.96,1705.48 3819.96,1650.16 3551.11,1650.16 3551.11,1992.05 3829.91,1992.05 "/>
<path class="fil0" d="M4235.28 1827.54c-4.05,-3.88 -8.84,-7.53 -14.44,-10.93 -5.58,-3.39 -11.94,-6.63 -19.05,-9.71 -7.12,-2.91 -16.59,-5.93 -28.4,-9.09 -11.8,-3.16 -25.88,-6.51 -42.21,-10.06 -28.15,-5.99 -46.52,-10.93 -55.09,-14.81 -9.07,-4.03 -15.9,-8.88 -20.5,-14.56 -4.61,-5.66 -6.92,-12.93 -6.92,-21.83 0,-13.59 5.42,-23.79 16.26,-30.59 10.83,-6.79 27.08,-10.18 48.77,-10.18 20.38,0 36.04,3.71 46.96,11.15 10.91,7.45 18.15,18.53 21.72,33.25l69.39 -9.46c-6.15,-30.59 -19.85,-52.51 -41.13,-65.77 -21.26,-13.25 -53.09,-19.88 -95.48,-19.88 -44.32,0 -78.09,8.2 -101.29,24.61 -23.22,16.43 -34.83,40.09 -34.83,70.99 0,18.11 3.4,33.23 10.19,45.38 6.79,12.13 16.25,21.83 28.39,29.12 5.5,3.23 11.15,6.19 16.98,8.84 5.82,2.68 13.59,5.42 23.29,8.26 9.71,2.84 23.16,6.27 40.3,10.31 14.89,3.08 27.34,5.86 37.36,8.37 10.03,2.51 17.63,4.81 22.82,6.92 10.51,4.22 18.6,9.47 24.26,15.77 5.66,6.31 8.5,14.32 8.5,24.02 0,15.21 -6.23,26.61 -18.7,34.21 -12.45,7.59 -31.21,11.4 -56.3,11.4 -24.1,0 -42.94,-4.08 -56.53,-12.25 -13.59,-8.16 -22.72,-21.32 -27.41,-39.43l-69.16 11.39c14.23,62.61 64.64,93.91 151.17,93.91 48.05,0 84.5,-8.86 109.33,-26.57 24.81,-17.71 37.23,-43.32 37.23,-76.79 0,-14.24 -2.18,-26.83 -6.53,-37.75 -4.38,-10.91 -10.67,-20.35 -18.93,-28.26z"/>
<path class="fil0" d="M4545.05 1996.9c48.05,0 84.5,-8.86 109.33,-26.57 24.81,-17.71 37.24,-43.32 37.24,-76.79 0,-14.24 -2.18,-26.83 -6.54,-37.75 -4.38,-10.91 -10.67,-20.35 -18.93,-28.26 -4.05,-3.88 -8.84,-7.53 -14.44,-10.93 -5.58,-3.39 -11.94,-6.63 -19.05,-9.71 -7.12,-2.91 -16.59,-5.93 -28.4,-9.09 -11.8,-3.16 -25.88,-6.51 -42.21,-10.06 -28.15,-5.99 -46.52,-10.93 -55.09,-14.81 -9.07,-4.03 -15.9,-8.88 -20.5,-14.56 -4.61,-5.66 -6.92,-12.93 -6.92,-21.83 0,-13.59 5.42,-23.79 16.26,-30.59 10.83,-6.79 27.08,-10.18 48.77,-10.18 20.38,0 36.04,3.71 46.96,11.15 10.91,7.45 18.15,18.53 21.72,33.25l69.39 -9.46c-6.15,-30.59 -19.85,-52.51 -41.13,-65.77 -21.26,-13.25 -53.09,-19.88 -95.48,-19.88 -44.32,0 -78.09,8.2 -101.29,24.61 -23.22,16.43 -34.83,40.09 -34.83,70.99 0,18.11 3.4,33.23 10.19,45.38 6.79,12.13 16.25,21.83 28.39,29.12 5.5,3.23 11.15,6.19 16.98,8.84 5.82,2.68 13.59,5.42 23.29,8.26 9.71,2.84 23.16,6.27 40.3,10.31 14.89,3.08 27.34,5.86 37.36,8.37 10.03,2.51 17.63,4.81 22.82,6.92 10.51,4.22 18.6,9.47 24.26,15.77 5.66,6.31 8.5,14.32 8.5,24.02 0,15.21 -6.23,26.61 -18.7,34.21 -12.45,7.59 -31.21,11.4 -56.3,11.4 -24.1,0 -42.94,-4.08 -56.53,-12.25 -13.59,-8.16 -22.72,-21.32 -27.41,-39.43l-69.16 11.39c14.23,62.61 64.64,93.91 151.17,93.91z"/>
<polygon class="fil0" points="5112.53,1705.48 5112.53,1650.16 4843.67,1650.16 4843.67,1992.05 5122.48,1992.05 5122.48,1936.72 4915.25,1936.72 4915.25,1846.69 5097.74,1846.69 5097.74,1791.37 4915.25,1791.37 4915.25,1705.48 "/>
<path class="fil0" d="M5274.51 1992.05l138.33 0c32.66,0 61.51,-6.83 86.49,-20.52 24.99,-13.66 44.58,-33.43 58.74,-59.31 14.15,-25.88 21.22,-57.1 21.22,-93.67 0,-54.19 -15.76,-95.8 -47.32,-124.84 -31.54,-29.03 -76.11,-43.55 -133.69,-43.55l-123.77 0 0 341.9zm71.59 -286.58l0 0 50.71 0c35.44,0 62.68,9.67 81.77,29 19.1,19.33 28.64,47.35 28.64,84.08 0,24.26 -4.12,45.21 -12.38,62.85 -8.25,17.64 -19.85,31.27 -34.82,40.9 -14.96,9.61 -32.72,14.43 -53.27,14.43l-60.65 0 0 -231.25z"/>
<polygon class="fil0" points="6041.85,1866.6 6216.8,1866.6 6216.8,1811.27 6041.85,1811.27 6041.85,1705.48 6222.38,1705.48 6222.38,1650.16 5970.26,1650.16 5970.26,1992.05 6041.85,1992.05 "/>
<path class="fil0" d="M6440.16 1975.42c25.73,14.32 56.46,21.48 92.21,21.48 34.95,0 65.41,-7.36 91.37,-22.09 25.97,-14.71 46.09,-35.38 60.42,-61.99 14.32,-26.61 21.48,-57.71 21.48,-93.31 0,-36.23 -6.89,-67.38 -20.64,-93.42 -13.73,-26.04 -33.47,-46.06 -59.21,-60.05 -25.72,-13.98 -56.7,-20.98 -92.92,-20.98 -36.08,0 -67.03,6.9 -92.81,20.73 -25.8,13.84 -45.54,33.77 -59.21,59.83 -13.68,26.03 -20.51,57.33 -20.51,93.89 0,36.57 6.86,68.1 20.61,94.64 13.76,26.53 33.5,46.96 59.21,61.26zm18.45 -242.89l0 0c17.47,-20.79 42.23,-31.17 74.26,-31.17 31.54,0 56.04,10.48 73.51,31.41 17.47,20.95 26.21,49.86 26.21,86.75 0,38.67 -8.68,68.5 -26.08,89.55 -17.39,21.02 -42.1,31.53 -74.13,31.53 -20.55,0 -38.28,-4.89 -53.25,-14.69 -14.97,-9.78 -26.5,-23.73 -34.58,-41.85 -8.09,-18.11 -12.14,-39.63 -12.14,-64.55 0,-37.2 8.73,-66.2 26.21,-86.99z"/>
<path class="fil0" d="M6930.73 1862.22l83.95 0 79.35 129.83 80.56 0 -92.45 -143.66c23.13,-5.49 41.5,-16.94 55.09,-34.33 13.59,-17.38 20.38,-38.06 20.38,-62 0,-32.84 -11.09,-58.03 -33.26,-75.6 -22.15,-17.54 -53.63,-26.31 -94.38,-26.31l-170.83 0 0 341.9 71.58 0 0 -129.83zm0 -156.51l0 0 91.72 0c21.04,0 36.8,4.12 47.32,12.38 10.51,8.25 15.76,20.55 15.76,36.89 0,16.33 -5.17,29.03 -15.53,38.09 -10.35,9.07 -25.54,13.59 -45.62,13.59l-93.65 0 0 -100.95z"/>
</g>
<g>
<path class="fil0" d="M626.71 3125.34c-312.09,-99.81 -457.62,-145.91 -454.68,-313.28 1.92,-109.35 154.05,-225.3 405.27,-225.24 287.5,0.07 384.64,181.28 397.49,255.31l111.45 1.45c-9.61,-46.43 -97.73,-297.37 -524.05,-297.37 -259.08,0 -494.54,135.56 -490.96,308.98 4,195.85 148.98,276.42 449.48,366.6 355.15,106.58 500.3,187.04 515.41,344.66 19.94,208.05 -256.07,322.96 -474.74,301.33 -358.56,-35.46 -443.99,-293.28 -445.51,-314.63l-115.87 0c38.99,194.95 288.15,355.74 561.59,355.74 379.27,0 604.13,-178.39 572.63,-410.05 -30.04,-221.08 -214.12,-279.64 -507.52,-373.49z"/>
<path class="fil0" d="M9782.93 3498.83c-30.06,-221.08 -214.12,-279.64 -507.52,-373.49 -312.09,-99.81 -457.62,-145.91 -454.68,-313.28 1.9,-109.35 154.05,-225.3 405.27,-225.24 287.5,0.07 384.61,181.28 397.49,255.31l111.45 1.45c-9.61,-46.43 -97.73,-297.37 -524.05,-297.37 -259.08,0 -494.54,135.56 -490.96,308.98 4,195.85 148.98,276.42 449.49,366.6 355.12,106.58 500.29,187.04 515.4,344.66 19.94,208.05 -256.07,322.96 -474.74,301.33 -358.56,-35.46 -443.99,-293.28 -445.51,-314.63l-115.89 0c39.01,194.95 288.16,355.74 561.61,355.74 379.27,0 604.13,-178.39 572.63,-410.05z"/>
<path class="fil0" d="M5596.47 3397.18c-63.07,271.65 -276.01,471.5 -528.74,471.5 -303.18,0 -548.64,-287.17 -548.64,-641.28 0,-354.12 245.46,-641.29 548.64,-641.29 253.2,0 465.67,199.85 528.74,471.99l131.94 0c-75.18,-294.45 -342.95,-512.25 -660.67,-512.25 -376.44,0 -682.04,305.12 -682.04,681.55 0,376.91 305.6,682.03 682.04,682.03 317.72,0 585,-217.32 660.67,-512.25l-131.94 0z"/>
<path class="fil0" d="M7151.54 3124.56l54.34 0 78.12 0c-49.51,-327.43 -332.32,-578.71 -673.82,-578.71 -376.89,0 -682.01,305.12 -682.01,681.55 0,376.91 305.12,682.03 682.01,682.03 258.55,0 483.2,-143.58 598.64,-356.05l-126.15 0c-95.55,188.7 -271.63,315.3 -472.49,315.3 -303.18,0 -549.08,-287.17 -549.08,-641.28 0,-34.93 2.42,-69.37 7.25,-102.84l1083.2 0zm-541.36 -538.45l0 0c261.01,0 479.78,213.44 535.07,499.16l-1070.56 0c55.29,-285.72 274.05,-499.16 535.49,-499.16z"/>
<path class="fil0" d="M7996.21 3125.34c-312.06,-99.81 -457.62,-145.91 -454.68,-313.28 1.92,-109.35 154.05,-225.3 405.29,-225.24 287.5,0.07 384.62,181.28 397.47,255.31l111.45 1.45c-9.61,-46.43 -97.7,-297.37 -524.03,-297.37 -259.07,0 -494.53,135.56 -490.98,308.98 4.02,195.85 148.98,276.42 449.48,366.6 355.15,106.58 500.3,187.04 515.41,344.66 19.94,208.05 -256.04,322.96 -474.74,301.33 -358.54,-35.46 -444,-293.28 -445.51,-314.63l-115.87 0c38.99,194.95 288.16,355.74 561.59,355.74 379.26,0 604.13,-178.39 572.65,-410.05 -30.06,-221.08 -214.14,-279.64 -507.54,-373.49z"/>
<path class="fil0" d="M3525.75 2586.11c252.73,0 465.69,199.85 528.75,471.99l131.46 0c-75.18,-294.45 -342.47,-512.25 -660.2,-512.25 -376.91,0 -682.03,305.12 -682.03,681.55 0,376.91 305.12,682.03 682.03,682.03 317.73,0 584.53,-217.32 660.2,-512.25l-131.46 0c-63.06,271.65 -276.49,471.5 -528.75,471.5 -303.18,0 -549.11,-287.17 -549.11,-641.28 0,-354.12 245.93,-641.29 549.11,-641.29z"/>
<path class="fil0" d="M2451.5 2559.5l-0.19 0 0 848.33c0,254.58 -206.37,460.96 -460.95,460.96 -254.61,0 -460.97,-206.38 -460.97,-460.96l0 -848.33 -123.69 0 0 851.72c0,335.84 312.91,498.18 588.05,498.18 205.31,0 381.48,-124.25 457.75,-301.59l0 260.98 123.69 0 0 -1309.62 -123.69 0 0 0.33z"/>
</g>
</g>
<polygon class="fil0" points="2756.34,4729.87 2801.29,4729.87 2801.29,4865.66 2972.41,4865.66 2972.41,4729.87 3017.36,4729.87 3017.36,5058.8 2972.41,5058.8 2972.41,4905.12 2801.29,4905.12 2801.29,5058.8 2756.34,5058.8 "/>
<path id="_1" class="fil0" d="M3317.68 4720.69c57.81,0 100.93,18.82 128.91,55.97 22.02,29.35 32.57,66.52 32.57,111.94 0,49.08 -12.39,89.91 -37.16,122.49 -29.35,38.07 -71.1,57.34 -125.24,57.34 -50.92,0 -90.37,-16.51 -119.27,-50.01 -26.15,-32.57 -39,-73.39 -39,-122.49 0,-44.49 11.01,-82.57 33.03,-114.23 28.44,-40.83 70.65,-61.02 126.15,-61.02zm4.59 307.83c39,0 67.44,-14.22 84.87,-42.2 17.89,-27.99 26.6,-60.55 26.6,-97.26 0,-38.53 -10.09,-69.73 -30.28,-93.12 -20.64,-23.86 -48.17,-35.33 -83.04,-35.33 -34.41,0 -61.93,11.47 -83.49,34.87 -21.56,23.4 -32.12,57.8 -32.12,103.22 0,36.71 9.18,67.44 27.52,92.21 18.35,25.23 48.17,37.62 89.92,37.62z"/>
<path id="_2" class="fil0" d="M3620.6 4729.87l63.77 0 94.5 278 94.05 -278 62.85 0 0 328.92 -42.21 0 0 -194.05c0,-6.88 0,-17.89 0.46,-33.49 0.46,-15.6 0.46,-32.12 0.46,-50.01l-94.05 277.55 -44.04 0 -94.51 -277.55 0 10.1c0,8.26 0,20.18 0.46,36.71 0.46,16.51 0.91,28.9 0.91,36.7l0 194.05 -42.66 0 0 -328.92z"/>
<polygon id="_3" class="fil0" points="4097.01,4729.87 4336.94,4729.87 4336.94,4770.24 4140.59,4770.24 4140.59,4869.79 4322.26,4869.79 4322.26,4907.87 4140.59,4907.87 4140.59,5019.8 4340.61,5019.8 4340.61,5058.8 4097.01,5058.8 "/>
<path id="_4" class="fil0" d="M5345.98 4952.83c0.92,18.35 5.5,33.49 12.85,45.42 15.14,21.56 40.83,32.57 78.45,32.57 16.51,0 32.12,-2.29 45.42,-7.33 26.61,-9.18 39.91,-25.7 39.91,-49.54 0,-17.89 -5.5,-30.74 -16.51,-38.53 -11.47,-7.34 -29.36,-13.76 -53.21,-19.27l-44.96 -10.1c-28.9,-6.42 -49.54,-13.76 -61.47,-21.56 -21.11,-13.76 -31.65,-34.41 -31.65,-61.94 0,-29.35 10.55,-53.67 30.74,-72.48 20.64,-19.27 49.54,-28.44 87.16,-28.44 34.41,0 63.77,8.26 87.63,24.77 24.31,16.51 36.24,43.12 36.24,79.82l-41.75 0c-2.3,-17.44 -6.88,-31.2 -14.22,-40.37 -13.76,-17.43 -36.7,-25.69 -69.27,-25.69 -26.6,0 -45.42,5.5 -56.88,16.51 -11.47,11.01 -16.98,23.85 -16.98,38.53 0,16.05 6.42,27.99 19.73,35.32 9.17,4.59 28.9,10.56 60.09,17.89l45.88 10.56c22.47,5.04 39.45,11.92 51.84,20.64 20.64,15.59 31.2,38.07 31.2,66.97 0,36.7 -13.31,62.85 -39.91,78.45 -26.16,15.6 -56.89,23.4 -92.22,23.4 -40.83,0 -72.94,-10.55 -95.88,-31.19 -23.4,-21.11 -34.87,-49.09 -34.41,-84.41l42.2 0z"/>
<polygon id="_5" class="fil0" points="5711.83,4729.87 5756.32,4729.87 5756.32,5019.8 5923.31,5019.8 5923.31,5058.8 5711.83,5058.8 "/>
<polygon id="_6" class="fil0" points="6060.32,4729.87 6300.25,4729.87 6300.25,4770.24 6103.9,4770.24 6103.9,4869.79 6285.56,4869.79 6285.56,4907.87 6103.9,4907.87 6103.9,5019.8 6303.91,5019.8 6303.91,5058.8 6060.32,5058.8 "/>
<polygon id="_7" class="fil0" points="6455.52,4729.87 6695.45,4729.87 6695.45,4770.24 6499.1,4770.24 6499.1,4869.79 6680.76,4869.79 6680.76,4907.87 6499.1,4907.87 6499.1,5019.8 6699.12,5019.8 6699.12,5058.8 6455.52,5058.8 "/>
<path id="_8" class="fil0" d="M6850.72 4729.87l148.17 0c29.36,0 52.76,8.26 71.11,24.77 17.89,16.51 26.6,39.45 26.6,69.26 0,25.7 -7.8,48.18 -23.85,67.45 -16.06,18.81 -40.84,28.44 -73.86,28.44l-103.22 0 0 139 -44.95 0 0 -328.92zm200.93 94.5c0,-24.31 -9.18,-40.83 -27.07,-49.54 -9.63,-4.59 -23.4,-6.88 -40.36,-6.88l-88.54 0 0 114.23 88.54 0c20.18,0 36.23,-4.58 48.62,-12.84 12.4,-8.72 18.82,-23.4 18.82,-44.96z"/>
<path id="_9" class="fil0" d="M4801.09 4857.86c14.21,-10.1 23.85,-18.35 29.35,-24.77 8.72,-10.1 12.85,-21.11 12.85,-33.49 0,-10.1 -3.21,-18.35 -9.63,-25.23 -6.42,-6.88 -14.68,-10.55 -25.69,-10.55 -16.51,0 -27.99,5.5 -34.41,16.51 -3.68,5.5 -5.05,11.92 -5.05,18.81 0,8.72 2.3,17.44 7.34,26.15 5.04,8.27 13.3,19.28 25.23,32.58zm-10.56 172.94c16.51,0 30.74,-3.67 42.66,-11.01 11.93,-7.8 21.57,-16.51 27.99,-25.69l-74.78 -90.83c-21.1,14.22 -34.41,24.77 -40.83,32.12 -10.1,11.46 -15.14,25.22 -15.14,41.29 0,17.43 6.42,30.73 19.27,40.36 12.85,9.18 26.61,13.76 40.83,13.76zm-26.15 -155.05c-14.21,-16.05 -23.4,-29.36 -27.98,-40.37 -4.59,-11.01 -7.34,-21.56 -7.34,-31.65 0,-21.11 7.34,-38.53 21.56,-52.76 14.69,-13.76 33.49,-20.64 57.8,-20.64 22.94,0 40.37,6.43 53.22,19.27 12.84,12.85 19.27,28.45 19.27,46.79 0,21.11 -6.43,39.46 -19.73,55.06 -7.8,9.63 -20.64,20.18 -39,32.11l60.1 71.56c4.13,-11.92 6.88,-20.64 8.26,-26.6 1.83,-5.97 3.2,-14.22 5.04,-24.77l38.08 0c-2.3,21.1 -7.34,41.29 -15.14,60.55 -7.81,19.27 -11.47,27.06 -11.47,23.4l58.72 71.1 -52.3 0 -30.74 -37.62c-12.39,13.31 -23.4,22.94 -33.48,28.9 -17.89,11.01 -38.53,16.51 -61.48,16.51 -34.41,0 -59.18,-9.17 -74.77,-27.98 -15.6,-18.35 -23.4,-39 -23.4,-62.39 0,-24.77 7.8,-45.42 22.93,-62.39 9.17,-10.09 26.61,-22.93 51.84,-38.07z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 16 KiB

BIN
backend/.DS_Store vendored

Binary file not shown.

View File

@ -10,5 +10,5 @@ SECRET_KEY=supersecretkey
FRONTEND_URL=http://frontend:3000
# Настройки Meilisearch
MEILISEARCH_URL=http://localhost:7700
MEILISEARCH_URL=http://meilisearch:7700
MEILISEARCH_KEY=dNmMmxymWpqTFyWSSFyg3xaGlEY6Pn7Ld-dyrnKRMzM

View File

@ -1,10 +1,26 @@
FROM python:3.10-slim
# Используем официальный образ с Uvicorn+Gunicorn, оптимизированный для FastAPI
FROM tiangolo/uvicorn-gunicorn-fastapi:python3.11
WORKDIR /app
COPY requirements.txt .
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
RUN pip install --no-cache-dir -r requirements.txt
COPY ./app ./app
# Для разработки код монтируется через volumes, а в продакшн-билде можно добавить COPY . /app
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--reload"] # hot-reload
EXPOSE 8000
# Точка входа: стандартный CMD из базового образа запустит Gunicorn+Uvicorn
# FROM python:3.10-slim
# WORKDIR /app
# COPY requirements.txt .
# RUN pip install --no-cache-dir -r requirements.txt
# # Для разработки код монтируется через volumes, а в продакшн-билде можно добавить COPY . /app
# CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--reload"] # hot-reload

Binary file not shown.

BIN
backend/app/.DS_Store vendored

Binary file not shown.

View File

@ -3,7 +3,8 @@
## Содержание
1. [API документация](api_documentation.md) - Подробное описание всех API эндпоинтов
2. [Структура базы данных](database_structure.md) - Описание таблиц и связей в базе данных
2. [API заказов](orders_api.md) - Подробное описание API заказов
3. [Структура базы данных](database_structure.md) - Описание таблиц и связей в базе данных
## Технологический стек
@ -114,4 +115,4 @@ alembic upgrade head
После запуска приложения, документация API доступна по адресу:
- Swagger UI: http://localhost:8000/docs
- ReDoc: http://localhost:8000/redoc
- ReDoc: http://localhost:8000/redoc

View File

@ -113,11 +113,14 @@
Базовый URL: `/orders`
Подробная документация по API заказов доступна в отдельном файле: [Документация API заказов](orders_api.md)
| Метод | Эндпоинт | Описание | Параметры запроса | Требуется авторизация | Формат ответа |
|-------|----------|----------|-------------------|------------------------|------------------|
| GET | `/` | Получение списка заказов | `skip`, `limit`, `status` | Да | `[{"id": number, "status": string, "total_amount": number, ...}]` |
| GET | `/{order_id}` | Получение информации о заказе | - | Да | `{"success": true, "order": {...}}` |
| POST | `/` | Создание нового заказа | `OrderCreate` (shipping_address_id, payment_method, notes, cart_items, items) | Да | `{"success": true, "order": {...}}` |
| POST | `/new` | Создание нового заказа (новый формат) | `OrderCreateNew` (user_info, delivery, items, payment_method, comment) | Нет | `{"success": true, "message": "...", "order": {...}}` |
| POST | `/` | Создание нового заказа (старый формат) | `OrderCreate` (shipping_address_id, payment_method, notes, cart_items, items) | Нет | `{"success": true, "order": {...}}` |
| PUT | `/{order_id}` | Обновление заказа | `OrderUpdate` (status, shipping_address_id, payment_method, payment_details, tracking_number, notes) | Да (админ) | `{"success": true, "order": {...}}` |
| POST | `/{order_id}/cancel` | Отмена заказа | - | Да | `{"success": true, "message": "..."}` |
@ -191,8 +194,9 @@
### Корзина и заказы
- `CartItemCreate`: variant_id, quantity
- `CartItemUpdate`: quantity
- `OrderCreateNew`: user_info (first_name, last_name, email, phone), delivery (method, address, cdek_info), items (product_id, variant_id, quantity, price), payment_method, comment
- `OrderCreate`: shipping_address_id, payment_method, notes, cart_items, items
- `OrderUpdate`: status, shipping_address_id, payment_method, payment_details, tracking_number, notes
- `OrderUpdate`: status, shipping_address_id, payment_method, payment_details, tracking_number, notes, delivery_method, city, delivery_address, cdek_info, courier_info, user_info_json
### Отзывы
- `ReviewCreate`: product_id, rating, text

View File

@ -128,11 +128,20 @@
|------|-----|----------|
| id | Integer | Первичный ключ |
| user_id | Integer | Внешний ключ на таблицу users |
| status | String | Статус заказа (новый, оплачен, отправлен, доставлен, отменен) |
| status | String | Статус заказа (pending, processing, shipped, delivered, cancelled, refunded) |
| total_amount | Float | Общая сумма заказа |
| shipping_address_id | Integer | Внешний ключ на таблицу addresses |
| payment_method | String | Способ оплаты |
| tracking_number | String | Номер отслеживания (опционально) |
| user_info_json | JSON | JSON с информацией о пользователе |
| delivery_method | String | Способ доставки (cdek, courier) |
| city | String | Город доставки |
| delivery_address | String | Адрес доставки (форматированный) |
| cdek_info | JSON | Информация о доставке CDEK |
| courier_info | JSON | Информация о курьерской доставке |
| shipping_address_id | Integer | Внешний ключ на таблицу addresses (для обратной совместимости) |
| payment_method | String | Способ оплаты (card, sbp) |
| payment_details | String | Детали оплаты |
| items_json | JSON | JSON со списком заказанных товаров |
| tracking_number | String | Номер отслеживания |
| notes | String | Примечания к заказу |
| created_at | DateTime | Дата и время создания |
| updated_at | DateTime | Дата и время обновления |
@ -207,4 +216,4 @@
13. `product_variants` 1:N `order_items` (вариант продукта может быть в нескольких заказах)
14. `orders` 1:N `order_items` (заказ может содержать несколько товаров)
15. `addresses` 1:N `orders` (адрес может быть использован в нескольких заказах)
15. `addresses` 1:N `orders` (адрес может быть использован в нескольких заказах)

594
backend/docs/orders_api.md Normal file
View File

@ -0,0 +1,594 @@
# Документация API заказов
## Содержание
1. [Общая информация](#общая-информация)
2. [Эндпоинты](#эндпоинты)
- [Получение списка заказов](#получение-списка-заказов)
- [Получение информации о заказе](#получение-информации-о-заказе)
- [Создание заказа (новый формат)](#создание-заказа-новый-формат)
- [Создание заказа (старый формат)](#создание-заказа-старый-формат)
- [Обновление заказа](#обновление-заказа)
- [Отмена заказа](#отмена-заказа)
3. [Модели данных](#модели-данных)
- [Структура заказа](#структура-заказа)
- [Структура элемента заказа](#структура-элемента-заказа)
- [Структура создания заказа (новый формат)](#структура-создания-заказа-новый-формат)
- [Структура создания заказа (старый формат)](#структура-создания-заказа-старый-формат)
- [Структура обновления заказа](#структура-обновления-заказа)
## Общая информация
Базовый URL: `/orders`
API заказов позволяет:
- Получать список заказов пользователя
- Получать детальную информацию о заказе
- Создавать новые заказы
- Обновлять существующие заказы
- Отменять заказы
## Эндпоинты
### Получение списка заказов
**Запрос:**
```
GET /orders
```
**Параметры запроса:**
- `skip` (опционально): Количество пропускаемых записей (по умолчанию: 0)
- `limit` (опционально): Максимальное количество возвращаемых записей (по умолчанию: 100)
- `status` (опционально): Фильтр по статусу заказа (pending, processing, shipped, delivered, cancelled, refunded)
**Требуется авторизация:** Да
**Формат ответа:**
```json
[
{
"id": 1,
"user_id": 123,
"status": "pending",
"total_amount": 5990.0,
"user_info_json": {
"first_name": "Иван",
"last_name": "Иванов",
"email": "ivan@example.com",
"phone": "+7 (999) 123-45-67"
},
"delivery_method": "cdek",
"city": "Москва",
"delivery_address": "Москва, ул. Примерная, д. 1",
"cdek_info": {
"pvz": {
"code": "MSK123",
"address": "ул. Примерная, д. 1"
}
},
"payment_method": "card",
"created_at": "2023-06-01T12:00:00",
"updated_at": "2023-06-01T12:00:00",
"items": [
{
"id": 1,
"order_id": 1,
"variant_id": 123,
"quantity": 1,
"price": 5990.0,
"product_id": 456,
"product_name": "Платье летнее",
"product_image": "dress.jpg",
"variant_name": "M",
"size": "M",
"total_price": 5990.0
}
]
}
]
```
### Получение информации о заказе
**Запрос:**
```
GET /orders/{order_id}
```
**Параметры запроса:**
- `order_id`: ID заказа
**Требуется авторизация:** Да
**Формат ответа:**
```json
{
"success": true,
"order": {
"id": 1,
"user_id": 123,
"status": "pending",
"total_amount": 5990.0,
"user_info_json": {
"first_name": "Иван",
"last_name": "Иванов",
"email": "ivan@example.com",
"phone": "+7 (999) 123-45-67"
},
"delivery_method": "cdek",
"city": "Москва",
"delivery_address": "Москва, ул. Примерная, д. 1",
"cdek_info": {
"pvz": {
"code": "MSK123",
"address": "ул. Примерная, д. 1"
}
},
"payment_method": "card",
"created_at": "2023-06-01T12:00:00",
"updated_at": "2023-06-01T12:00:00",
"items": [
{
"id": 1,
"order_id": 1,
"variant_id": 123,
"quantity": 1,
"price": 5990.0,
"product_id": 456,
"product_name": "Платье летнее",
"product_image": "dress.jpg",
"variant_name": "M",
"size": "M",
"total_price": 5990.0
}
]
}
}
```
### Создание заказа (новый формат)
**Запрос:**
```
POST /orders/new
```
**Тело запроса:**
```json
{
"user_info": {
"first_name": "Иван",
"last_name": "Иванов",
"email": "ivan@example.com",
"phone": "+7 (999) 123-45-67"
},
"delivery": {
"method": "cdek",
"address": {
"city": "Москва",
"street": "Примерная",
"house": "1",
"apartment": "123",
"postal_code": "123456",
"formatted_address": "Москва, ул. Примерная, д. 1, кв. 123"
},
"cdek_info": {
"pvz": {
"code": "MSK123",
"city_code": 44,
"city": "Москва",
"address": "ул. Примерная, д. 1",
"work_time": "Пн-Вс 10:00-20:00",
"location": [37.123, 55.456]
},
"tariff": {
"tariff_code": 136,
"tariff_name": "Посылка склад-склад",
"delivery_sum": 300,
"period_min": 1,
"period_max": 2
},
"delivery_type": "office"
}
},
"items": [
{
"product_id": 456,
"variant_id": 123,
"quantity": 1,
"price": 5990.0
}
],
"payment_method": "card",
"comment": "Позвоните перед доставкой"
}
```
**Требуется авторизация:** Нет (опционально)
**Формат ответа:**
```json
{
"success": true,
"message": "Заказ успешно создан",
"order_id": 1,
"order": {
"id": 1,
"user_id": 123,
"status": "pending",
"total_amount": 5990.0,
"user_info_json": {
"first_name": "Иван",
"last_name": "Иванов",
"email": "ivan@example.com",
"phone": "+7 (999) 123-45-67"
},
"delivery_method": "cdek",
"city": "Москва",
"delivery_address": "Москва, ул. Примерная, д. 1, кв. 123",
"cdek_info": {
"pvz": {
"code": "MSK123",
"city_code": 44,
"city": "Москва",
"address": "ул. Примерная, д. 1",
"work_time": "Пн-Вс 10:00-20:00",
"location": [37.123, 55.456]
},
"tariff": {
"tariff_code": 136,
"tariff_name": "Посылка склад-склад",
"delivery_sum": 300,
"period_min": 1,
"period_max": 2
},
"delivery_type": "office"
},
"payment_method": "card",
"notes": "Позвоните перед доставкой",
"created_at": "2023-06-01T12:00:00",
"updated_at": "2023-06-01T12:00:00",
"items": [
{
"id": 1,
"order_id": 1,
"variant_id": 123,
"quantity": 1,
"price": 5990.0,
"product_id": 456,
"product_name": "Платье летнее",
"product_image": "dress.jpg",
"variant_name": "M",
"size": "M",
"total_price": 5990.0
}
]
},
"user_message": "Для вас был создан аккаунт с email: ivan@example.com"
}
```
### Создание заказа (старый формат)
**Запрос:**
```
POST /orders
```
**Тело запроса:**
```json
{
"shipping_address_id": 1,
"payment_method": "card",
"notes": "Позвоните перед доставкой",
"cart_items": [1, 2, 3],
"items": [
{
"variant_id": 123,
"quantity": 1
}
]
}
```
**Требуется авторизация:** Нет (опционально)
**Формат ответа:**
```json
{
"success": true,
"message": "Заказ успешно создан",
"order": {
"id": 1,
"user_id": 123,
"status": "pending",
"total_amount": 5990.0,
"shipping_address_id": 1,
"shipping_address": {
"id": 1,
"address_line1": "ул. Примерная, д. 1",
"address_line2": "кв. 123",
"city": "Москва",
"state": "",
"postal_code": "123456",
"country": "Россия",
"is_default": true
},
"payment_method": "card",
"notes": "Позвоните перед доставкой",
"created_at": "2023-06-01T12:00:00",
"updated_at": "2023-06-01T12:00:00",
"items": [
{
"id": 1,
"order_id": 1,
"variant_id": 123,
"quantity": 1,
"price": 5990.0,
"product_id": 456,
"product_name": "Платье летнее",
"product_image": "dress.jpg",
"variant_name": "M",
"size": "M",
"total_price": 5990.0
}
]
}
}
```
### Обновление заказа
**Запрос:**
```
PUT /orders/{order_id}
```
**Тело запроса:**
```json
{
"status": "cancelled",
"shipping_address_id": 2,
"payment_method": "card",
"payment_details": "Оплата картой при получении",
"tracking_number": "TRACK123456",
"notes": "Новый комментарий к заказу"
}
```
**Требуется авторизация:** Да (обычные пользователи могут только отменять заказы)
**Формат ответа:**
```json
{
"success": true,
"message": "Заказ успешно обновлен",
"order": {
"id": 1,
"user_id": 123,
"status": "cancelled",
"total_amount": 5990.0,
"shipping_address_id": 2,
"shipping_address": {
"id": 2,
"address_line1": "ул. Новая, д. 2",
"address_line2": "кв. 456",
"city": "Москва",
"state": "",
"postal_code": "654321",
"country": "Россия",
"is_default": false
},
"payment_method": "card",
"payment_details": "Оплата картой при получении",
"tracking_number": "TRACK123456",
"notes": "Новый комментарий к заказу",
"created_at": "2023-06-01T12:00:00",
"updated_at": "2023-06-01T13:00:00",
"items": [
{
"id": 1,
"order_id": 1,
"variant_id": 123,
"quantity": 1,
"price": 5990.0,
"product_id": 456,
"product_name": "Платье летнее",
"product_image": "dress.jpg",
"variant_name": "M",
"size": "M",
"total_price": 5990.0
}
]
}
}
```
### Отмена заказа
**Запрос:**
```
POST /orders/{order_id}/cancel
```
**Требуется авторизация:** Да
**Формат ответа:**
```json
{
"success": true,
"message": "Заказ успешно отменен",
"order": {
"id": 1,
"user_id": 123,
"status": "cancelled",
"total_amount": 5990.0,
"shipping_address_id": 1,
"shipping_address": {
"id": 1,
"address_line1": "ул. Примерная, д. 1",
"address_line2": "кв. 123",
"city": "Москва",
"state": "",
"postal_code": "123456",
"country": "Россия",
"is_default": true
},
"payment_method": "card",
"notes": "Позвоните перед доставкой",
"created_at": "2023-06-01T12:00:00",
"updated_at": "2023-06-01T13:00:00",
"items": [
{
"id": 1,
"order_id": 1,
"variant_id": 123,
"quantity": 1,
"price": 5990.0,
"product_id": 456,
"product_name": "Платье летнее",
"product_image": "dress.jpg",
"variant_name": "M",
"size": "M",
"total_price": 5990.0
}
]
}
}
```
## Модели данных
### Структура заказа
| Поле | Тип | Описание |
|------|-----|----------|
| id | Integer | Первичный ключ |
| user_id | Integer | Внешний ключ на таблицу users |
| status | String | Статус заказа (pending, processing, shipped, delivered, cancelled, refunded) |
| total_amount | Float | Общая сумма заказа |
| user_info_json | JSON | JSON с информацией о пользователе |
| delivery_method | String | Способ доставки (cdek, courier) |
| city | String | Город доставки |
| delivery_address | String | Адрес доставки (форматированный) |
| cdek_info | JSON | Информация о доставке CDEK |
| courier_info | JSON | Информация о курьерской доставке |
| shipping_address_id | Integer | Внешний ключ на таблицу addresses (для обратной совместимости) |
| payment_method | String | Способ оплаты (card, sbp) |
| payment_details | String | Детали оплаты |
| items_json | JSON | JSON со списком заказанных товаров |
| tracking_number | String | Номер отслеживания |
| notes | String | Примечания к заказу |
| created_at | DateTime | Дата и время создания |
| updated_at | DateTime | Дата и время обновления |
### Структура элемента заказа
| Поле | Тип | Описание |
|------|-----|----------|
| id | Integer | Первичный ключ |
| order_id | Integer | Внешний ключ на таблицу orders |
| variant_id | Integer | Внешний ключ на таблицу product_variants |
| quantity | Integer | Количество товара |
| price | Float | Цена товара на момент заказа |
| created_at | DateTime | Дата и время создания |
### Структура создания заказа (новый формат)
```typescript
interface UserInfo {
first_name: string;
last_name: string;
email: string;
phone: string;
}
interface Address {
city: string;
street: string;
house: string;
apartment?: string;
postal_code?: string;
formatted_address?: string;
}
interface CdekPvz {
code: string;
city_code: number;
city: string;
address: string;
work_time?: string;
location?: [number, number];
}
interface CdekTariff {
tariff_code: number;
tariff_name: string;
delivery_sum: number;
period_min: number;
period_max: number;
}
interface CdekInfo {
pvz: CdekPvz;
tariff: CdekTariff;
delivery_type: string;
}
interface DeliveryInfo {
method: string;
address: Address;
cdek_info?: CdekInfo;
}
interface OrderItem {
product_id: number;
variant_id: number;
quantity: number;
price: number;
}
interface OrderCreateNew {
user_info: UserInfo;
delivery: DeliveryInfo;
items: OrderItem[];
payment_method: string;
comment?: string;
}
```
### Структура создания заказа (старый формат)
```typescript
interface OrderItemCreate {
variant_id: number;
quantity: number;
}
interface OrderCreate {
shipping_address_id?: number;
payment_method?: string;
notes?: string;
cart_items?: number[];
items?: OrderItemCreate[];
}
```
### Структура обновления заказа
```typescript
interface OrderUpdate {
status?: string;
shipping_address_id?: number;
payment_method?: string;
payment_details?: string;
tracking_number?: string;
notes?: string;
delivery_method?: string;
city?: string;
delivery_address?: string;
cdek_info?: any;
courier_info?: any;
user_info_json?: any;
}
```

View File

@ -9,19 +9,24 @@ services:
expose:
- "8000"
environment:
# - DATABASE_URL=postgresql://postgres:postgres@postgres:5432/shop_db
- DATABASE_URL=postgresql://postgres:postgres@postgres:5432/shop_db
- DEBUG=0
- SECRET_KEY=${SECRET_KEY:-supersecretkey}
- UPLOAD_DIRECTORY=/app/uploads
- MEILISEARCH_URL=http://meilisearch:7700
- MEILISEARCH_KEY=dNmMmxymWpqTFyWSSFyg3xaGlEY6Pn7Ld-dyrnKRMzM
depends_on:
postgres:
condition: service_healthy
meilisearch:
condition: service_started
volumes:
- ./backend/uploads:/app/uploads
networks:
app_network:
aliases:
- backend
- fastapi
restart: always
healthcheck:
test: ["CMD", "curl", "--fail", "http://localhost:8000/" ]
@ -42,6 +47,9 @@ services:
- NEXT_PUBLIC_API_URL=https://${DOMAIN_NAME}/api
- NEXT_PUBLIC_BASE_URL=https://${DOMAIN_NAME}
- NODE_ENV=production
volumes:
- frontend_next_prod:/app/.next # сохраняем сборку в отдельном томе
command: sh -c "npm run build && npm start" # явно запускаем сборку перед стартом
depends_on:
backend:
condition: service_healthy
@ -117,10 +125,38 @@ services:
aliases:
- redis
meilisearch:
image: getmeili/meilisearch:latest
container_name: dressed-for-success-meilisearch
hostname: meilisearch
expose:
- "7700"
environment:
- MEILI_MASTER_KEY=dNmMmxymWpqTFyWSSFyg3xaGlEY6Pn7Ld-dyrnKRMzM
- MEILI_NO_ANALYTICS=true
- MEILI_ENV=production
volumes:
- meilisearch_data:/data.ms
healthcheck:
test: ["CMD", "wget", "--spider", "http://localhost:7700/health"]
interval: 10s
timeout: 5s
retries: 3
start_period: 15s
restart: always
networks:
app_network:
aliases:
- meilisearch
networks:
app_network:
driver: bridge
volumes:
postgres_data:
driver: local
meilisearch_data:
driver: local
frontend_next_prod:
driver: local

View File

@ -1,50 +1,116 @@
version: '3.8' # Compose Specification v3.8
version: '3.8'
services:
# --- Backend Service ---
fastapi:
build:
context: ./backend # директория с Dockerfile FastAPI
volumes:
- ./backend:/app # монтируем код для live-reload
command: uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload # hot-reload
ports:
- "8000:8000"
context: ./backend
dockerfile: Dockerfile
# Не монтируем код в продакшене, он уже скопирован в образ
# volumes:
# - ./backend:/app
# Используем CMD из базового образа tiangolo/uvicorn-gunicorn-fastapi (Gunicorn + Uvicorn workers)
# command: uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload # Это для разработки
command: uvicorn app.main:app --host 0.0.0.0 --port 8000
expose:
- "8000" # Внутренний порт, доступный другим сервисам в сети
networks:
- app-network
environment:
# Переменная для подключения к MeiliSearch внутри Docker-сети
- MEILISEARCH_URL=http://meilisearch:7700
- MEILISEARCH_KEY=dNmMmxymWpqTFyWSSFyg3xaGlEY6Pn7Ld-dyrnKRMzM
# Добавьте сюда другие переменные окружения для бэкенда (ключи API, настройки БД и т.д.)
# - DATABASE_URL=...
# - SECRET_KEY=...
depends_on:
- meilisearch # Запускать после MeiliSearch
restart: unless-stopped
# --- Frontend Service ---
frontend:
build:
context: ./frontend
dockerfile: Dockerfile
# Не монтируем исходный код в продакшене
# volumes:
# - ./frontend:/app
# Сохраняем .next для возможного ускорения перезапусков (опционально)
# volumes:
# - frontend_next:/app/.next
# Команда запускает уже собранное приложение
command: npm start
expose:
- "3000" # Внутренний порт для Next.js сервера
networks:
- app-network
environment:
# URL для API-запросов из БРАУЗЕРА (через Nginx)
# Браузер будет обращаться к /api/... на том же домене/хосте
- NEXT_PUBLIC_API_URL=/api
# URL для API-запросов со стороны СЕРВЕРА Next.js (SSR, API Routes)
# Используем внутреннее имя сервиса Docker для серверных запросов
- NEXT_SERVER_API_URL=http://fastapi/api
# URL для доступа к MinIO (для изображений)
- NEXT_PUBLIC_MINIO_URL=http://45.129.128.113:9000
# Включаем режим отладки для логирования URL изображений
- NEXT_PUBLIC_DEBUG=true
# Убедитесь, что ваш Next.js код использует INTERNAL_API_URL для серверных запросов
# и NEXT_PUBLIC_API_URL (или просто относительный путь /api) для клиентских
depends_on:
- fastapi # Желательно запускать после бэкенда
restart: unless-stopped
# --- PHP Service ---
php:
image: php:8.2-apache # официальный PHP Apache образ
image: php:8.2-apache
volumes:
- ./php:/var/www/html
ports:
- "8081:80"
- ./php:/var/www/html # Монтируем PHP скрипты
expose:
- "80" # Apache внутри контейнера слушает порт 80
networks:
- app-network
restart: unless-stopped
# --- Nginx Reverse Proxy ---
nginx:
image: nginx:stable
image: nginx:alpine
volumes:
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro # монтируем конфиг NGINX
# Монтируем наш конфиг Nginx (только для чтения)
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
# Опционально: монтируем статику, если она не раздается через Next.js/FastAPI
# - ./static:/usr/share/nginx/html/static:ro
ports:
# Единственная точка входа: порт 80 хоста -> порт 80 Nginx
- "80:80"
# Если нужен HTTPS (рекомендуется), добавьте:
# - "443:443"
# И настройте SSL в nginx.conf, монтируя сертификаты
depends_on:
- fastapi
- frontend
- php
networks:
- app-network
restart: always
# --- MeiliSearch Service ---
meilisearch:
image: getmeili/meilisearch:latest
container_name: dressed-for-success-meilisearch
container_name: meilisearch
hostname: meilisearch
ports:
- "7700:7700"
# Не публикуем порт наружу, доступ только через FastAPI/Nginx
# ports:
# - "7700:7700"
expose:
- "7700" # Внутренний порт
environment:
# ВАЖНО: Используйте СВОЙ надежный мастер-ключ!
- MEILI_MASTER_KEY=dNmMmxymWpqTFyWSSFyg3xaGlEY6Pn7Ld-dyrnKRMzM
- MEILI_NO_ANALYTICS=true
- MEILI_ENV=production
volumes:
- meilisearch_data:/meili_data
- meili_data:/data.ms # Сохранение данных MeiliSearch
healthcheck:
test: ["CMD", "wget", "--no-verbose", "--spider", "http://localhost:7700/health"]
interval: 10s
@ -53,14 +119,88 @@ services:
start_period: 15s
restart: always
networks:
app-network:
aliases:
- meilisearch
dns_search: .
volumes:
meilisearch_data:
- app-network
# Алиас не обязателен, если имя сервиса совпадает с hostname
# aliases:
# - meilisearch
# --- Сеть для взаимодействия контейнеров ---
networks:
app-network: # общая сеть для контейнеров
app-network:
driver: bridge
# --- Тома для сохранения данных ---
volumes:
meili_data: # Для данных MeiliSearch
driver: local
frontend_next: # Для кэша сборки Next.js (опционально)
driver: local
# version: '3.8' # Compose Specification v3.8
# services:
# fastapi:
# build:
# context: ./backend # директория с Dockerfile FastAPI
# volumes:
# - ./backend:/app # монтируем код для live-reload
# command: uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload # hot-reload
# ports:
# - "8000:8000"
# networks:
# - app-network
# php:
# image: php:8.2-apache # официальный PHP Apache образ
# volumes:
# - ./php:/var/www/html
# ports:
# - "8081:80"
# networks:
# - app-network
# nginx:
# image: nginx:stable
# volumes:
# - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro # монтируем конфиг NGINX
# ports:
# - "80:80"
# depends_on:
# - fastapi
# - php
# networks:
# - app-network
# meilisearch:
# image: getmeili/meilisearch:latest
# container_name: dressed-for-success-meilisearch
# hostname: meilisearch
# ports:
# - "7700:7700"
# environment:
# - MEILI_MASTER_KEY=dNmMmxymWpqTFyWSSFyg3xaGlEY6Pn7Ld-dyrnKRMzM
# - MEILI_NO_ANALYTICS=true
# - MEILI_ENV=production
# volumes:
# - meilisearch_data:/meili_data
# healthcheck:
# test: ["CMD", "wget", "--no-verbose", "--spider", "http://localhost:7700/health"]
# interval: 10s
# timeout: 5s
# retries: 3
# start_period: 15s
# restart: always
# networks:
# app-network:
# aliases:
# - meilisearch
# dns_search: .
# volumes:
# meilisearch_data:
# networks:
# app-network: # общая сеть для контейнеров
# driver: bridge

BIN
frontend/.DS_Store vendored

Binary file not shown.

21
frontend/Dockerfile Normal file
View File

@ -0,0 +1,21 @@
# Используем Node.js Alpine для сборки и запуска (один этап, без builder)
FROM node:20-alpine
WORKDIR /app
# Копируем package.json и package-lock.json
COPY package*.json ./
# Устанавливаем зависимости
RUN npm ci --legacy-peer-deps
# Копируем весь исходный код приложения
COPY . .
# Собираем проект
RUN npm run build
# Открываем порт для SSR сервера
EXPOSE 3000
# Запуск в режиме production
CMD ["npm", "run", "start"]

View File

@ -1,6 +1,6 @@
"use client"
import { Suspense, useState, useEffect } from "react"
import React, { Suspense, useState, useEffect } from "react"
import Link from "next/link"
import { notFound } from "next/navigation"
import { ArrowLeft, ChevronRight, Truck, RotateCcw, Heart } from "lucide-react"
@ -26,6 +26,10 @@ interface ProductPageProps {
}
export default function ProductPage({ params }: ProductPageProps) {
// Используем React.use() для доступа к свойствам params
const resolvedParams = React.use(params);
const slug = resolvedParams.slug;
const [product, setProduct] = useState<ProductDetails | null>(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState<Error | null>(null)
@ -42,7 +46,7 @@ export default function ProductPage({ params }: ProductPageProps) {
const fetchProduct = async () => {
try {
setLoading(true)
const productData = await catalogService.getProductBySlug(params.slug)
const productData = await catalogService.getProductBySlug(slug)
if (!productData) {
return notFound()
}
@ -55,28 +59,28 @@ export default function ProductPage({ params }: ProductPageProps) {
}
}
fetchProduct()
}, [params.slug])
}, [slug])
return (
<main className="bg-tertiary/5 min-h-screen">
<div className="container mx-auto px-4 py-8 md:py-12">
{/* Навигация и хлебные крошки */}
<motion.div
<motion.div
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }}
className="mb-8"
>
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3">
<Link
href="/catalog"
<Link
href="/catalog"
className="flex items-center text-sm text-primary/70 hover:text-primary transition-colors group"
>
<ArrowLeft className="h-4 w-4 mr-2 group-hover:transform group-hover:-translate-x-1 transition-transform" />
<span>Вернуться в каталог</span>
</Link>
{/* Хлебные крошки */}
<motion.nav
<motion.nav
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.5, delay: 0.3 }}
@ -88,8 +92,8 @@ export default function ProductPage({ params }: ProductPageProps) {
{product?.category_name && (
<>
<ChevronRight className="h-3 w-3 mx-1.5 text-neutral-400 flex-shrink-0" />
<Link
href={`/catalog?category_id=${product.category_id}`}
<Link
href={`/catalog?category_id=${product.category_id}`}
className="hover:text-primary transition-colors"
>
{product.category_name}
@ -198,7 +202,7 @@ export default function ProductPage({ params }: ProductPageProps) {
<ProductDetailsComponent product={product} />
<Separator className="my-6 bg-primary/10" />
{/* Описание товара */}
<motion.div
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: 0.4 }}
@ -206,21 +210,21 @@ export default function ProductPage({ params }: ProductPageProps) {
>
<Tabs defaultValue="description" className="w-full">
<TabsList className="w-full grid grid-cols-2 bg-tertiary/10 h-auto mb-4 rounded-lg p-1">
<TabsTrigger
value="description"
<TabsTrigger
value="description"
className="rounded-md data-[state=active]:bg-white data-[state=active]:shadow-sm text-sm py-2.5"
>
Описание
</TabsTrigger>
<TabsTrigger
value="care"
<TabsTrigger
value="care"
className="rounded-md data-[state=active]:bg-white data-[state=active]:shadow-sm text-sm py-2.5"
>
Уход
</TabsTrigger>
</TabsList>
<motion.div
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3 }}
@ -237,7 +241,7 @@ export default function ProductPage({ params }: ProductPageProps) {
<p className="text-primary/50 italic">Описание отсутствует</p>
)}
</TabsContent>
<TabsContent value="care" className="text-primary/80 text-sm leading-relaxed mt-0">
{product.care_instructions ? (
typeof product.care_instructions === 'string' ? (
@ -253,7 +257,7 @@ export default function ProductPage({ params }: ProductPageProps) {
</Tabs>
</motion.div>
{/* Информация о доставке и возврате */}
<motion.div
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: 0.6 }}
@ -270,7 +274,7 @@ export default function ProductPage({ params }: ProductPageProps) {
Доставка по всей России 1-3 рабочих дня
</p>
</div>
<div className="bg-tertiary/10 rounded-lg p-4 border border-tertiary/20 transition-all duration-300 hover:shadow-md group">
<div className="flex items-center mb-2">
<div className="bg-primary p-2 rounded-full mr-3 text-white group-hover:bg-primary/90 transition-all duration-300">
@ -303,12 +307,12 @@ function ProductSkeleton() {
<Skeleton className="h-8 w-32" />
<Skeleton className="h-5 w-64 sm:w-96" />
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-8 lg:gap-12">
<div className="order-1 md:order-none">
<Skeleton className="aspect-square w-full rounded-3xl shadow-md" />
</div>
<div className="order-2 md:order-none">
<Skeleton className="rounded-3xl p-6 space-y-6 h-full bg-white/50">
<div className="space-y-4">
@ -316,9 +320,9 @@ function ProductSkeleton() {
<Skeleton className="h-12 w-full sm:w-4/5" />
<Skeleton className="h-8 w-40" />
</div>
<Separator className="bg-primary/10" />
<div className="space-y-4 py-4">
<Skeleton className="h-6 w-1/3" />
<div className="flex gap-2 flex-wrap">
@ -329,9 +333,9 @@ function ProductSkeleton() {
<Skeleton className="h-12 w-full rounded-full" />
<Skeleton className="h-12 w-full rounded-full" />
</div>
<Separator className="bg-primary/10" />
<div className="space-y-4 py-4">
<div className="flex gap-2">
<Skeleton className="h-10 w-1/2 rounded-lg" />
@ -339,7 +343,7 @@ function ProductSkeleton() {
</div>
<Skeleton className="h-40 w-full rounded-lg border border-neutral-200" />
</div>
<div className="grid grid-cols-2 gap-4 pt-4">
<Skeleton className="h-24 w-full rounded-lg" />
<Skeleton className="h-24 w-full rounded-lg" />

View File

@ -49,7 +49,7 @@ export default function HomePage() {
id: 2,
name: "Chik & Active",
description: "Комфортные комплекты для активного отдыха, спорта и расслабленных выходных.",
image: "/placeholder.svg?height=800&width=600",
image: "/images/home/drops/DFS_chik&active.svg",
status: "Скоро",
isAvailable: false,
},
@ -57,7 +57,7 @@ export default function HomePage() {
id: 3,
name: "Home & Sleep",
description: "Уютные вещи для дома, отдыха и приятных сновидений.",
image: "/placeholder.svg?height=800&width=600",
image: "/images/home/drops/DFS_home&sleep.svg",
status: "Скоро",
isAvailable: false,
},
@ -92,7 +92,7 @@ export default function HomePage() {
}}
>
<Image
src="/images/hero/image-3.jpeg"
src="/images/home/image-7.jpg"
alt="Dressed for Success Collection"
fill
className="object-cover"
@ -353,14 +353,15 @@ export default function HomePage() {
<div className="flex flex-col md:flex-row h-full">
{/* Изображение коллекции */}
<div className="relative md:w-1/3 aspect-[4/3] md:aspect-auto overflow-hidden">
<div className="relative md:w-1/3 aspect-[4/3] md:aspect-auto overflow-hidden bg-white flex items-center justify-center">
<Image
src={collection.image}
alt={collection.name}
fill
className="object-cover transition-transform duration-700 group-hover:scale-105"
className="object-contain transition-transform duration-700 group-hover:scale-105"
placeholder="blur"
blurDataURL="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///ywAAAAAAQABAAACAUwAOw=="
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
/>
</div>
@ -430,9 +431,9 @@ export default function HomePage() {
<div className="flex justify-center">
<div className="w-16 h-[1px] bg-tertiary/60"></div>
</div>
<p className="mt-6 text-white/80 text-lg italic">
{/* <p className="mt-6 text-white/80 text-lg italic">
Кристина, создательница бренда
</p>
</p> */}
</motion.div>
</div>
</section>
@ -487,9 +488,9 @@ export default function HomePage() {
<div className="grid grid-cols-1 md:grid-cols-2 gap-10 items-center">
{/* Левая колонка - Фото */}
<div className="relative">
<div className="relative aspect-[4/5] overflow-hidden rounded-2xl">
<div className="relative aspect-[2/3] overflow-hidden rounded-2xl">
<Image
src="/images/home/air-dress.jpeg"
src="/images/home/IMG_9028.jpeg"
alt="Платья AIR"
fill
className="object-cover"
@ -555,9 +556,9 @@ export default function HomePage() {
{/* Правая колонка - Фото */}
<div className="relative order-1 md:order-2">
<div className="relative aspect-[4/5] overflow-hidden rounded-2xl">
<div className="relative aspect-[2/3] overflow-hidden rounded-2xl">
<Image
src="/images/home/shik-blouse.jpeg"
src="/images/home/IMG_9135.jpeg"
alt="Блузы SHIK"
fill
className="object-cover"
@ -581,9 +582,9 @@ export default function HomePage() {
<div className="grid grid-cols-1 md:grid-cols-2 gap-10 items-center">
{/* Левая колонка - Фото */}
<div className="relative">
<div className="relative aspect-[4/5] overflow-hidden rounded-2xl">
<div className="relative aspect-[2/3] overflow-hidden rounded-2xl">
<Image
src="/images/home/ibiza-shorts.jpeg"
src="/images/home/IMG_9138.jpeg"
alt="Шорты IBIZA"
fill
className="object-cover"

BIN
frontend/app/.DS_Store vendored

Binary file not shown.

View File

@ -1,9 +1,9 @@
'use client';
import { useState, useEffect } from 'react';
import React, { useState, useEffect } from 'react';
import { useRouter } from 'next/navigation';
import Link from 'next/link';
import { ArrowLeft, Loader2, Package, MapPin, CreditCard, Truck, Receipt, ShoppingBag, Calendar, Clock, Edit,
import { ArrowLeft, Loader2, Package, MapPin, CreditCard, Truck, Receipt, ShoppingBag, Calendar, Clock, Edit,
Download, Mail, ExternalLink, Copy, Printer, AlertTriangle, CheckCircle2, RefreshCw, ChevronDown } from 'lucide-react';
import { toast } from 'sonner';
import { format } from 'date-fns';
@ -133,7 +133,7 @@ const ORDER_STATUSES = [
// Функция получения информации о статусе
function getStatusInfo(status: string) {
return ORDER_STATUSES.find(s => s.value === status) ||
return ORDER_STATUSES.find(s => s.value === status) ||
{ value: status, label: status, color: 'bg-gray-100 text-gray-800' };
}
@ -171,6 +171,10 @@ function formatDate(dateString?: string): string {
export default function OrderDetailsPage({ params }: OrderDetailsPageProps) {
const router = useRouter();
// Используем React.use() для доступа к свойствам params
const resolvedParams = React.use(params);
const orderId = resolvedParams.id;
const [order, setOrder] = useState<Order | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
@ -195,16 +199,16 @@ export default function OrderDetailsPage({ params }: OrderDetailsPageProps) {
setError(null);
try {
const orderId = parseInt(params.id);
if (isNaN(orderId)) {
const id = parseInt(orderId);
if (isNaN(id)) {
throw new Error('Неверный ID заказа');
}
const response = await api.get(`/orders/${orderId}`);
const response = await api.get(`/orders/${id}`);
// Обработка ответа API
let orderData: Order | null = null;
if (response && typeof response === 'object') {
if ('success' in response && response.success && 'order' in response) {
// Если API вернул объект вида { success: true, order: {...} }
@ -220,7 +224,7 @@ export default function OrderDetailsPage({ params }: OrderDetailsPageProps) {
orderData = response as Order;
}
}
if (orderData) {
// Адаптация полей варианта
if (orderData.items) {
@ -232,7 +236,7 @@ export default function OrderDetailsPage({ params }: OrderDetailsPageProps) {
variant_color: item.color // Добавим обработку цвета, если он есть
}));
}
setOrder(orderData);
setUpdatedStatus(orderData.status || '');
setUpdatedTrackingNumber(orderData.tracking_number || '');
@ -251,9 +255,9 @@ export default function OrderDetailsPage({ params }: OrderDetailsPageProps) {
// Обновление заказа
const updateOrder = async () => {
if (!order) return;
setIsActionLoading(true);
try {
// В реальности здесь был бы запрос к API
// const response = await api.patch(`/orders/${order.id}`, {
@ -261,7 +265,7 @@ export default function OrderDetailsPage({ params }: OrderDetailsPageProps) {
// tracking_number: updatedTrackingNumber,
// notes: updatedNotes
// });
// Имитация успешного обновления
const updatedOrder = {
...order,
@ -270,9 +274,9 @@ export default function OrderDetailsPage({ params }: OrderDetailsPageProps) {
notes: updatedNotes,
updated_at: new Date().toISOString()
};
setOrder(updatedOrder);
// Добавляем запись в историю заказа об обновлении статуса
if (updatedStatus !== order.status) {
const newHistoryItem: OrderHistoryItem = {
@ -283,29 +287,29 @@ export default function OrderDetailsPage({ params }: OrderDetailsPageProps) {
created_at: new Date().toISOString(),
created_by: 'Администратор'
};
setOrderHistory([...orderHistory, newHistoryItem]);
}
// При обновлении отслеживания также добавляем запись в историю
if (updatedTrackingNumber && updatedTrackingNumber !== order.tracking_number) {
const trackingHistoryItem: OrderHistoryItem = {
id: Date.now() + 1, // уникальный id
order_id: order.id,
status: updatedStatus,
comment: order.tracking_number
? `Номер отслеживания изменен на "${updatedTrackingNumber}"`
comment: order.tracking_number
? `Номер отслеживания изменен на "${updatedTrackingNumber}"`
: `Добавлен номер отслеживания "${updatedTrackingNumber}"`,
created_at: new Date().toISOString(),
created_by: 'Администратор'
};
setOrderHistory([...orderHistory, trackingHistoryItem]);
}
setIsEditing(false);
toast.success('Заказ успешно обновлен');
// Если статус изменился на "Отправлен", предложить отправить письмо клиенту
if (updatedStatus === 'shipped' && order.status !== 'shipped') {
// Разместите эту функциональность в следующем релизе
@ -329,46 +333,46 @@ export default function OrderDetailsPage({ params }: OrderDetailsPageProps) {
// Обработка объекта адреса в разных форматах
let addressLines = [];
if (address.address_line1) {
addressLines.push(address.address_line1);
}
if (address.address_line2) {
addressLines.push(address.address_line2);
}
if (address.city || address.state || address.postal_code) {
let cityLine = [address.city, address.state, address.postal_code].filter(Boolean).join(', ');
if (cityLine) addressLines.push(cityLine);
}
if (address.country) {
addressLines.push(address.country);
}
// Если нет данных адреса в стандартных полях, попробуем другие форматы
if (addressLines.length === 0) {
if (address.full_address) {
return address.full_address;
}
if (address.street) {
addressLines.push(`${address.street} ${address.house || ''} ${address.apartment ? `, кв. ${address.apartment}` : ''}`);
}
if (address.city || address.postal_code) {
addressLines.push(`${address.city || ''} ${address.postal_code || ''}`);
}
}
return addressLines.join(', ');
};
// Отправка письма клиенту со статусом заказа
const handleSendEmail = async (type: 'status' | 'invoice' | 'tracking') => {
if (!order) return;
setIsActionLoading(true);
try {
// Подготовка данных для отправки в зависимости от типа письма
@ -376,13 +380,13 @@ export default function OrderDetailsPage({ params }: OrderDetailsPageProps) {
order_id: order.id,
email_type: type
};
// В реальном приложении вызов API для отправки письма
// const response = await api.post(`/orders/${order.id}/send-email`, emailData);
// Имитация запроса к API
await new Promise(resolve => setTimeout(resolve, 1000));
toast.success(`Письмо успешно отправлено на ${order.user_email || 'email клиента'}`);
} catch (error) {
console.error('Ошибка при отправке письма:', error);
@ -391,11 +395,11 @@ export default function OrderDetailsPage({ params }: OrderDetailsPageProps) {
setIsActionLoading(false);
}
};
// Копирование информации о заказе в буфер обмена
const handleCopyOrderInfo = () => {
if (!order) return;
const orderInfo = `
Заказ #${order.id}
Клиент: ${order.user_name || `Пользователь #${order.user_id}`}
@ -404,16 +408,16 @@ export default function OrderDetailsPage({ params }: OrderDetailsPageProps) {
Дата создания: ${formatDate(order.created_at)}
Общая сумма: ${formatPrice(order.total_amount)}
`;
navigator.clipboard.writeText(orderInfo)
.then(() => toast.success('Информация о заказе скопирована в буфер обмена'))
.catch(() => toast.error('Не удалось скопировать информацию'));
};
// Обновление данных заказа
const handleRefreshOrder = async () => {
if (!order) return;
setIsActionLoading(true);
try {
await loadOrder(); // Используем существующую функцию загрузки заказа
@ -430,7 +434,7 @@ export default function OrderDetailsPage({ params }: OrderDetailsPageProps) {
const getOrderProgress = () => {
const statuses = ['pending', 'processing', 'shipped', 'delivered'];
const currentIndex = statuses.indexOf(order?.status || '');
if (currentIndex === -1) return 0;
return (currentIndex / (statuses.length - 1)) * 100;
};
@ -438,11 +442,11 @@ export default function OrderDetailsPage({ params }: OrderDetailsPageProps) {
// Загрузка истории заказа
const loadOrderHistory = async () => {
if (!order) return;
try {
// В реальном приложении здесь будет запрос к API
// const response = await api.get(`/orders/${order.id}/history`);
// Имитация данных истории заказа для примера
const mockHistory: OrderHistoryItem[] = [
{
@ -454,7 +458,7 @@ export default function OrderDetailsPage({ params }: OrderDetailsPageProps) {
created_by: 'Система'
}
];
// Добавляем запись об обновлении статуса, если заказ был обновлен
if (order.updated_at && order.updated_at !== order.created_at) {
mockHistory.push({
@ -466,14 +470,14 @@ export default function OrderDetailsPage({ params }: OrderDetailsPageProps) {
created_by: 'Администратор'
});
}
setOrderHistory(mockHistory);
} catch (error) {
console.error('Ошибка при загрузке истории заказа:', error);
// Не показываем ошибку пользователю, так как это не критичная функция
}
};
// Загружаем историю при изменении заказа
useEffect(() => {
if (order) {
@ -484,20 +488,20 @@ export default function OrderDetailsPage({ params }: OrderDetailsPageProps) {
// Использование функции в useEffect
useEffect(() => {
loadOrder();
}, [params.id]);
}, [orderId]);
// Добавление комментария к истории заказа
const addComment = async () => {
if (!order || !newComment.trim()) return;
setIsCommentLoading(true);
try {
// В реальном приложении здесь будет запрос к API
// const response = await api.post(`/orders/${order.id}/history`, {
// comment: newComment,
// });
// Имитация успешного ответа
const newHistoryItem: OrderHistoryItem = {
id: Date.now(), // временный id
@ -507,14 +511,14 @@ export default function OrderDetailsPage({ params }: OrderDetailsPageProps) {
created_at: new Date().toISOString(),
created_by: 'Администратор'
};
// Добавляем новый комментарий в историю
setOrderHistory([...orderHistory, newHistoryItem]);
// Сбрасываем форму и закрываем диалог
setNewComment('');
setCommentDialogOpen(false);
toast.success('Комментарий добавлен к истории заказа');
} catch (error) {
console.error('Ошибка при добавлении комментария:', error);
@ -527,16 +531,16 @@ export default function OrderDetailsPage({ params }: OrderDetailsPageProps) {
// Экспорт истории заказа в PDF
const exportOrderHistory = async () => {
if (!order || orderHistory.length === 0) return;
setIsExporting(true);
try {
// В реальном приложении здесь был бы код для генерации PDF
// Например, с использованием библиотеки jsPDF или запрос к бэкенду
// Имитация экспорта для демонстрации
await new Promise(resolve => setTimeout(resolve, 1500));
toast.success('История заказа экспортирована');
} catch (error) {
console.error('Ошибка при экспорте истории заказа:', error);
@ -549,14 +553,14 @@ export default function OrderDetailsPage({ params }: OrderDetailsPageProps) {
// Печать информации о заказе
const printOrder = () => {
if (!order) return;
// Открываем новое окно для печати
const printWindow = window.open('', '_blank');
if (!printWindow) {
toast.error('Не удалось открыть окно печати. Проверьте настройки блокировки всплывающих окон в браузере.');
return;
}
// Формируем HTML для печати
const printContent = `
<!DOCTYPE html>
@ -587,7 +591,7 @@ export default function OrderDetailsPage({ params }: OrderDetailsPageProps) {
<h1>Заказ #${order.id}</h1>
<div>${new Date().toLocaleDateString('ru-RU')}</div>
</div>
<div class="order-info">
<div class="info-row">
<div class="info-label">Дата создания:</div>
@ -613,7 +617,7 @@ export default function OrderDetailsPage({ params }: OrderDetailsPageProps) {
<div>${order.user_email}</div>
</div>` : ''}
</div>
<h2>Товары</h2>
<table>
<thead>
@ -660,7 +664,7 @@ export default function OrderDetailsPage({ params }: OrderDetailsPageProps) {
</tr>
</tfoot>
</table>
${order.shipping_address ? `
<h2>Адрес доставки</h2>
<div>
@ -673,11 +677,11 @@ export default function OrderDetailsPage({ params }: OrderDetailsPageProps) {
].filter(Boolean).join(', ')}<br>
${order.shipping_address.country || ''}
</div>` : ''}
${order.notes ? `
<h2>Примечания</h2>
<div>${order.notes.replace(/\n/g, '<br>')}</div>` : ''}
<div class="footer">
Документ создан ${new Date().toLocaleDateString('ru-RU', {
day: '2-digit',
@ -687,18 +691,18 @@ export default function OrderDetailsPage({ params }: OrderDetailsPageProps) {
minute: '2-digit'
})}
</div>
<button onclick="window.print(); window.close();" style="margin-top: 20px; padding: 10px 15px;">
Распечатать
</button>
</body>
</html>
`;
// Записываем HTML в новое окно и запускаем печать
printWindow.document.write(printContent);
printWindow.document.close();
// Даем время для загрузки содержимого перед печатью
setTimeout(() => {
printWindow.focus();
@ -709,19 +713,19 @@ export default function OrderDetailsPage({ params }: OrderDetailsPageProps) {
// Функция для отправки уведомления клиенту
const sendNotification = async () => {
if (!order || !notifyEmail.trim() || !notifyMessage.trim()) return;
setIsNotifyLoading(true);
try {
// В реальности здесь был бы запрос к API
// await api.post(`/orders/${order.id}/notify`, {
// email: notifyEmail,
// message: notifyMessage
// });
// Имитация успешной отправки
await new Promise(resolve => setTimeout(resolve, 1000));
// Добавляем запись в историю заказа
const notifyHistoryItem: OrderHistoryItem = {
id: Date.now(),
@ -731,13 +735,13 @@ export default function OrderDetailsPage({ params }: OrderDetailsPageProps) {
created_at: new Date().toISOString(),
created_by: 'Администратор'
};
setOrderHistory([...orderHistory, notifyHistoryItem]);
// Закрываем диалог и сбрасываем форму
setNotifyDialogOpen(false);
setNotifyMessage('');
toast.success('Уведомление отправлено клиенту');
} catch (error) {
console.error('Ошибка при отправке уведомления:', error);
@ -755,14 +759,14 @@ export default function OrderDetailsPage({ params }: OrderDetailsPageProps) {
delivered: `Уважаемый клиент!\n\nВаш заказ №${order?.id} доставлен. Спасибо за покупку!\удем рады видеть вас снова.\n\nС уважением,\nКоманда магазина`,
cancelled: `Уважаемый клиент!\n\nК сожалению, ваш заказ №${order?.id} был отменен. Если у вас есть вопросы, пожалуйста, свяжитесь с нами.\n\nС уважением,\nКоманда магазина`
};
return templates[status] || `Уважаемый клиент!\n\nВаш заказ №${order?.id} обновлен. Статус заказа: ${getStatusInfo(status).label}.\n\nС уважением,\nКоманда магазина`;
};
// Открытие диалога уведомления с предзаполненными данными
const openNotifyDialog = () => {
if (!order) return;
setNotifyEmail(order.user_email || '');
setNotifyMessage(prepareNotificationTemplate(order.status));
setNotifyDialogOpen(true);
@ -791,7 +795,7 @@ export default function OrderDetailsPage({ params }: OrderDetailsPageProps) {
Вернуться к списку заказов
</Button>
</div>
<Card>
<CardHeader>
<CardTitle className="text-destructive">Ошибка</CardTitle>
@ -820,10 +824,10 @@ export default function OrderDetailsPage({ params }: OrderDetailsPageProps) {
<ArrowLeft className="mr-2 h-4 w-4" />
Вернуться к списку заказов
</Button>
<div className="flex items-center gap-2">
<Button
variant="outline"
<Button
variant="outline"
size="sm"
onClick={handleRefreshOrder}
disabled={isActionLoading}
@ -831,7 +835,7 @@ export default function OrderDetailsPage({ params }: OrderDetailsPageProps) {
<RefreshCw className="h-4 w-4 mr-1" />
Обновить
</Button>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" size="sm">
@ -870,7 +874,7 @@ export default function OrderDetailsPage({ params }: OrderDetailsPageProps) {
</DropdownMenu>
</div>
</div>
{/* Действия с заказом */}
<div className="flex flex-wrap gap-2 justify-end">
<Button
@ -882,16 +886,16 @@ export default function OrderDetailsPage({ params }: OrderDetailsPageProps) {
<Mail className="h-4 w-4 mr-2" />
Уведомить клиента
</Button>
<Button
variant="outline"
size="sm"
<Button
variant="outline"
size="sm"
onClick={() => setIsEditing(true)}
>
<Edit className="h-4 w-4 mr-2" />
Редактировать заказ
</Button>
<Button
variant="outline"
size="sm"
@ -900,10 +904,10 @@ export default function OrderDetailsPage({ params }: OrderDetailsPageProps) {
<Printer className="h-4 w-4 mr-2" />
Печать
</Button>
{/* ...другие кнопки действий... */}
</div>
{/* Заголовок заказа */}
<Card>
<CardHeader className="flex flex-row items-center justify-between">
@ -916,7 +920,7 @@ export default function OrderDetailsPage({ params }: OrderDetailsPageProps) {
</div>
{order?.status && <OrderStatusBadge status={order.status} />}
</CardHeader>
{order?.status && !['cancelled', 'refunded'].includes(order.status) && (
<CardContent>
<div className="space-y-2">
@ -925,8 +929,8 @@ export default function OrderDetailsPage({ params }: OrderDetailsPageProps) {
<span>{Math.round(getOrderProgress())}%</span>
</div>
<div className="h-2 bg-muted rounded-full overflow-hidden">
<div
className="h-full bg-primary"
<div
className="h-full bg-primary"
style={{width: `${getOrderProgress()}%`}}
></div>
</div>
@ -940,7 +944,7 @@ export default function OrderDetailsPage({ params }: OrderDetailsPageProps) {
</CardContent>
)}
</Card>
{/* Предупреждение для отмененных заказов */}
{order?.status === 'cancelled' && (
<div className="bg-red-50 border border-red-200 rounded-md p-4 flex items-start">
@ -954,7 +958,7 @@ export default function OrderDetailsPage({ params }: OrderDetailsPageProps) {
</div>
</div>
)}
{/* Уведомление для доставленных заказов */}
{order?.status === 'delivered' && (
<div className="bg-green-50 border border-green-200 rounded-md p-4 flex items-start">
@ -967,7 +971,7 @@ export default function OrderDetailsPage({ params }: OrderDetailsPageProps) {
</div>
</div>
)}
{/* Клиент и адрес доставки */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{/* Информация о клиенте */}
@ -990,7 +994,7 @@ export default function OrderDetailsPage({ params }: OrderDetailsPageProps) {
<div className="flex justify-between">
<dt className="text-muted-foreground">Email:</dt>
<dd className="font-medium">
<a
<a
href={`mailto:${order.user_email}`}
className="text-primary hover:underline flex items-center"
>
@ -1004,7 +1008,7 @@ export default function OrderDetailsPage({ params }: OrderDetailsPageProps) {
<div className="flex justify-between">
<dt className="text-muted-foreground">Телефон:</dt>
<dd className="font-medium">
<a
<a
href={`tel:${order.shipping_address.phone}`}
className="text-primary hover:underline flex items-center"
>
@ -1016,12 +1020,12 @@ export default function OrderDetailsPage({ params }: OrderDetailsPageProps) {
)}
</dl>
</div>
<div className="flex gap-2">
{order?.user_email && (
<Button
variant="outline"
size="sm"
<Button
variant="outline"
size="sm"
className="w-full"
onClick={openNotifyDialog}
>
@ -1063,11 +1067,11 @@ export default function OrderDetailsPage({ params }: OrderDetailsPageProps) {
</div>
</address>
</div>
<div className="flex gap-2">
<Button
variant="outline"
size="sm"
<Button
variant="outline"
size="sm"
className="w-full"
onClick={() => {
if (order?.shipping_address) {
@ -1078,7 +1082,7 @@ export default function OrderDetailsPage({ params }: OrderDetailsPageProps) {
order.shipping_address.postal_code,
order.shipping_address.country
].filter(Boolean).join(', ');
if (address) {
// Копируем адрес в буфер обмена
navigator.clipboard.writeText(address)
@ -1095,10 +1099,10 @@ export default function OrderDetailsPage({ params }: OrderDetailsPageProps) {
<Copy className="h-4 w-4 mr-2" />
Копировать адрес
</Button>
<Button
variant="outline"
size="sm"
<Button
variant="outline"
size="sm"
className="w-full"
onClick={() => {
if (order?.shipping_address) {
@ -1109,7 +1113,7 @@ export default function OrderDetailsPage({ params }: OrderDetailsPageProps) {
order.shipping_address.postal_code,
order.shipping_address.country
].filter(Boolean).join(', ');
if (address.trim()) {
// Открываем адрес в Google Maps
window.open(`https://www.google.com/maps/search/?api=1&query=${encodeURIComponent(address)}`, '_blank');
@ -1134,7 +1138,7 @@ export default function OrderDetailsPage({ params }: OrderDetailsPageProps) {
</CardContent>
</Card>
</div>
{/* Детали заказа */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
{/* Общая информация о заказе */}
@ -1176,7 +1180,7 @@ export default function OrderDetailsPage({ params }: OrderDetailsPageProps) {
</div>
</CardContent>
</Card>
{/* Информация об оплате */}
<Card>
<CardHeader className="pb-3">
@ -1212,7 +1216,7 @@ export default function OrderDetailsPage({ params }: OrderDetailsPageProps) {
</div>
</CardContent>
</Card>
{/* Информация о доставке */}
<Card>
<CardHeader className="pb-3">
@ -1233,9 +1237,9 @@ export default function OrderDetailsPage({ params }: OrderDetailsPageProps) {
<span className="text-muted-foreground">Трекинг-номер:</span>
<span className="font-medium flex items-center">
{order.tracking_number}
<Button
variant="ghost"
size="sm"
<Button
variant="ghost"
size="sm"
className="h-6 w-6 p-0 ml-1"
onClick={() => {
navigator.clipboard.writeText(order.tracking_number || '')
@ -1248,9 +1252,9 @@ export default function OrderDetailsPage({ params }: OrderDetailsPageProps) {
</span>
</div>
<div className="pt-1">
<Button
variant="outline"
size="sm"
<Button
variant="outline"
size="sm"
className="w-full text-xs"
onClick={() => {
if (order?.tracking_number) {
@ -1293,7 +1297,7 @@ export default function OrderDetailsPage({ params }: OrderDetailsPageProps) {
</CardContent>
</Card>
</div>
{/* Товары в заказе */}
<Card>
<CardHeader className="pb-3">
@ -1323,9 +1327,9 @@ export default function OrderDetailsPage({ params }: OrderDetailsPageProps) {
<div className="flex items-center gap-3">
{item.product_image ? (
<div className="h-12 w-12 rounded bg-muted flex items-center justify-center overflow-hidden">
<img
src={item.product_image}
alt={item.product_name || 'Товар'}
<img
src={item.product_image}
alt={item.product_name || 'Товар'}
className="h-full w-full object-cover"
/>
</div>
@ -1364,7 +1368,7 @@ export default function OrderDetailsPage({ params }: OrderDetailsPageProps) {
</TableFooter>
</Table>
</div>
{/* Мобильная версия списка товаров */}
<div className="md:hidden space-y-4">
{order.items.map((item) => (
@ -1372,9 +1376,9 @@ export default function OrderDetailsPage({ params }: OrderDetailsPageProps) {
<div className="flex items-center gap-3">
{item.product_image ? (
<div className="h-16 w-16 rounded bg-muted flex-shrink-0 overflow-hidden">
<img
src={item.product_image}
alt={item.product_name || 'Товар'}
<img
src={item.product_image}
alt={item.product_name || 'Товар'}
className="h-full w-full object-cover"
/>
</div>
@ -1412,7 +1416,7 @@ export default function OrderDetailsPage({ params }: OrderDetailsPageProps) {
</div>
</div>
))}
<div className="flex justify-between items-center pt-4 border-t">
<span className="font-medium">Итого:</span>
<span className="font-bold text-lg">{formatPrice(order.total_amount)}</span>
@ -1426,7 +1430,7 @@ export default function OrderDetailsPage({ params }: OrderDetailsPageProps) {
)}
</CardContent>
</Card>
{/* История заказа */}
<Card>
<CardHeader className="pb-3 flex flex-row items-center justify-between">
@ -1436,8 +1440,8 @@ export default function OrderDetailsPage({ params }: OrderDetailsPageProps) {
</CardTitle>
<div className="flex items-center gap-2">
{orderHistory.length > 0 && (
<Button
variant="outline"
<Button
variant="outline"
size="sm"
onClick={exportOrderHistory}
disabled={isExporting}
@ -1450,8 +1454,8 @@ export default function OrderDetailsPage({ params }: OrderDetailsPageProps) {
{isExporting ? 'Экспорт...' : 'Экспорт'}
</Button>
)}
<Button
variant="outline"
<Button
variant="outline"
size="sm"
onClick={() => setCommentDialogOpen(true)}
>
@ -1464,7 +1468,7 @@ export default function OrderDetailsPage({ params }: OrderDetailsPageProps) {
<div className="relative space-y-0">
{/* Вертикальная линия временной шкалы */}
<div className="absolute left-[15px] top-[24px] bottom-0 w-[2px] bg-muted" />
{orderHistory.map((historyItem, index) => (
<div key={historyItem.id} className="flex gap-4 pb-6 relative">
{/* Маркер статуса */}
@ -1475,7 +1479,7 @@ export default function OrderDetailsPage({ params }: OrderDetailsPageProps) {
{historyItem.status === 'delivered' && <CheckCircle2 className="h-4 w-4" />}
{historyItem.status === 'cancelled' && <AlertTriangle className="h-4 w-4" />}
</div>
{/* Содержание события */}
<div className="flex-1">
<p className="font-medium">
@ -1506,7 +1510,7 @@ export default function OrderDetailsPage({ params }: OrderDetailsPageProps) {
)}
</CardContent>
</Card>
{/* Примечания к заказу */}
{order?.notes && (
<Card>
@ -1518,7 +1522,7 @@ export default function OrderDetailsPage({ params }: OrderDetailsPageProps) {
</CardContent>
</Card>
)}
{/* Диалог редактирования заказа */}
<Dialog open={isEditing} onOpenChange={setIsEditing}>
<DialogContent className="sm:max-w-[500px]">
@ -1528,7 +1532,7 @@ export default function OrderDetailsPage({ params }: OrderDetailsPageProps) {
Обновите информацию о заказе. Нажмите "Сохранить" когда закончите.
</DialogDescription>
</DialogHeader>
<div className="space-y-4 py-4">
<div className="space-y-2">
<Label htmlFor="status">Статус заказа</Label>
@ -1548,7 +1552,7 @@ export default function OrderDetailsPage({ params }: OrderDetailsPageProps) {
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="tracking">Трекинг-номер</Label>
<Input
@ -1558,7 +1562,7 @@ export default function OrderDetailsPage({ params }: OrderDetailsPageProps) {
placeholder="Введите трекинг-номер"
/>
</div>
<div className="space-y-2">
<Label htmlFor="notes">Примечания</Label>
<Textarea
@ -1570,14 +1574,14 @@ export default function OrderDetailsPage({ params }: OrderDetailsPageProps) {
/>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setIsEditing(false)}>Отмена</Button>
<Button onClick={updateOrder}>Сохранить</Button>
</DialogFooter>
</DialogContent>
</Dialog>
{/* Диалог добавления комментария */}
<Dialog open={commentDialogOpen} onOpenChange={setCommentDialogOpen}>
<DialogContent>
@ -1587,7 +1591,7 @@ export default function OrderDetailsPage({ params }: OrderDetailsPageProps) {
Комментарий будет добавлен в историю заказа и будет виден только администраторам.
</DialogDescription>
</DialogHeader>
<div className="py-4">
<Label htmlFor="comment">Комментарий</Label>
<Textarea
@ -1599,16 +1603,16 @@ export default function OrderDetailsPage({ params }: OrderDetailsPageProps) {
rows={4}
/>
</div>
<DialogFooter>
<Button
variant="outline"
<Button
variant="outline"
onClick={() => setCommentDialogOpen(false)}
disabled={isCommentLoading}
>
Отмена
</Button>
<Button
<Button
onClick={addComment}
disabled={isCommentLoading || !newComment.trim()}
className="ml-2"
@ -1625,7 +1629,7 @@ export default function OrderDetailsPage({ params }: OrderDetailsPageProps) {
</DialogFooter>
</DialogContent>
</Dialog>
{/* Диалог для отправки уведомления клиенту */}
<Dialog open={notifyDialogOpen} onOpenChange={setNotifyDialogOpen}>
<DialogContent className="max-w-md">
@ -1635,7 +1639,7 @@ export default function OrderDetailsPage({ params }: OrderDetailsPageProps) {
Отправьте email-уведомление клиенту о статусе заказа.
</DialogDescription>
</DialogHeader>
<div className="space-y-4 py-2">
<div>
<Label htmlFor="notify-email">Email получателя</Label>
@ -1648,7 +1652,7 @@ export default function OrderDetailsPage({ params }: OrderDetailsPageProps) {
className="mt-1"
/>
</div>
<div>
<Label htmlFor="notify-message">Сообщение</Label>
<Textarea
@ -1661,7 +1665,7 @@ export default function OrderDetailsPage({ params }: OrderDetailsPageProps) {
/>
</div>
</div>
<DialogFooter>
<Button
variant="outline"
@ -1689,4 +1693,4 @@ export default function OrderDetailsPage({ params }: OrderDetailsPageProps) {
</Dialog>
</div>
);
}
}

View File

@ -1,17 +1,17 @@
"use client";
import { useEffect, useState } from "react";
import React, { useEffect, useState } from "react";
import { useRouter } from "next/navigation";
import { toast } from "react-hot-toast";
import { AlertCircle } from "lucide-react";
import catalogService, {
ProductUpdateComplete,
ProductDetails,
Category,
Size,
Collection,
ProductImage,
import catalogService, {
ProductUpdateComplete,
ProductDetails,
Category,
Size,
Collection,
ProductImage,
getImageUrl,
normalizeProductImage
} from "@/lib/catalog";
@ -23,7 +23,9 @@ import ProductCompleteForm from "@/components/admin/ProductCompleteForm";
export default function ProductPage({ params }: { params: { id: string } }) {
const router = useRouter();
const productId = parseInt(params.id);
// Используем React.use() для доступа к свойствам params
const resolvedParams = React.use(params);
const productId = parseInt(resolvedParams.id);
const [product, setProduct] = useState<ProductDetails | null>(null);
const [categories, setCategories] = useState<Category[]>([]);
@ -51,7 +53,7 @@ export default function ProductPage({ params }: { params: { id: string } }) {
try {
setLoading(true);
setError(null);
console.log('Загрузка данных для продукта ID:', productId);
const [productResponse, categoriesResponse, sizesResponse, collectionsResponse] = await Promise.all([
@ -71,31 +73,31 @@ export default function ProductPage({ params }: { params: { id: string } }) {
}
setProduct(productResponse);
// Преобразуем категории если не массив
if (categoriesResponse) {
const categories = Array.isArray(categoriesResponse)
? categoriesResponse
const categories = Array.isArray(categoriesResponse)
? categoriesResponse
: ('categories' in categoriesResponse as any ? (categoriesResponse as any).categories : []);
setCategories(categories);
} else {
setCategories([]);
}
// Преобразуем размеры если не массив
if (sizesResponse) {
const sizes = Array.isArray(sizesResponse)
? sizesResponse
const sizes = Array.isArray(sizesResponse)
? sizesResponse
: ('sizes' in sizesResponse as any ? (sizesResponse as any).sizes : []);
setSizes(sizes);
} else {
setSizes([]);
}
// Преобразуем коллекции если не массив
if (collectionsResponse) {
const collections = Array.isArray(collectionsResponse)
? collectionsResponse
const collections = Array.isArray(collectionsResponse)
? collectionsResponse
: ('collections' in collectionsResponse as any ? (collectionsResponse as any).collections : []);
setCollections(collections);
} else {
@ -121,7 +123,7 @@ export default function ProductPage({ params }: { params: { id: string } }) {
setSaving(true);
setError(null);
console.log('Сохранение товара с данными:', formData);
// Подготовка данных для комплексного обновления товара
const updateData: ProductUpdateComplete = {
name: formData.name,
@ -133,7 +135,7 @@ export default function ProductPage({ params }: { params: { id: string } }) {
is_active: formData.is_active,
category_id: formData.category_id ? parseInt(formData.category_id) : undefined,
collection_id: formData.collection_id ? parseInt(formData.collection_id) : undefined,
// Обработка вариантов товара
variants: formData.variants?.map((variant: any) => ({
id: variant.id, // если id есть - будет обновление, если нет - создание
@ -142,12 +144,12 @@ export default function ProductPage({ params }: { params: { id: string } }) {
stock: parseInt(variant.stock) || 0,
is_active: variant.is_active !== false
})),
// Добавляем список вариантов для удаления (преобразуем в массив чисел)
variants_to_remove: Array.isArray(formData.variantsToRemove)
? formData.variantsToRemove.map((id: any) => parseInt(id))
variants_to_remove: Array.isArray(formData.variantsToRemove)
? formData.variantsToRemove.map((id: any) => parseInt(id))
: [],
// Добавляем список обновленных изображений
images: formData.images?.filter((img: any) => img.id).map((img: any) => ({
id: parseInt(img.id),
@ -155,22 +157,22 @@ export default function ProductPage({ params }: { params: { id: string } }) {
alt_text: img.alt_text || '',
is_primary: img.is_primary || false
})),
// Добавляем список изображений для удаления (преобразуем в массив чисел)
images_to_remove: Array.isArray(formData.imagesToRemove)
? formData.imagesToRemove.map((id: any) => parseInt(id))
images_to_remove: Array.isArray(formData.imagesToRemove)
? formData.imagesToRemove.map((id: any) => parseInt(id))
: []
};
console.log('Отправка запроса на комплексное обновление товара:', updateData);
try {
// Отправляем запрос напрямую с помощью api вместо сервиса
const response = await api.put(`/catalog/products/${productId}/complete`, updateData);
console.log('Ответ API при обновлении товара:', response);
let updatedProduct: ProductDetails | null = null;
// Проверяем структуру ответа
if (response && typeof response === 'object') {
// Формат { success, product }
@ -182,11 +184,11 @@ export default function ProductPage({ params }: { params: { id: string } }) {
updatedProduct = response as ProductDetails;
}
}
if (!updatedProduct) {
throw new Error('Не удалось обновить товар: неверный формат ответа от сервера');
}
console.log('Товар успешно обновлен:', updatedProduct);
// Загрузка локальных изображений отдельно
@ -196,24 +198,24 @@ export default function ProductPage({ params }: { params: { id: string } }) {
try {
const file = formData.localImages[i];
console.log(`Загрузка изображения ${i + 1}/${formData.localImages.length}: ${file.name}`);
// Делаем изображение основным, только если нет других изображений
const isPrimary = i === 0 && (!updatedProduct.images || updatedProduct.images.length === 0);
// Создаем FormData для загрузки файла
const imageFormData = new FormData();
imageFormData.append('file', file);
imageFormData.append('is_primary', isPrimary ? 'true' : 'false');
// Отправляем запрос напрямую с помощью api
const imageResponse = await api.post(`/catalog/products/${productId}/images`, imageFormData, {
headers: {
'Content-Type': 'multipart/form-data',
},
});
console.log(`Ответ API при загрузке изображения ${i + 1}:`, imageResponse);
// Добавляем небольшую задержку между загрузками
if (i < formData.localImages.length - 1) {
await new Promise(resolve => setTimeout(resolve, 300));
@ -226,17 +228,17 @@ export default function ProductPage({ params }: { params: { id: string } }) {
}
toast.success('Товар успешно обновлен');
// Перезагружаем товар, чтобы отобразить актуальные данные
try {
console.log('Перезагрузка данных продукта после обновления');
// Запрашиваем продукт напрямую с помощью api вместо сервиса
const refreshResponse = await api.get(`/catalog/products/${productId}`);
console.log('Ответ API при перезагрузке товара:', refreshResponse);
let refreshedProduct: ProductDetails | null = null;
// Проверяем структуру ответа
if (refreshResponse && typeof refreshResponse === 'object') {
// Формат { success, product }
@ -248,9 +250,9 @@ export default function ProductPage({ params }: { params: { id: string } }) {
refreshedProduct = refreshResponse as ProductDetails;
}
}
console.log('Полученные обновленные данные:', refreshedProduct);
if (refreshedProduct) {
// Обрабатываем изображения перед установкой в состояние
if (refreshedProduct.images && Array.isArray(refreshedProduct.images)) {
@ -259,13 +261,13 @@ export default function ProductPage({ params }: { params: { id: string } }) {
image_url: normalizeProductImage(image.image_url)
}));
}
// Устанавливаем primary_image если его нет
if (!refreshedProduct.primary_image && refreshedProduct.images && refreshedProduct.images.length > 0) {
const primaryImage = refreshedProduct.images.find(img => img.is_primary);
refreshedProduct.primary_image = primaryImage ? primaryImage.image_url : refreshedProduct.images[0].image_url;
}
setProduct(refreshedProduct);
} else {
console.warn('Не удалось получить обновленные данные продукта');
@ -350,7 +352,7 @@ export default function ProductPage({ params }: { params: { id: string } }) {
<Button variant="outline" onClick={handleCancel}>Вернуться</Button>
</div>
<Separator className="mb-4" />
{error && (
<Alert variant="destructive" className="mb-4 rounded">
<AlertCircle className="h-4 w-4" />
@ -371,4 +373,4 @@ export default function ProductPage({ params }: { params: { id: string } }) {
/>
</div>
);
}
}

Binary file not shown.

View File

@ -1,316 +1,272 @@
'use client';
import { useState, useCallback, useMemo } from 'react';
import { useRouter } from 'next/navigation';
import { useMemo } from 'react';
import Link from 'next/link';
import { format } from 'date-fns';
import { ru } from 'date-fns/locale';
import { Calendar as CalendarIcon, Search, Filter, RefreshCw, Eye, Edit } from 'lucide-react';
import { Eye, MoreHorizontal, CheckCircle, XCircle, Clock } from 'lucide-react';
import { useOrders } from '@/hooks/useOrders';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Badge } from '@/components/ui/badge';
import { Calendar } from '@/components/ui/calendar';
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
import { cn } from '@/lib/utils';
import { Checkbox } from '@/components/ui/checkbox';
import { DataTable } from '@/components/ui/data-table';
import { Order } from '@/lib/orders';
import useOrdersCache from '@/hooks/useOrdersCache';
import AdminErrorAlert from '@/components/admin/AdminErrorAlert';
import AdminLoadingState from '@/components/admin/AdminLoadingState';
import { cn } from '@/lib/utils';
import { ColumnDef } from '@tanstack/react-table';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Badge } from '@/components/ui/badge';
import { Skeleton } from '@/components/ui/skeleton';
interface OrderManagerProps {
pageSize?: number;
showFilters?: boolean;
showSearch?: boolean;
}
// Статусы заказов
const orderStatuses = [
{ value: 'pending', label: 'Ожидает оплаты', color: 'bg-yellow-100 text-yellow-800' },
{ value: 'paid', label: 'Оплачен', color: 'bg-green-100 text-green-800' },
{ value: 'processing', label: 'В обработке', color: 'bg-blue-100 text-blue-800' },
{ value: 'shipped', label: 'Отправлен', color: 'bg-purple-100 text-purple-800' },
{ value: 'delivered', label: 'Доставлен', color: 'bg-green-100 text-green-800' },
{ value: 'cancelled', label: 'Отменен', color: 'bg-red-100 text-red-800' },
];
export default function OrderManagerOptimized({
pageSize = 10,
showFilters = true,
showSearch = true
}: OrderManagerProps) {
const router = useRouter();
// Состояние компонента
const [currentPage, setCurrentPage] = useState(1);
const [searchQuery, setSearchQuery] = useState('');
const [statusFilter, setStatusFilter] = useState<string>('');
const [dateRange, setDateRange] = useState<[Date | null, Date | null]>([null, null]);
// Используем хук для получения заказов
export default function OrderManagerOptimized() {
const {
orders,
totalOrders,
totalPages,
isLoading,
loading,
error,
refetch,
getStatusLabel,
getStatusClass,
formatDate,
formatAmount
} = useOrdersCache({
page: currentPage,
pageSize,
status: statusFilter,
search: searchQuery,
dateRange
});
selectedOrders,
pagination,
filters,
updateOrderStatus,
setFilter,
setPage,
setPageSize,
selectOrders
} = useOrders();
// Обработчик поиска
const handleSearch = useCallback(() => {
setCurrentPage(1);
}, []);
// Мемоизированные колонки таблицы
const columns = useMemo<ColumnDef<Order>[]>(() => [
{
id: 'select',
header: ({ table }) => (
<Checkbox
checked={table.getIsAllPageRowsSelected()}
onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
aria-label="Выбрать все"
/>
),
cell: ({ row }) => (
<Checkbox
checked={row.getIsSelected()}
onCheckedChange={(value) => row.toggleSelected(!!value)}
aria-label="Выбрать строку"
/>
),
enableSorting: false,
enableHiding: false
},
{
accessorKey: 'id',
header: 'ID',
cell: ({ row }) => <span className="font-mono text-xs">{row.original.id}</span>,
size: 70
},
{
accessorKey: 'created_at',
header: 'Дата',
cell: ({ row }) => {
const date = new Date(row.getValue('created_at'));
return (
<div className="text-sm">
<div>{format(date, 'dd MMM yyyy', { locale: ru })}</div>
<div className="text-gray-500">{format(date, 'HH:mm', { locale: ru })}</div>
</div>
);
}
},
{
accessorKey: 'user_name',
header: 'Клиент',
cell: ({ row }) => {
const order = row.original;
// Получаем данные пользователя из разных возможных источников
const userName = order.user_name ||
(order.user_info_json ?
`${order.user_info_json.first_name || ''} ${order.user_info_json.last_name || ''}` :
'Неизвестно');
const email = order.user_email ||
(order.user_info_json ? order.user_info_json.email : null) ||
'Нет email';
const phone = (order.user_info_json ? order.user_info_json.phone : null) ||
'Нет телефона';
// Обработчик изменения фильтра статуса
const handleStatusFilterChange = useCallback((value: string) => {
setStatusFilter(value === 'all' ? '' : value);
setCurrentPage(1);
}, []);
return (
<div>
<div className="font-medium">{userName}</div>
<div className="text-xs text-gray-500">{email}</div>
<div className="text-xs text-gray-500">{phone}</div>
</div>
);
}
},
{
accessorKey: 'total_amount',
header: 'Сумма',
cell: ({ row }) => {
// Используем total_amount вместо total, так как в API используется total_amount
const total = row.original.total_amount;
return (
<div className="font-medium">
{new Intl.NumberFormat('ru-RU', {
style: 'currency',
currency: 'RUB'
}).format(total || 0)}
</div>
);
}
},
{
accessorKey: 'status',
header: 'Статус',
cell: ({ row }) => {
const status = row.getValue('status') as string;
const statusInfo = orderStatuses.find(s => s.value === status) ||
{ value: status, label: status, color: 'bg-gray-100 text-gray-800' };
// Обработчик изменения страницы
const handlePageChange = useCallback((page: number) => {
setCurrentPage(page);
}, []);
return (
<Badge className={cn(statusInfo.color, 'capitalize')}>
{statusInfo.label}
</Badge>
);
}
},
{
id: 'actions',
header: 'Действия',
cell: ({ row }) => (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon">
<MoreHorizontal className="h-4 w-4" />
<span className="sr-only">Открыть меню</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuLabel>Действия</DropdownMenuLabel>
<DropdownMenuItem asChild>
<Link href={`/admin/orders/${row.original.id}`}>
<Eye className="mr-2 h-4 w-4" />
Просмотреть детали
</Link>
</DropdownMenuItem>
<DropdownMenuLabel>Изменить статус</DropdownMenuLabel>
{orderStatuses.map(status => (
<DropdownMenuItem
key={status.value}
onClick={() => updateOrderStatus(row.original.id, status.value)}
disabled={row.original.status === status.value || loading.update}
>
{status.value === 'paid' && <CheckCircle className="mr-2 h-4 w-4 text-green-500" />}
{status.value === 'cancelled' && <XCircle className="mr-2 h-4 w-4 text-red-500" />}
{status.value === 'pending' && <Clock className="mr-2 h-4 w-4 text-yellow-500" />}
{!['paid', 'cancelled', 'pending'].includes(status.value) && <div className="w-4 h-4 mr-2" />}
{status.label}
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
)
}
], [loading.update, updateOrderStatus]);
// Обработчик перехода на страницу просмотра заказа
const handleViewOrder = useCallback((id: number) => {
router.push(`/admin/orders/${id}`);
}, [router]);
// Обработчик перехода на страницу редактирования заказа
const handleEditOrder = useCallback((id: number) => {
router.push(`/admin/orders/${id}/edit`);
}, [router]);
// Статусы заказов для фильтра
const orderStatuses = useMemo(() => [
{ value: 'pending', label: 'Ожидает оплаты' },
{ value: 'paid', label: 'Оплачен' },
{ value: 'processing', label: 'В обработке' },
{ value: 'shipped', label: 'Отправлен' },
{ value: 'delivered', label: 'Доставлен' },
{ value: 'cancelled', label: 'Отменен' }
], []);
// Если данные загружаются, показываем индикатор загрузки
if (isLoading && !orders.length) {
return <AdminLoadingState message="Загрузка заказов..." />;
}
// Если произошла ошибка, показываем сообщение об ошибке
if (error && !orders.length) {
if (error) {
return (
<AdminErrorAlert
title="Ошибка загрузки заказов"
message={String(error)}
onRetry={() => refetch()}
/>
<div className="p-4 text-red-500">
Произошла ошибка при загрузке заказов: {error.message}
</div>
);
}
return (
<div className="space-y-6">
{/* Фильтры и поиск */}
{(showFilters || showSearch) && (
<Card>
<CardContent className="p-6">
<div className="flex flex-col md:flex-row gap-4">
{showSearch && (
<div className="flex-1">
<div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400" size={18} />
<Input
placeholder="Поиск заказов..."
className="pl-10"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && handleSearch()}
/>
</div>
</div>
)}
{showFilters && (
<>
<div className="w-full md:w-48">
<Select
value={statusFilter || 'all'}
onValueChange={handleStatusFilterChange}
>
<SelectTrigger>
<SelectValue placeholder="Статус" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">Все статусы</SelectItem>
{orderStatuses.map((status) => (
<SelectItem key={status.value} value={status.value}>
{status.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="w-full md:w-64">
<Popover>
<PopoverTrigger asChild>
<Button
variant="outline"
className="w-full justify-start text-left font-normal"
>
<CalendarIcon className="mr-2 h-4 w-4" />
{dateRange[0] && dateRange[1] ? (
<>
{format(dateRange[0], 'dd.MM.yyyy', { locale: ru })} - {format(dateRange[1], 'dd.MM.yyyy', { locale: ru })}
</>
) : (
<span>Выберите период</span>
)}
</Button>
</PopoverTrigger>
<PopoverContent className="w-auto p-0" align="start">
<Calendar
mode="range"
selected={dateRange as [Date, Date]}
onSelect={(range) => setDateRange(range)}
initialFocus
locale={ru}
/>
<div className="p-3 border-t border-border flex justify-between">
<Button
variant="outline"
size="sm"
onClick={() => setDateRange([null, null])}
>
Сбросить
</Button>
<Button
size="sm"
onClick={() => handleSearch()}
>
Применить
</Button>
</div>
</PopoverContent>
</Popover>
</div>
</>
)}
<Button onClick={handleSearch} className="md:w-auto">
<Filter className="h-4 w-4 mr-2" />
Применить
</Button>
<Button variant="outline" onClick={() => refetch()} className="md:w-auto">
<RefreshCw className="h-4 w-4 mr-2" />
Обновить
</Button>
</div>
</CardContent>
</Card>
)}
<div className="space-y-4">
<div className="flex items-center justify-between">
<h1 className="text-2xl font-bold">Управление заказами</h1>
</div>
{/* Таблица заказов */}
<Card>
<CardHeader className="pb-3">
<div className="flex justify-between items-center">
<div>
<CardTitle>Заказы</CardTitle>
<CardDescription>
Всего заказов: {totalOrders}
</CardDescription>
</div>
</div>
</CardHeader>
<CardContent>
<div className="overflow-x-auto">
<table className="w-full">
<thead>
<tr className="border-b">
<th className="py-3 px-4 text-left font-medium">ID</th>
<th className="py-3 px-4 text-left font-medium">Клиент</th>
<th className="py-3 px-4 text-left font-medium">Дата</th>
<th className="py-3 px-4 text-left font-medium">Статус</th>
<th className="py-3 px-4 text-left font-medium">Сумма</th>
<th className="py-3 px-4 text-right font-medium">Действия</th>
</tr>
</thead>
<tbody>
{orders.map((order) => (
<tr key={order.id} className="border-b hover:bg-gray-50">
<td className="py-3 px-4">
#{order.id}
</td>
<td className="py-3 px-4">
{order.user_name || `Пользователь #${order.user_id}`}
</td>
<td className="py-3 px-4">
{formatDate(order.created_at)}
</td>
<td className="py-3 px-4">
<Badge className={cn(getStatusClass(order.status))}>
{getStatusLabel(order.status)}
</Badge>
</td>
<td className="py-3 px-4">
{order.total !== undefined && order.total !== null
? formatAmount(order.total)
: 'Н/Д'}
</td>
<td className="py-3 px-4 text-right">
<div className="flex justify-end space-x-2">
<Button variant="ghost" size="icon" onClick={() => handleViewOrder(order.id)}>
<Eye className="h-4 w-4" />
</Button>
<Button variant="ghost" size="icon" onClick={() => handleEditOrder(order.id)}>
<Edit className="h-4 w-4" />
</Button>
</div>
</td>
</tr>
))}
{orders.length === 0 && (
<tr>
<td colSpan={6} className="py-6 text-center text-gray-500">
Заказы не найдены
</td>
</tr>
)}
</tbody>
</table>
</div>
{/* Пагинация */}
{totalPages > 1 && (
<div className="flex justify-between items-center mt-4">
<div className="text-sm text-gray-500">
Страница {currentPage} из {totalPages}
</div>
<div className="flex space-x-2">
<Button
variant="outline"
size="sm"
onClick={() => handlePageChange(currentPage - 1)}
disabled={currentPage === 1}
>
Назад
</Button>
<Button
variant="outline"
size="sm"
onClick={() => handlePageChange(currentPage + 1)}
disabled={currentPage === totalPages}
>
Вперед
</Button>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<Input
placeholder="Поиск по имени, email, телефону..."
value={filters.search}
onChange={(e) => setFilter('search', e.target.value)}
/>
<Select
value={filters.status || "all"}
onValueChange={(value) => setFilter('status', value === "all" ? "" : value)}
>
<SelectTrigger>
<SelectValue placeholder="Все статусы" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">Все статусы</SelectItem>
{orderStatuses.map((status) => (
<SelectItem key={status.value} value={status.value}>
{status.label}
</SelectItem>
))}
</SelectContent>
</Select>
<Select
value={filters.dateRange || "all"}
onValueChange={(value) => setFilter('dateRange', value === "all" ? "" : value)}
>
<SelectTrigger>
<SelectValue placeholder="Период" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">Все время</SelectItem>
<SelectItem value="today">Сегодня</SelectItem>
<SelectItem value="week">Эта неделя</SelectItem>
<SelectItem value="month">Этот месяц</SelectItem>
<SelectItem value="year">Этот год</SelectItem>
</SelectContent>
</Select>
</div>
{loading.fetch && !orders.length ? (
<div className="space-y-3">
{Array.from({ length: 5 }).map((_, i) => (
<div key={i} className="flex items-center space-x-4">
<Skeleton className="h-12 w-12" />
<div className="space-y-2">
<Skeleton className="h-4 w-[250px]" />
<Skeleton className="h-4 w-[200px]" />
</div>
</div>
)}
</CardContent>
</Card>
))}
</div>
) : (
<DataTable<Order>
columns={columns}
data={orders}
loading={loading.fetch}
pagination={{
pageIndex: pagination.page - 1,
pageSize: pagination.pageSize,
pageCount: Math.ceil(pagination.total / pagination.pageSize),
onPageChange: (pageIndex) => setPage(pageIndex + 1),
onPageSizeChange: setPageSize
}}
selection={{
selectedRowIds: selectedOrders,
onSelectionChange: selectOrders
}}
/>
)}
</div>
);
}

View File

@ -98,7 +98,7 @@ const CdekWidgetPopup = dynamic(() => import("./cdek-widget-popup"), {
});
const YANDEX_API_KEY = process.env.NEXT_PUBLIC_YANDEX_MAPS_KEY || "46c9d016-fad5-489c-86d5-56ce393e0344";
const SERVICE_PATH = "http://localhost:8081/service.php";
const SERVICE_PATH = process.env.NEXT_PUBLIC_BASE_URL + "/php/service.php";
// Задержка debounce для поля ввода города (в миллисекундах)
const CITY_DEBOUNCE_DELAY = 500;

View File

@ -1,18 +1,22 @@
'use client';
import { useState } from 'react';
import {
ColumnDef,
flexRender,
getCoreRowModel,
useReactTable,
getPaginationRowModel,
SortingState,
getSortedRowModel,
ColumnFiltersState,
getFilteredRowModel,
VisibilityState,
RowSelectionState,
PaginationState,
Table as TableInstance,
Row,
Header,
Cell,
TableOptions,
RowSelectionState,
HeaderGroup
} from '@tanstack/react-table';
import {
@ -51,48 +55,61 @@ export function DataTable<TData extends { id: number }>({
pagination,
selection
}: DataTableProps<TData>) {
const tableOptions: TableOptions<TData> = {
const [sorting, setSorting] = useState<SortingState>([]);
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]);
const [columnVisibility, setColumnVisibility] = useState<VisibilityState>({});
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
// Преобразуем массив ID в объект для react-table
const initialRowSelection = selection?.selectedRowIds.reduce((acc, id) => {
acc[id] = true;
return acc;
}, {} as RowSelectionState) || {};
const table = useReactTable({
data,
columns,
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
pageCount: pagination?.pageCount,
manualPagination: !!pagination,
enableRowSelection: !!selection,
enableMultiRowSelection: !!selection,
onSortingChange: setSorting,
getSortedRowModel: getSortedRowModel(),
onColumnFiltersChange: setColumnFilters,
getFilteredRowModel: getFilteredRowModel(),
onColumnVisibilityChange: setColumnVisibility,
onRowSelectionChange: (updatedSelection) => {
setRowSelection(updatedSelection);
// Если есть обработчик выбора строк
if (selection) {
const selectedRows = table.getSelectedRowModel().rows;
const selectedIds = selectedRows.map(row => row.original.id);
selection.onSelectionChange(selectedIds);
}
},
state: {
sorting,
columnFilters,
columnVisibility,
rowSelection: initialRowSelection,
pagination: pagination ? {
pageIndex: pagination.pageIndex,
pageSize: pagination.pageSize
} : undefined,
rowSelection: selection ? data.reduce<RowSelectionState>((acc, row) => {
acc[row.id] = selection.selectedRowIds.includes(row.id);
return acc;
}, {}) : {}
},
onPaginationChange: pagination ? (updater: ((state: PaginationState) => PaginationState) | PaginationState) => {
if (typeof updater === 'function') {
const newState = updater(table.getState().pagination);
pagination.onPageChange(newState.pageIndex);
}
} : undefined,
onRowSelectionChange: selection ? () => {
const selectedRows = table.getSelectedRowModel().rows;
const selectedIds = selectedRows.map(row => row.original.id);
selection.onSelectionChange(selectedIds);
} : undefined
};
const table = useReactTable(tableOptions);
manualPagination: !!pagination,
pageCount: pagination?.pageCount || -1,
enableRowSelection: !!selection,
enableMultiRowSelection: !!selection,
});
return (
<div className="space-y-4">
<div className="rounded-md border">
<Table>
<TableHeader>
<TableRow>
{table.getHeaderGroups().map((headerGroup: HeaderGroup<TData>) => (
headerGroup.headers.map((header: Header<TData, unknown>) => (
{table.getHeaderGroups().map((headerGroup: HeaderGroup<TData>) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header: Header<TData, unknown>) => (
<TableHead key={header.id}>
{header.isPlaceholder
? null
@ -101,9 +118,9 @@ export function DataTable<TData extends { id: number }>({
header.getContext()
)}
</TableHead>
))
))}
</TableRow>
))}
</TableRow>
))}
</TableHeader>
<TableBody>
{loading ? (
@ -141,64 +158,75 @@ export function DataTable<TData extends { id: number }>({
{pagination && (
<div className="flex items-center justify-between">
<div className="flex items-center space-x-2">
<p className="text-sm font-medium">Строк на странице</p>
<Select
value={`${pagination.pageSize}`}
onValueChange={(value) => pagination.onPageSizeChange(Number(value))}
>
<SelectTrigger className="h-8 w-[70px]">
<SelectValue placeholder={pagination.pageSize} />
</SelectTrigger>
<SelectContent side="top">
{[10, 20, 30, 40, 50].map((pageSize) => (
<SelectItem key={pageSize} value={`${pageSize}`}>
{pageSize}
</SelectItem>
))}
</SelectContent>
</Select>
<div className="flex-1 text-sm text-muted-foreground">
{selection && Object.keys(rowSelection).length > 0 && (
<div>Выбрано {Object.keys(rowSelection).length} из {table.getFilteredRowModel().rows.length}</div>
)}
</div>
<div className="flex items-center space-x-2">
<Button
variant="outline"
size="sm"
onClick={() => pagination.onPageChange(0)}
disabled={!table.getCanPreviousPage()}
>
«
</Button>
<Button
variant="outline"
size="sm"
onClick={() => pagination.onPageChange(table.getState().pagination.pageIndex - 1)}
disabled={!table.getCanPreviousPage()}
>
</Button>
<span className="text-sm">
<div className="flex items-center space-x-6 lg:space-x-8">
<div className="flex items-center space-x-2">
<p className="text-sm font-medium">Строк на странице</p>
<Select
value={`${pagination.pageSize}`}
onValueChange={(value) => {
const pageSize = Number(value);
table.setPageSize(pageSize);
pagination.onPageSizeChange(pageSize);
}}
>
<SelectTrigger className="h-8 w-[70px]">
<SelectValue placeholder={pagination.pageSize} />
</SelectTrigger>
<SelectContent side="top">
{[10, 20, 30, 40, 50].map((pageSize) => (
<SelectItem key={pageSize} value={`${pageSize}`}>
{pageSize}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="flex w-[100px] items-center justify-center text-sm font-medium">
Страница {table.getState().pagination.pageIndex + 1} из{' '}
{pagination.pageCount}
</span>
<Button
variant="outline"
size="sm"
onClick={() => pagination.onPageChange(table.getState().pagination.pageIndex + 1)}
disabled={!table.getCanNextPage()}
>
</Button>
<Button
variant="outline"
size="sm"
onClick={() => pagination.onPageChange(pagination.pageCount - 1)}
disabled={!table.getCanNextPage()}
>
»
</Button>
</div>
<div className="flex items-center space-x-2">
<Button
variant="outline"
size="sm"
onClick={() => pagination.onPageChange(0)}
disabled={!table.getCanPreviousPage() || loading}
>
«
</Button>
<Button
variant="outline"
size="sm"
onClick={() => pagination.onPageChange(table.getState().pagination.pageIndex - 1)}
disabled={!table.getCanPreviousPage() || loading}
>
</Button>
<Button
variant="outline"
size="sm"
onClick={() => pagination.onPageChange(table.getState().pagination.pageIndex + 1)}
disabled={!table.getCanNextPage() || loading}
>
</Button>
<Button
variant="outline"
size="sm"
onClick={() => pagination.onPageChange(pagination.pageCount - 1)}
disabled={!table.getCanNextPage() || loading}
>
»
</Button>
</div>
</div>
</div>
)}
</div>
);
}
}

View File

@ -3,6 +3,14 @@ import { toast } from 'react-hot-toast';
import api from '@/lib/api';
import { cacheKeys } from '@/lib/api-cache';
/**
* Хук для выполнения GET-запросов с использованием react-query
* @param key Ключ кэша
* @param url URL для запроса
* @param params Параметры запроса
* @param options Дополнительные опции для useQuery
* @returns Результат выполнения useQuery
*/
export function useAdminQuery<TData = unknown, TError = unknown>(
key: string | string[],
url: string,
@ -10,23 +18,35 @@ export function useAdminQuery<TData = unknown, TError = unknown>(
options: Omit<UseQueryOptions<TData, TError, TData>, 'queryKey' | 'queryFn'> = {}
) {
const queryKey = Array.isArray(key) ? key : [key];
return useQuery<TData, TError>({
queryKey,
queryFn: async () => {
try {
return await api.get<TData>(url, { params });
const response = await api.get<TData>(url, { params });
return response;
} catch (error) {
console.error(`Error fetching data from ${url}:`, error);
throw error;
}
},
// Оптимизированные настройки кэширования
staleTime: 5 * 60 * 1000, // 5 минут - данные считаются свежими
gcTime: 10 * 60 * 1000, // 10 минут - данные хранятся в кэше
...options
});
}
/**
* Хук для выполнения мутаций (POST, PUT, DELETE) с использованием react-query
* @param method Метод запроса
* @param url URL для запроса или функция, возвращающая URL
* @param options Дополнительные опции для useMutation
* @returns Результат выполнения useMutation
*/
export function useAdminMutation<TData = unknown, TError = unknown, TVariables = unknown>(
method: 'post' | 'put' | 'delete',
url: string,
url: string | ((variables: TVariables) => string),
options: {
onSuccessMessage?: string;
onErrorMessage?: string;
@ -41,36 +61,51 @@ export function useAdminMutation<TData = unknown, TError = unknown, TVariables =
invalidateQueries = [],
mutationOptions = {}
} = options;
return useMutation<TData, TError, TVariables>({
mutationFn: async (variables) => {
try {
const resolvedUrl = typeof url === 'function' ? url(variables) : url;
if (method === 'delete') {
return await api.delete<TData>(url);
return await api.delete<TData>(resolvedUrl);
} else {
return await api[method]<TData>(url, variables);
return await api[method]<TData>(resolvedUrl, variables);
}
} catch (error) {
console.error(`Error executing ${method.toUpperCase()} ${url}:`, error);
console.error(`Error in ${method.toUpperCase()} request to ${typeof url === 'string' ? url : 'dynamic URL'}:`, error);
throw error;
}
},
onSuccess: (data, variables, context) => {
if (onSuccessMessage) toast.success(onSuccessMessage);
// Показываем сообщение об успехе
if (onSuccessMessage) {
toast.success(onSuccessMessage);
}
// Инвалидируем кэш для указанных запросов
if (invalidateQueries.length > 0) {
invalidateQueries.forEach(key => {
if (Array.isArray(key)) {
queryClient.invalidateQueries({ queryKey: key });
invalidateQueries.forEach(queryKey => {
if (Array.isArray(queryKey)) {
queryClient.invalidateQueries({ queryKey });
} else {
queryClient.invalidateQueries({ queryKey: [key] });
queryClient.invalidateQueries({ queryKey: [queryKey] });
}
});
}
// Вызываем пользовательский обработчик успеха
if (mutationOptions.onSuccess) {
mutationOptions.onSuccess(data, variables, context);
}
},
onError: (error, variables, context) => {
if (onErrorMessage) toast.error(onErrorMessage);
// Показываем сообщение об ошибке
if (onErrorMessage) {
toast.error(onErrorMessage);
}
// Вызываем пользовательский обработчик ошибки
if (mutationOptions.onError) {
mutationOptions.onError(error, variables, context);
}
@ -127,7 +162,7 @@ export function useSizes(params: Record<string, any> = {}, options = {}) {
export function useOrders(params: Record<string, any> = {}, options = {}) {
return useAdminQuery(
[cacheKeys.orders, JSON.stringify(params)],
'/admin/orders',
'/orders',
params,
options
);
@ -136,7 +171,7 @@ export function useOrders(params: Record<string, any> = {}, options = {}) {
export function useOrder(id: number | string, options = {}) {
return useAdminQuery(
cacheKeys.order(id),
`/admin/orders/${id}`,
`/orders/${id}`,
{},
options
);
@ -145,7 +180,7 @@ export function useOrder(id: number | string, options = {}) {
export function useDashboardStats(options = {}) {
return useAdminQuery(
cacheKeys.dashboardStats,
'/admin/dashboard/stats',
'/dashboard/stats',
{},
options
);
@ -154,7 +189,7 @@ export function useDashboardStats(options = {}) {
export function useRecentOrders(limit = 5, options = {}) {
return useAdminQuery(
cacheKeys.recentOrders,
'/admin/orders/recent',
'/orders/recent',
{ limit },
options
);
@ -163,7 +198,7 @@ export function useRecentOrders(limit = 5, options = {}) {
export function usePopularProducts(limit = 5, options = {}) {
return useAdminQuery(
cacheKeys.popularProducts,
'/admin/products/popular',
'/catalog/products/popular',
{ limit },
options
);
@ -182,4 +217,4 @@ export default {
useDashboardStats,
useRecentOrders,
usePopularProducts
};
};

286
frontend/hooks/useOrders.ts Normal file
View File

@ -0,0 +1,286 @@
import { useState, useCallback, useEffect } from 'react';
import { useRouter, useSearchParams } from 'next/navigation';
import { toast } from 'react-hot-toast';
import { useAdminQuery, useAdminMutation } from '@/hooks/useAdminApi';
import { cacheKeys } from '@/lib/api-cache';
import { Order } from '@/lib/orders';
interface OrdersState {
orders: Order[];
loading: {
fetch: boolean;
update: boolean;
};
error: Error | null;
selectedOrders: number[];
pagination: {
page: number;
pageSize: number;
total: number;
};
filters: {
search: string;
status: string;
dateRange: string;
};
}
export function useOrders() {
const router = useRouter();
const searchParams = useSearchParams();
// Состояние
const [state, setState] = useState<OrdersState>({
orders: [],
loading: {
fetch: true,
update: false,
},
error: null,
selectedOrders: [],
pagination: {
page: 1,
pageSize: 10,
total: 0,
},
filters: {
search: '',
status: '',
dateRange: '',
},
});
// Получаем параметры из URL при первой загрузке
useEffect(() => {
const page = searchParams.get('page') ? parseInt(searchParams.get('page') as string, 10) : 1;
const search = searchParams.get('search') || '';
const status = searchParams.get('status') || '';
const dateRange = searchParams.get('dateRange') || '';
setState(prev => ({
...prev,
pagination: {
...prev.pagination,
page,
},
filters: {
search,
status,
dateRange,
},
}));
}, [searchParams]);
// Формируем параметры запроса
const queryParams = {
skip: (state.pagination.page - 1) * state.pagination.pageSize,
limit: state.pagination.pageSize,
search: state.filters.search,
status: state.filters.status || undefined,
date_range: state.filters.dateRange || undefined,
};
// Запрос на получение заказов
const { data, isLoading, error, refetch } = useAdminQuery<Order[]>(
[cacheKeys.orders, JSON.stringify(queryParams)],
'/orders',
queryParams,
{
onSuccess: (data) => {
if (data) {
// API возвращает массив заказов, а не объект с полями orders и total
const orders = Array.isArray(data) ? data : [];
setState(prev => ({
...prev,
orders: orders,
pagination: {
...prev.pagination,
total: orders.length, // Используем длину массива как общее количество
},
loading: {
...prev.loading,
fetch: false,
},
}));
}
},
onError: (err) => {
setState(prev => ({
...prev,
error: err as Error,
loading: {
...prev.loading,
fetch: false,
},
}));
},
}
);
// Мутация для обновления статуса заказа
const updateStatusMutation = useAdminMutation<any, Error, { id: number, status: string }>(
'put',
({ id }) => `/orders/${id}`,
{
onSuccessMessage: 'Статус заказа успешно обновлен',
onErrorMessage: 'Не удалось обновить статус заказа',
invalidateQueries: [cacheKeys.orders],
mutationOptions: {
onMutate: () => {
setState(prev => ({
...prev,
loading: {
...prev.loading,
update: true,
},
}));
},
onSettled: () => {
setState(prev => ({
...prev,
loading: {
...prev.loading,
update: false,
},
}));
},
},
}
);
// Обработчик обновления статуса заказа
const updateOrderStatus = useCallback(async (id: number, status: string) => {
try {
// Передаем только статус в теле запроса, как требуется API
await updateStatusMutation.mutateAsync({
id,
status
});
} catch (error) {
console.error('Error updating order status:', error);
}
}, [updateStatusMutation]);
// Обработчик изменения фильтров
const setFilter = useCallback((key: keyof OrdersState['filters'], value: any) => {
setState(prev => ({
...prev,
filters: {
...prev.filters,
[key]: value,
},
pagination: {
...prev.pagination,
page: 1, // Сбрасываем страницу при изменении фильтров
},
}));
// Обновляем URL с новыми параметрами
const params = new URLSearchParams(searchParams.toString());
params.set('page', '1');
if (value !== null && value !== '') {
params.set(key, value.toString());
} else {
params.delete(key);
}
// Обновляем URL
router.push(`/admin/orders?${params.toString()}`);
}, [router, searchParams]);
// Обработчик изменения страницы
const setPage = useCallback((page: number) => {
setState(prev => ({
...prev,
pagination: {
...prev.pagination,
page,
},
}));
// Обновляем URL с новой страницей
const params = new URLSearchParams(searchParams.toString());
params.set('page', page.toString());
router.push(`/admin/orders?${params.toString()}`);
}, [router, searchParams]);
// Обработчик изменения размера страницы
const setPageSize = useCallback((pageSize: number) => {
setState(prev => ({
...prev,
pagination: {
...prev.pagination,
pageSize,
page: 1, // Сбрасываем страницу при изменении размера
},
}));
// Обновляем URL с новым размером страницы
const params = new URLSearchParams(searchParams.toString());
params.set('page', '1');
params.set('pageSize', pageSize.toString());
router.push(`/admin/orders?${params.toString()}`);
}, [router, searchParams]);
// Обработчик выбора заказов
const selectOrders = useCallback((selectedIds: number[]) => {
setState(prev => ({
...prev,
selectedOrders: selectedIds,
}));
}, []);
// Обновляем состояние при изменении данных
useEffect(() => {
if (data) {
// API возвращает массив заказов, а не объект с полями orders и total
const orders = Array.isArray(data) ? data : [];
setState(prev => ({
...prev,
orders: orders,
pagination: {
...prev.pagination,
total: orders.length, // Используем длину массива как общее количество
},
loading: {
...prev.loading,
fetch: false,
},
}));
}
}, [data]);
// Обновляем состояние при ошибке
useEffect(() => {
if (error) {
setState(prev => ({
...prev,
error: error as Error,
loading: {
...prev.loading,
fetch: false,
},
}));
}
}, [error]);
return {
orders: state.orders,
loading: {
fetch: isLoading || state.loading.fetch,
update: state.loading.update
},
error: state.error,
selectedOrders: state.selectedOrders,
pagination: state.pagination,
filters: state.filters,
updateOrderStatus,
setFilter,
setPage,
setPageSize,
selectOrders,
refetch,
};
}
export default useOrders;

View File

@ -1,54 +1,284 @@
import { useState, useCallback } from 'react';
import { useProducts as useProductsApi } from './useAdminApi';
import { useState, useCallback, useEffect } from 'react';
import { useRouter, useSearchParams } from 'next/navigation';
import { toast } from 'react-hot-toast';
import { useAdminQuery, useAdminMutation } from '@/hooks/useAdminApi';
import { cacheKeys } from '@/lib/api-cache';
import { Product } from '@/lib/catalog';
interface Filters {
search?: string;
category?: string;
is_active?: boolean | '';
interface ProductsState {
products: Product[];
loading: {
fetch: boolean;
delete: boolean;
};
error: Error | null;
selectedProducts: number[];
pagination: {
page: number;
pageSize: number;
total: number;
};
filters: {
search: string;
category: string;
collection: string;
active: boolean | null;
};
}
export function useProducts() {
const [filters, setFilters] = useState<Filters>({});
const [page, setPage] = useState(1);
const [pageSize, setPageSize] = useState(20);
const [selectedProducts, setSelectedProducts] = useState<number[]>([]);
const router = useRouter();
const searchParams = useSearchParams();
const params: Record<string, any> = {
skip: (page - 1) * pageSize,
limit: pageSize,
search: filters.search || undefined,
category_id: filters.category || undefined,
is_active: filters.is_active === '' ? undefined : filters.is_active,
// Состояние
const [state, setState] = useState<ProductsState>({
products: [],
loading: {
fetch: true,
delete: false,
},
error: null,
selectedProducts: [],
pagination: {
page: 1,
pageSize: 10,
total: 0,
},
filters: {
search: '',
category: '',
collection: '',
active: null,
},
});
// Получаем параметры из URL при первой загрузке
useEffect(() => {
const page = searchParams.get('page') ? parseInt(searchParams.get('page') as string, 10) : 1;
const search = searchParams.get('search') || '';
const category = searchParams.get('category') || '';
const collection = searchParams.get('collection') || '';
const active = searchParams.get('active')
? searchParams.get('active') === 'true'
: null;
setState(prev => ({
...prev,
pagination: {
...prev.pagination,
page,
},
filters: {
search,
category,
collection,
active,
},
}));
}, [searchParams]);
// Формируем параметры запроса
const queryParams = {
skip: (state.pagination.page - 1) * state.pagination.pageSize,
limit: state.pagination.pageSize,
search: state.filters.search,
category_id: state.filters.category || undefined,
collection_id: state.filters.collection || undefined,
is_active: state.filters.active,
};
const { data, isLoading, error, refetch } = useProductsApi(params) as { data?: { products: Product[]; total: number }, isLoading: boolean, error: any, refetch: () => void };
const products: Product[] = data?.products || [];
const total: number = data?.total || 0;
// Запрос на получение продуктов
const { data, isLoading, error, refetch } = useAdminQuery<{
products: Product[];
total: number;
}>(
[cacheKeys.products, JSON.stringify(queryParams)],
'/catalog/products',
queryParams,
{
onSuccess: (data) => {
if (data) {
setState(prev => ({
...prev,
products: data.products || [],
pagination: {
...prev.pagination,
total: data.total || 0,
},
loading: {
...prev.loading,
fetch: false,
},
}));
}
},
onError: (err) => {
setState(prev => ({
...prev,
error: err as Error,
loading: {
...prev.loading,
fetch: false,
},
}));
},
}
);
// Удаление продуктов (заглушка, реальный API — через useAdminMutation)
// Мутация для удаления продуктов
const deleteMutation = useAdminMutation<any, Error, number[]>(
'delete',
(ids) => `/catalog/products/${ids.join(',')}`,
{
onSuccessMessage: 'Продукты успешно удалены',
onErrorMessage: 'Не удалось удалить продукты',
invalidateQueries: [cacheKeys.products],
mutationOptions: {
onMutate: () => {
setState(prev => ({
...prev,
loading: {
...prev.loading,
delete: true,
},
}));
},
onSettled: () => {
setState(prev => ({
...prev,
loading: {
...prev.loading,
delete: false,
},
selectedProducts: [],
}));
},
},
}
);
// Обработчик удаления продуктов
const deleteProducts = useCallback(async (ids: number[]) => {
// Здесь должен быть вызов useAdminMutation, но для совместимости оставим заглушку
setSelectedProducts((prev) => prev.filter(id => !ids.includes(id)));
// Можно добавить toast.success('Удалено')
if (ids.length === 0) return;
try {
await deleteMutation.mutateAsync(ids);
} catch (error) {
console.error('Error deleting products:', error);
}
}, [deleteMutation]);
// Обработчик изменения фильтров
const setFilter = useCallback((key: keyof ProductsState['filters'], value: any) => {
setState(prev => ({
...prev,
filters: {
...prev.filters,
[key]: value,
},
pagination: {
...prev.pagination,
page: 1, // Сбрасываем страницу при изменении фильтров
},
}));
// Обновляем URL с новыми параметрами
const params = new URLSearchParams(searchParams.toString());
params.set('page', '1');
if (value !== null && value !== '') {
params.set(key, value.toString());
} else {
params.delete(key);
}
// Обновляем URL
router.push(`/admin/products?${params.toString()}`);
}, [router, searchParams]);
// Обработчик изменения страницы
const setPage = useCallback((page: number) => {
setState(prev => ({
...prev,
pagination: {
...prev.pagination,
page,
},
}));
// Обновляем URL с новой страницей
const params = new URLSearchParams(searchParams.toString());
params.set('page', page.toString());
router.push(`/admin/products?${params.toString()}`);
}, [router, searchParams]);
// Обработчик изменения размера страницы
const setPageSize = useCallback((pageSize: number) => {
setState(prev => ({
...prev,
pagination: {
...prev.pagination,
pageSize,
page: 1, // Сбрасываем страницу при изменении размера
},
}));
// Обновляем URL с новым размером страницы
const params = new URLSearchParams(searchParams.toString());
params.set('page', '1');
params.set('pageSize', pageSize.toString());
router.push(`/admin/products?${params.toString()}`);
}, [router, searchParams]);
// Обработчик выбора продуктов
const selectProducts = useCallback((selectedIds: number[]) => {
setState(prev => ({
...prev,
selectedProducts: selectedIds,
}));
}, []);
// Управление фильтрами
const setFilter = (key: keyof Filters, value: any) => {
setFilters(prev => ({ ...prev, [key]: value }));
setPage(1);
};
// Обновляем состояние при изменении данных
useEffect(() => {
if (data) {
setState(prev => ({
...prev,
products: data.products || [],
pagination: {
...prev.pagination,
total: data.total || 0,
},
loading: {
...prev.loading,
fetch: false,
},
}));
}
}, [data]);
// Управление выбором
const selectProducts = (ids: number[]) => setSelectedProducts(ids);
// Обновляем состояние при ошибке
useEffect(() => {
if (error) {
setState(prev => ({
...prev,
error: error as Error,
loading: {
...prev.loading,
fetch: false,
},
}));
}
}, [error]);
return {
products,
loading: isLoading,
error,
selectedProducts,
pagination: { page, pageSize, total },
filters,
products: state.products,
loading: {
fetch: isLoading || state.loading.fetch,
delete: state.loading.delete
},
error: state.error,
selectedProducts: state.selectedProducts,
pagination: state.pagination,
filters: state.filters,
deleteProducts,
setFilter,
setPage,
@ -56,4 +286,4 @@ export function useProducts() {
selectProducts,
refetch,
};
}
}

View File

@ -23,7 +23,7 @@ API для работы с аутентификацией пользовател
### Методы
- **login(credentials: LoginCredentials): Promise<TokenResponse>**
- **login(credentials: LoginCredentials): Promise<TokenResponse>**
Авторизация пользователя.
```typescript
// Пример
@ -33,28 +33,28 @@ API для работы с аутентификацией пользовател
});
```
- **register(data: RegisterData): Promise<any>**
- **register(data: RegisterData): Promise<any>**
Регистрация нового пользователя.
- **logout(): void**
- **logout(): void**
Выход из системы (удаление токена).
- **isAuthenticated(): boolean**
- **isAuthenticated(): boolean**
Проверка авторизации пользователя.
- **getToken(): string | null**
- **getToken(): string | null**
Получение текущего токена авторизации.
- **resetPassword(email: string): Promise<any>**
- **resetPassword(email: string): Promise<any>**
Запрос на сброс пароля.
- **setNewPassword(token: string, password: string): Promise<any>**
- **setNewPassword(token: string, password: string): Promise<any>**
Установка нового пароля после сброса.
- **changePassword(currentPassword: string, newPassword: string): Promise<any>**
- **changePassword(currentPassword: string, newPassword: string): Promise<any>**
Изменение пароля авторизованным пользователем.
- **getProfile(): Promise<UserProfile | null>**
- **getProfile(): Promise<UserProfile | null>**
Получение профиля текущего пользователя.
## Аутентификация API (auth-api.ts)
@ -63,7 +63,7 @@ API для работы с аутентификацией пользовател
### Константы
- **TOKEN_KEY = 'token'**
- **TOKEN_KEY = 'token'**
Константа для ключа токена в localStorage.
### Интерфейсы
@ -88,7 +88,7 @@ interface User {
### Методы
- **login(email: string, password: string): Promise<ApiResponse<any>>**
- **login(email: string, password: string): Promise<ApiResponse<any>>**
Авторизация пользователя через API.
```typescript
// Пример использования
@ -97,16 +97,16 @@ interface User {
console.log('Авторизация успешна');
}
```
Метод выполняет следующие действия:
- Создает FormData для отправки данных в формате x-www-form-urlencoded
- Отправляет POST-запрос на `/auth/login`
- Сохраняет полученный токен в localStorage по ключу TOKEN_KEY
- Возвращает объект с результатом операции
- **getProfile(): Promise<User | null>**
- **getProfile(): Promise<User | null>**
Получение профиля текущего пользователя из API.
Метод выполняет следующие действия:
- Получает токен из localStorage
- Отправляет GET-запрос на `/users/me` с токеном в заголовке Authorization
@ -114,14 +114,14 @@ interface User {
- Проверяет наличие поля is_admin или устанавливает его на основе роли 'admin'
- Возвращает объект пользователя или null в случае ошибки
- **logout(): void**
- **logout(): void**
Выход из системы.
Метод удаляет токен из localStorage.
- **isAuthenticated(): boolean**
- **isAuthenticated(): boolean**
Проверка авторизации пользователя.
Метод проверяет наличие токена в localStorage.
### Особенности реализации
@ -137,31 +137,31 @@ API для работы с пользователями.
### Методы
- **getCurrentUser(): Promise<User>**
- **getCurrentUser(): Promise<User>**
Получение профиля текущего пользователя.
- **updateCurrentUser(userData: UserUpdate): Promise<User>**
- **updateCurrentUser(userData: UserUpdate): Promise<User>**
Обновление профиля текущего пользователя.
- **addAddress(address: AddressCreate): Promise<Address>**
- **addAddress(address: AddressCreate): Promise<Address>**
Добавление нового адреса для текущего пользователя.
- **updateAddress(addressId: number, address: AddressUpdate): Promise<Address>**
- **updateAddress(addressId: number, address: AddressUpdate): Promise<Address>**
Обновление адреса.
- **deleteAddress(addressId: number): Promise<void>**
- **deleteAddress(addressId: number): Promise<void>**
Удаление адреса.
- **getUserById(userId: number): Promise<User>**
- **getUserById(userId: number): Promise<User>**
Получение информации о пользователе по ID (только для админов).
- **getUsers(params?: { skip?: number; limit?: number; search?: string; }): Promise<{ users: User[]; total: number; }>**
- **getUsers(params?: { skip?: number; limit?: number; search?: string; }): Promise<{ users: User[]; total: number; }>**
Получение списка всех пользователей (только для админов).
- **updateUserRole(userId: number, role: string): Promise<User | null>**
- **updateUserRole(userId: number, role: string): Promise<User | null>**
Изменение роли пользователя (только для админов).
- **toggleUserActive(userId: number, active: boolean): Promise<User | null>**
- **toggleUserActive(userId: number, active: boolean): Promise<User | null>**
Блокировка/разблокировка пользователя (только для админов).
## Каталог (catalog.ts)
@ -170,49 +170,49 @@ API для работы с каталогом товаров.
### Методы
- **getProducts(params?): Promise<ProductsResponse>**
- **getProducts(params?): Promise<ProductsResponse>**
Получение списка продуктов с параметрами фильтрации и пагинации.
- **getProductById(productId: number): Promise<ProductDetails | null>**
- **getProductById(productId: number): Promise<ProductDetails | null>**
Получение детальной информации о продукте по ID.
- **getProductBySlug(slug: string): Promise<ProductDetails | null>**
- **getProductBySlug(slug: string): Promise<ProductDetails | null>**
Получение продукта по slug.
- **createProduct(productData: ProductCreate): Promise<Product | null>**
- **createProduct(productData: ProductCreate): Promise<Product | null>**
Создание нового продукта (только для админов).
- **updateProduct(productId: number, productData: ProductUpdate): Promise<Product | null>**
- **updateProduct(productId: number, productData: ProductUpdate): Promise<Product | null>**
Обновление продукта (только для админов).
- **deleteProduct(productId: number): Promise<boolean>**
- **deleteProduct(productId: number): Promise<boolean>**
Удаление продукта (только для админов).
- **addProductVariant(productId: number, variantData: ProductVariantCreate): Promise<ProductVariant | null>**
- **addProductVariant(productId: number, variantData: ProductVariantCreate): Promise<ProductVariant | null>**
Добавление варианта продукта (только для админов).
- **updateProductVariant(variantId: number, variantData: ProductVariantUpdate): Promise<ProductVariant | null>**
- **updateProductVariant(variantId: number, variantData: ProductVariantUpdate): Promise<ProductVariant | null>**
Обновление варианта продукта (только для админов).
- **deleteProductVariant(variantId: number): Promise<boolean>**
- **deleteProductVariant(variantId: number): Promise<boolean>**
Удаление варианта продукта (только для админов).
- **uploadProductImage(productId: number, file: File, isPrimary?: boolean): Promise<ProductImage | null>**
- **uploadProductImage(productId: number, file: File, isPrimary?: boolean): Promise<ProductImage | null>**
Загрузка изображения продукта (только для админов).
- **updateProductImage(imageId: number, imageData: ProductImageUpdate): Promise<ProductImage | null>**
- **updateProductImage(imageId: number, imageData: ProductImageUpdate): Promise<ProductImage | null>**
Обновление изображения продукта (только для админов).
- **deleteProductImage(imageId: number): Promise<boolean>**
- **deleteProductImage(imageId: number): Promise<boolean>**
Удаление изображения продукта (только для админов).
- **getSizes(params?): Promise<Size[]>**
- **getSizes(params?): Promise<Size[]>**
Получение списка размеров.
- **getSizeById(sizeId: number): Promise<Size | null>**
- **getSizeById(sizeId: number): Promise<Size | null>**
Получение размера по ID.
- **createSize(sizeData: SizeCreate): Promise<Size | null>**
- **createSize(sizeData: SizeCreate): Promise<Size | null>**
Создание нового размера (только для админов).
```typescript
// Пример использования
@ -226,7 +226,7 @@ API для работы с каталогом товаров.
}
```
- **updateSize(sizeId: number, sizeData: SizeUpdate): Promise<Size | null>**
- **updateSize(sizeId: number, sizeData: SizeUpdate): Promise<Size | null>**
Обновление размера (только для админов).
```typescript
// Пример использования
@ -239,7 +239,7 @@ API для работы с каталогом товаров.
}
```
- **deleteSize(sizeId: number): Promise<boolean>**
- **deleteSize(sizeId: number): Promise<boolean>**
Удаление размера (только для админов).
```typescript
// Пример использования
@ -248,10 +248,10 @@ API для работы с каталогом товаров.
}
```
- **getCategoriesTree(): Promise<Category[]>**
- **getCategoriesTree(): Promise<Category[]>**
Получение дерева категорий.
- **getCollections(params?): Promise<CollectionsResponse>**
- **getCollections(params?): Promise<CollectionsResponse>**
Получение списка коллекций с пагинацией.
```typescript
// Пример использования
@ -260,7 +260,7 @@ API для работы с каталогом товаров.
const { collections, total } = response;
```
- **createCollection(data: CollectionCreate): Promise<Collection | null>**
- **createCollection(data: CollectionCreate): Promise<Collection | null>**
Создание новой коллекции (только для админов).
```typescript
// Пример использования
@ -275,7 +275,7 @@ API для работы с каталогом товаров.
}
```
- **updateCollection(id: number, data: CollectionUpdate): Promise<Collection | null>**
- **updateCollection(id: number, data: CollectionUpdate): Promise<Collection | null>**
Обновление коллекции (только для админов).
```typescript
// Пример использования
@ -288,7 +288,7 @@ API для работы с каталогом товаров.
}
```
- **deleteCollection(id: number): Promise<boolean>**
- **deleteCollection(id: number): Promise<boolean>**
Удаление коллекции (только для админов).
```typescript
// Пример использования
@ -297,41 +297,41 @@ API для работы с каталогом товаров.
}
```
- **getProductsReport(params?: { start_date?: string; end_date?: string; limit?: number; sort_by?: 'sales' | 'revenue' | 'profit'; order?: 'asc' | 'desc'; }): Promise<ProductReport[] | null>**
- **getProductsReport(params?: { start_date?: string; end_date?: string; limit?: number; sort_by?: 'sales' | 'revenue' | 'profit'; order?: 'asc' | 'desc'; }): Promise<ProductReport[] | null>**
Получение отчета по товарам (только для админов).
- **createProductComplete(data: ProductCreateComplete): Promise<ProductDetails | null>**
- **createProductComplete(data: ProductCreateComplete): Promise<ProductDetails | null>**
Создание товара с вариантами и изображениями в одном запросе (только для админов).
- **updateProductComplete(productId: number, data: ProductUpdateComplete): Promise<ProductDetails | null>**
- **updateProductComplete(productId: number, data: ProductUpdateComplete): Promise<ProductDetails | null>**
Обновление товара с вариантами и изображениями в одном запросе (только для админов).
### Вспомогательные функции
- **normalizeProductImage(imageUrl: string | null | undefined): string**
- **normalizeProductImage(imageUrl: string | null | undefined): string**
Нормализует URL изображения продукта.
- **processProductImages(product: ProductDetails): ProductDetails**
- **processProductImages(product: ProductDetails): ProductDetails**
Обрабатывает все изображения продукта.
- **getImageUrl(path: string | null | undefined): string**
- **getImageUrl(path: string | null | undefined): string**
Получает нормализованный URL изображения.
### Сервис для работы с категориями (categoryService)
- **getCategories(): Promise<Category[]>**
- **getCategories(): Promise<Category[]>**
Получение списка всех категорий в виде дерева.
- **getCategoryById(categoryId: number): Promise<Category | null>**
- **getCategoryById(categoryId: number): Promise<Category | null>**
Получение категории по ID.
- **createCategory(data: CategoryCreate): Promise<Category | null>**
- **createCategory(data: CategoryCreate): Promise<Category | null>**
Создание новой категории.
- **updateCategory(categoryId: number, data: CategoryUpdate): Promise<Category | null>**
- **updateCategory(categoryId: number, data: CategoryUpdate): Promise<Category | null>**
Обновление категории.
- **deleteCategory(categoryId: number): Promise<boolean>**
- **deleteCategory(categoryId: number): Promise<boolean>**
Удаление категории.
### Интерфейсы
@ -654,22 +654,22 @@ API для работы с корзиной покупок.
### Методы
- **getCart(): Promise<Cart>**
- **getCart(): Promise<Cart>**
Получение текущей корзины пользователя.
- **addToCart(item: CartItemCreate): Promise<CartItem | null>**
- **addToCart(item: CartItemCreate): Promise<CartItem | null>**
Добавление товара в корзину.
- **updateCartItem(itemId: number, data: CartItemUpdate): Promise<CartItem | null>**
- **updateCartItem(itemId: number, data: CartItemUpdate): Promise<CartItem | null>**
Обновление количества товара в корзине.
- **removeFromCart(itemId: number): Promise<boolean>**
- **removeFromCart(itemId: number): Promise<boolean>**
Удаление товара из корзины.
- **clearCart(): Promise<boolean>**
- **clearCart(): Promise<boolean>**
Очистка корзины.
- **validateCart(): Promise<{ valid: boolean; items?: { id: number; available: boolean; stock?: number; }[]; message?: string; }>**
- **validateCart(): Promise<{ valid: boolean; items?: { id: number; available: boolean; stock?: number; }[]; message?: string; }>**
Проверка доступности товаров в корзине.
## Заказы (orders.ts)
@ -678,43 +678,95 @@ API для работы с заказами и адресами.
### Методы
- **getUserOrders(params?): Promise<Order[]>**
- **getUserOrders(params?): Promise<Order[]>**
Получение заказов пользователя.
- **getOrderById(orderId: number): Promise<Order | null>**
- **getOrderById(orderId: number): Promise<Order | null>**
Получение заказа по ID.
- **createOrder(orderData: OrderCreate): Promise<Order | null>**
Создание нового заказа.
- **createOrderNew(orderData: OrderCreateNew): Promise<OrderResponse | null>**
Создание нового заказа с использованием нового формата данных.
```typescript
// Пример использования
const orderData = {
user_info: {
first_name: "Иван",
last_name: "Иванов",
email: "ivan@example.com",
phone: "+7 (999) 123-45-67"
},
delivery: {
method: "cdek",
address: {
city: "Москва",
street: "Примерная",
house: "1",
apartment: "123",
formatted_address: "Москва, ул. Примерная, д. 1, кв. 123"
},
cdek_info: {
pvz: {
code: "MSK123",
city: "Москва",
address: "ул. Примерная, д. 1"
},
tariff: {
tariff_code: 136,
tariff_name: "Посылка склад-склад",
delivery_sum: 300
},
delivery_type: "office"
}
},
items: [
{
product_id: 456,
variant_id: 123,
quantity: 1,
price: 5990.0
}
],
payment_method: "card",
comment: "Позвоните перед доставкой"
};
- **updateOrder(orderId: number, orderData: OrderUpdate): Promise<Order | null>**
const result = await ordersService.createOrderNew(orderData);
if (result) {
console.log(`Заказ успешно создан, ID: ${result.order_id}`);
}
```
- **createOrder(orderData: OrderCreate): Promise<Order | null>**
Создание нового заказа (старый формат).
- **updateOrder(orderId: number, orderData: OrderUpdate): Promise<Order | null>**
Обновление заказа.
- **cancelOrder(orderId: number): Promise<boolean>**
- **cancelOrder(orderId: number): Promise<boolean>**
Отмена заказа.
- **getUserAddresses(): Promise<Address[]>**
- **getUserAddresses(): Promise<Address[]>**
Получение адресов пользователя.
- **getAddressById(addressId: number): Promise<Address | null>**
- **getAddressById(addressId: number): Promise<Address | null>**
Получение адреса по ID.
- **createAddress(addressData: AddressCreate): Promise<Address | null>**
- **createAddress(addressData: AddressCreate): Promise<Address | null>**
Создание нового адреса.
- **updateAddress(addressId: number, addressData: AddressUpdate): Promise<Address | null>**
- **updateAddress(addressId: number, addressData: AddressUpdate): Promise<Address | null>**
Обновление адреса.
- **deleteAddress(addressId: number): Promise<boolean>**
- **deleteAddress(addressId: number): Promise<boolean>**
Удаление адреса.
- **setDefaultAddress(addressId: number): Promise<Address | null>**
- **setDefaultAddress(addressId: number): Promise<Address | null>**
Установка адреса по умолчанию.
- **getAllOrders(params?: { skip?: number; limit?: number; status?: string; date_from?: string; date_to?: string; search?: string; }): Promise<Order[] | null>**
- **getAllOrders(params?: { skip?: number; limit?: number; status?: string; date_from?: string; date_to?: string; search?: string; }): Promise<Order[] | null>**
Получение списка всех заказов (только для админов).
- **getSalesReport(params: { period: 'daily' | 'weekly' | 'monthly' | 'yearly'; start_date?: string; end_date?: string; }): Promise<SalesReport | null>**
- **getSalesReport(params: { period: 'daily' | 'weekly' | 'monthly' | 'yearly'; start_date?: string; end_date?: string; }): Promise<SalesReport | null>**
Получение отчета по продажам (только для админов).
## Отзывы (reviews.ts)
@ -723,19 +775,19 @@ API для работы с отзывами о товарах.
### Методы
- **getProductReviews(productId: number, params?): Promise<Review[]>**
- **getProductReviews(productId: number, params?): Promise<Review[]>**
Получение отзывов для конкретного продукта.
- **createReview(reviewData: ReviewCreate): Promise<Review | null>**
- **createReview(reviewData: ReviewCreate): Promise<Review | null>**
Создание нового отзыва.
- **updateReview(reviewId: number, reviewData: ReviewUpdate): Promise<Review | null>**
- **updateReview(reviewId: number, reviewData: ReviewUpdate): Promise<Review | null>**
Обновление отзыва (только для владельца или админа).
- **deleteReview(reviewId: number): Promise<boolean>**
- **deleteReview(reviewId: number): Promise<boolean>**
Удаление отзыва (только для владельца или админа).
- **approveReview(reviewId: number): Promise<Review | null>**
- **approveReview(reviewId: number): Promise<Review | null>**
Одобрение отзыва (только для админов).
## Список желаний (wishlist.ts)
@ -744,19 +796,19 @@ API для работы со списком желаемых товаров.
### Методы
- **getWishlist(): Promise<Wishlist>**
- **getWishlist(): Promise<Wishlist>**
Получение списка желаний пользователя.
- **addToWishlist(productId: number): Promise<WishlistItem | null>**
- **addToWishlist(productId: number): Promise<WishlistItem | null>**
Добавление товара в список желаний.
- **isInWishlist(productId: number): Promise<boolean>**
- **isInWishlist(productId: number): Promise<boolean>**
Проверка наличия товара в списке желаний.
- **removeFromWishlist(itemId: number): Promise<boolean>**
- **removeFromWishlist(itemId: number): Promise<boolean>**
Удаление товара из списка желаний.
- **clearWishlist(): Promise<boolean>**
- **clearWishlist(): Promise<boolean>**
Очистка списка желаний.
## Контент (content.ts)
@ -765,22 +817,22 @@ API для работы с контентом сайта.
### Методы
- **getPages(params?): Promise<Page[]>**
- **getPages(params?): Promise<Page[]>**
Получение списка страниц.
- **getPageById(pageId: number): Promise<Page | null>**
- **getPageById(pageId: number): Promise<Page | null>**
Получение страницы по ID.
- **getPageBySlug(slug: string): Promise<Page | null>**
- **getPageBySlug(slug: string): Promise<Page | null>**
Получение страницы по slug.
- **createPage(pageData: PageCreate): Promise<Page | null>**
- **createPage(pageData: PageCreate): Promise<Page | null>**
Создание новой страницы (только для админов).
- **updatePage(pageId: number, pageData: PageUpdate): Promise<Page | null>**
- **updatePage(pageId: number, pageData: PageUpdate): Promise<Page | null>**
Обновление страницы (только для админов).
- **deletePage(pageId: number): Promise<boolean>**
- **deletePage(pageId: number): Promise<boolean>**
Удаление страницы (только для админов).
## Настройки (settings.ts)
@ -789,31 +841,31 @@ API для работы с настройками магазина.
### Методы
- **getStoreSettings(): Promise<StoreSetting | null>**
- **getStoreSettings(): Promise<StoreSetting | null>**
Получение настроек магазина.
- **updateStoreSettings(settings: Partial<StoreSetting>): Promise<StoreSetting | null>**
- **updateStoreSettings(settings: Partial<StoreSetting>): Promise<StoreSetting | null>**
Обновление настроек магазина (только для админов).
- **getSettingsByGroup(group: string): Promise<SiteSetting[] | null>**
- **getSettingsByGroup(group: string): Promise<SiteSetting[] | null>**
Получение всех настроек по группе.
- **getSettingByKey(key: string): Promise<SiteSetting | null>**
- **getSettingByKey(key: string): Promise<SiteSetting | null>**
Получение настройки по ключу.
- **updateSetting(key: string, value: any): Promise<SiteSetting | null>**
- **updateSetting(key: string, value: any): Promise<SiteSetting | null>**
Обновление настройки (только для админов).
- **getLocale(lang: string): Promise<LocaleData | null>**
- **getLocale(lang: string): Promise<LocaleData | null>**
Получение данных локализации для указанного языка.
- **getAvailableLanguages(): Promise<string[] | null>**
- **getAvailableLanguages(): Promise<string[] | null>**
Получение списка доступных языков.
- **formatPrice(price: number, settings?: StoreSetting): string**
- **formatPrice(price: number, settings?: StoreSetting): string**
Форматирование цены с использованием правильной валюты.
- **getDashboardStats(): Promise<DashboardStats | null>**
- **getDashboardStats(): Promise<DashboardStats | null>**
Получение статистики для панели управления (только для админов).
## Аналитика (analytics.ts)
@ -822,28 +874,28 @@ API для работы с аналитикой.
### Методы
- **logEvent(event: AnalyticsLogCreate): Promise<AnalyticsLog | null>**
- **logEvent(event: AnalyticsLogCreate): Promise<AnalyticsLog | null>**
Логирование события аналитики.
- **getLogs(params?): Promise<AnalyticsLog[]>**
- **getLogs(params?): Promise<AnalyticsLog[]>**
Получение логов аналитики (только для админов).
- **getReport(reportType: string, params?): Promise<any>**
- **getReport(reportType: string, params?): Promise<any>**
Получение отчета по аналитике (только для админов).
- **initTracking(): string**
- **initTracking(): string**
Инициализация отслеживания сессии.
- **trackPageView(path: string): Promise<void>**
- **trackPageView(path: string): Promise<void>**
Отслеживание просмотра страницы.
- **trackProductView(productId: number): Promise<void>**
- **trackProductView(productId: number): Promise<void>**
Отслеживание просмотра товара.
- **trackAddToCart(productId: number, additionalData?: Record<string, any>): Promise<void>**
- **trackAddToCart(productId: number, additionalData?: Record<string, any>): Promise<void>**
Отслеживание добавления товара в корзину.
- **getUsersReport(params?: { start_date?: string; end_date?: string; limit?: number; sort_by?: 'orders' | 'total_spent' | 'average_order_value'; order?: 'asc' | 'desc'; }): Promise<UserReport[] | null>**
- **getUsersReport(params?: { start_date?: string; end_date?: string; limit?: number; sort_by?: 'orders' | 'total_spent' | 'average_order_value'; order?: 'asc' | 'desc'; }): Promise<UserReport[] | null>**
Получение отчета по пользователям (только для админов).
## API Клиент (api.ts)
@ -858,7 +910,7 @@ API для работы с аналитикой.
### Объекты
- **apiStatus**
- **apiStatus**
Объект для мониторинга состояния API.
```typescript
export const apiStatus = {
@ -866,7 +918,7 @@ API для работы с аналитикой.
connectionError: false,
lastConnectionError: null as Error | null,
isAuthenticated: false,
// Метод для проверки состояния соединения с API
checkConnection: async (): Promise<boolean> => { ... }
};
@ -874,10 +926,10 @@ API для работы с аналитикой.
### Вспомогательные функции
- **getToken(): string | null**
- **getToken(): string | null**
Получение токена из localStorage.
- **fetchApi<T>(url: string, options: RequestInit = {}): Promise<ApiResponse<T>>**
- **fetchApi<T>(url: string, options: RequestInit = {}): Promise<ApiResponse<T>>**
Функция для выполнения запросов к API с унифицированным обработчиком ошибок.
```typescript
// Пример использования
@ -894,24 +946,24 @@ API для работы с аналитикой.
#### Перехватчики запросов
- **request**
- **request**
Добавляет токен авторизации в заголовки запроса, если токен есть в localStorage.
- **response**
- **response**
Обрабатывает ответы от сервера. Для ошибок 401 (Unauthorized) логирует проблему, но больше не удаляет токен автоматически, чтобы избежать проблем при перезагрузке страницы.
#### Методы API клиента
- **get<T>(url: string, params = {}): Promise<T>**
- **get<T>(url: string, params = {}): Promise<T>**
Выполняет GET-запрос к API.
- **post<T>(url: string, data = {}, config = {}): Promise<T>**
- **post<T>(url: string, data = {}, config = {}): Promise<T>**
Выполняет POST-запрос к API.
- **put<T>(url: string, data = {}, config = {}): Promise<T>**
- **put<T>(url: string, data = {}, config = {}): Promise<T>**
Выполняет PUT-запрос к API.
- **delete<T>(url: string, config = {}): Promise<T>**
- **delete<T>(url: string, config = {}): Promise<T>**
Выполняет DELETE-запрос к API.
### Интерфейсы
@ -932,4 +984,4 @@ export interface ApiResponse<T = any> {
- Логирование ошибок 401, но без автоматического удаления токена
- Отладочный режим для логирования запросов и ответов
- Проверка состояния соединения с API
- Единый интерфейс для обработки ответов
- Единый интерфейс для обработки ответов

View File

@ -1,27 +1,47 @@
import axios from 'axios';
// Получаем URL API из переменных окружения или используем значение по умолчанию
export const PUBLIC_API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8000/api';
export const PUBLIC_BASE_URL = process.env.NEXT_PUBLIC_BASE_URL || 'http://localhost:8000';
// Для клиентских запросов (из браузера)
export const PUBLIC_API_URL = process.env.NEXT_PUBLIC_API_URL || '/api';
// Для серверных запросов (SSR)
export const SERVER_API_URL = process.env.NEXT_SERVER_API_URL || 'http://localhost/api';
// Базовый URL для статических ресурсов
export const PUBLIC_BASE_URL = process.env.NEXT_PUBLIC_BASE_URL || 'http://localhost';
// Определяем, выполняется ли код на сервере или в браузере
export const isServer = typeof window === 'undefined';
// Константа для ключа токена в localStorage
const TOKEN_KEY = 'token';
// Отладочный режим для API
export const apiStatus = {
debugMode: true, // Временно включаем отладочный режим для диагностики проблем
// debugMode: process.env.NEXT_PUBLIC_DEBUG === 'true',
// Используем переменную окружения для включения/выключения отладки
debugMode: process.env.NEXT_PUBLIC_DEBUG === 'true',
connectionError: false,
lastConnectionError: null as Error | null,
isAuthenticated: false, // Флаг аутентификации пользователя
isAuthenticated: false as boolean, // Флаг аутентификации пользователя (не const)
SUCCESS: 'success',
ERROR: 'error'
} as const;
};
// Для отладки выводим URL API в консоль
console.log('🔍 Режим отладки API включен');
console.error('🌐 Клиентский API URL:', PUBLIC_API_URL);
console.log('🖥️ Серверный API URL:', SERVER_API_URL);
console.log('📁 BASE URL:', PUBLIC_BASE_URL);
console.log('🔄 Среда выполнения:', isServer ? 'Сервер' : 'Браузер');
console.log('📋 ENV переменные:');
console.log(' - NEXT_PUBLIC_API_URL:', process.env.NEXT_PUBLIC_API_URL);
console.log(' - NEXT_SERVER_API_URL:', process.env.NEXT_SERVER_API_URL);
console.log(' - NEXT_PUBLIC_BASE_URL:', process.env.NEXT_PUBLIC_BASE_URL);
console.log(' - NEXT_PUBLIC_DEBUG:', process.env.NEXT_PUBLIC_DEBUG);
// Проверяем, не содержит ли URL порт 8000
if (PUBLIC_API_URL.includes(':8000')) {
console.warn('⚠️ Клиентский API URL содержит порт 8000, что может вызывать проблемы при работе через Nginx');
}
if (apiStatus.debugMode) {
console.log('API URL:', PUBLIC_API_URL);
console.log('BASE URL:', PUBLIC_BASE_URL);
}
// Получение токена из localStorage
@ -35,8 +55,9 @@ const getToken = (): string | null => {
};
// Создаем экземпляр клиента Axios с базовыми настройками
// Используем разные URL для клиента и сервера
const instance = axios.create({
baseURL: PUBLIC_API_URL,
baseURL: isServer ? SERVER_API_URL : PUBLIC_API_URL,
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
@ -44,6 +65,11 @@ const instance = axios.create({
timeout: 30000, // 30 секунд таймаут для запросов
});
// Логируем, какой URL используется
if (apiStatus.debugMode) {
console.log('🔌 Axios использует baseURL:', instance.defaults.baseURL);
}
// Добавляем перехватчик для исходящих запросов
instance.interceptors.request.use(
(config) => {
@ -198,7 +224,15 @@ export async function fetchApi<T>(
headers['Authorization'] = `Bearer ${token}`;
}
const response = await fetch(`${PUBLIC_API_URL}${url}`, {
// Выбираем правильный базовый URL в зависимости от среды выполнения
const baseUrl = isServer ? SERVER_API_URL : PUBLIC_API_URL;
// Логируем запрос в режиме отладки
if (apiStatus.debugMode) {
console.log(`🔄 fetchApi запрос: ${baseUrl}${url}`);
}
const response = await fetch(`${baseUrl}${url}`, {
...options,
headers,
});

View File

@ -217,11 +217,11 @@ export interface GetProductsParams {
export async function getProducts(params: GetProductsParams): Promise<ProductsResponse> {
try {
const response = await api.get<{success: boolean, products: Product[], total: number, skip: number, limit: number}>('/catalog/products', { params });
if (apiStatus.debugMode) {
console.log('Ответ API getProducts:', response);
}
if (response && typeof response === 'object' && 'success' in response && response.success && 'products' in response) {
return {
products: response.products,
@ -230,7 +230,7 @@ export async function getProducts(params: GetProductsParams): Promise<ProductsRe
current_page: Math.floor(response.skip / response.limit) + 1
};
}
// Запасной вариант для обратной совместимости
if (response && typeof response === 'object') {
if ('data' in response && typeof response.data === 'object') {
@ -244,7 +244,7 @@ export async function getProducts(params: GetProductsParams): Promise<ProductsRe
};
}
}
// Если response само является ProductsResponse
if ('products' in response && Array.isArray(response.products)) {
const typedResponse = response as any;
@ -256,7 +256,7 @@ export async function getProducts(params: GetProductsParams): Promise<ProductsRe
};
}
}
console.error('Неизвестный формат ответа API:', response);
return { products: [], total: 0 };
} catch (error) {
@ -268,11 +268,11 @@ export async function getProducts(params: GetProductsParams): Promise<ProductsRe
export async function getProduct(id: number): Promise<ProductDetails> {
try {
const response = await api.get<{success: boolean, product: ProductDetails}>(`/catalog/products/${id}`);
if (response && typeof response === 'object' && 'success' in response && response.success && 'product' in response) {
return response.product;
}
throw new Error('Неверный формат ответа API при получении продукта');
} catch (error) {
console.error(`Ошибка при получении продукта ${id}:`, error);
@ -283,11 +283,11 @@ export async function getProduct(id: number): Promise<ProductDetails> {
export async function createProduct(data: ProductCreateComplete): Promise<ProductDetails> {
try {
const response = await api.post<{success: boolean, product: ProductDetails}>('/catalog/products', data);
if (response && typeof response === 'object' && 'success' in response && response.success && 'product' in response) {
return response.product;
}
throw new Error('Неверный формат ответа API при создании продукта');
} catch (error) {
console.error('Ошибка при создании продукта:', error);
@ -298,11 +298,11 @@ export async function createProduct(data: ProductCreateComplete): Promise<Produc
export async function updateProduct(id: number, data: ProductUpdateComplete): Promise<ProductDetails> {
try {
const response = await api.put<{success: boolean, product: ProductDetails}>(`/catalog/products/${id}`, data);
if (response && typeof response === 'object' && 'success' in response && response.success && 'product' in response) {
return response.product;
}
throw new Error('Неверный формат ответа API при обновлении продукта');
} catch (error) {
console.error(`Ошибка при обновлении продукта ${id}:`, error);
@ -337,17 +337,17 @@ export async function uploadProductImage(file: File): Promise<string> {
try {
const formData = new FormData();
formData.append('file', file);
const response = await api.post<{success: boolean, url: string}>('/catalog/upload', formData, {
headers: {
'Content-Type': 'multipart/form-data'
}
});
if (response && typeof response === 'object' && 'success' in response && response.success && 'url' in response) {
return response.url;
}
throw new Error('Неверный формат ответа API при загрузке изображения');
} catch (error) {
console.error('Ошибка при загрузке изображения:', error);
@ -361,51 +361,65 @@ export async function uploadProductImage(file: File): Promise<string> {
* @returns Полный URL изображения или плейсхолдер.
*/
export function normalizeProductImage(imageUrl: string | null | undefined): string {
const minioBaseUrl = process.env.NEXT_PUBLIC_MINIO_URL;
const placeholder = '/placeholder.jpg'; // Путь к вашему плейсхолдеру
if (!imageUrl) {
if (apiStatus.debugMode) {
// console.log('Ключ/URL изображения отсутствует, возвращаю плейсхолдер');
console.log('Ключ/URL изображения отсутствует, возвращаю плейсхолдер');
}
return placeholder;
}
// Если это уже полный URL (http, https, data URI, blob)
if (imageUrl.startsWith('http') || imageUrl.startsWith('data:') || imageUrl.startsWith('blob:')) {
// Обработка полного URL MinIO - преобразуем в относительный путь
if (imageUrl.includes('45.129.128.113:9000/dressedforsuccess/')) {
// Извлекаем только путь /dressedforsuccess/filename.jpg из полного URL
const match = imageUrl.match(/\/dressedforsuccess\/[^\/]+\.\w+$/);
if (match) {
const relativePath = match[0];
if (apiStatus.debugMode) {
console.log(`Преобразование полного URL MinIO в относительный путь: ${imageUrl} -> ${relativePath}`);
}
return relativePath;
}
}
// Если это data URI или blob, возвращаем как есть
if (imageUrl.startsWith('data:') || imageUrl.startsWith('blob:')) {
if (apiStatus.debugMode) {
// console.log(`Изображение уже содержит полный URL/Data URI/Blob: ${imageUrl}`);
console.log(`Изображение содержит Data URI/Blob: ${imageUrl}`);
}
return imageUrl;
}
// Если это старый путь /uploads (на всякий случай, если где-то остались)
if (imageUrl.startsWith('/uploads/')) {
// В идеале, этот блок нужно будет удалить после миграции всех данных
const oldUrl = `${PUBLIC_BASE_URL}${imageUrl}`;
if (apiStatus.debugMode) {
// console.warn(`Обнаружен старый формат URL /uploads/: ${imageUrl}. Формируется URL: ${oldUrl}`);
}
return oldUrl;
}
// Если это путь к MinIO, который начинается с /dressedforsuccess/
if (imageUrl.startsWith('/dressedforsuccess/') || imageUrl.includes('/dressedforsuccess/')) {
// Убедимся, что путь начинается с /
const cleanImageUrl = imageUrl.startsWith('/') ? imageUrl : `/${imageUrl}`;
// Если есть базовый URL MinIO и imageUrl не пустой - формируем полный URL
if (minioBaseUrl && typeof imageUrl === 'string' && imageUrl.trim() !== '') {
// Убираем возможный слэш в конце базового URL и в начале ключа
const cleanBaseUrl = minioBaseUrl.endsWith('/') ? minioBaseUrl.slice(0, -1) : minioBaseUrl;
const cleanImageUrl = imageUrl.startsWith('/') ? imageUrl.slice(1) : imageUrl;
const fullUrl = `${cleanBaseUrl}/${cleanImageUrl}`;
if (apiStatus.debugMode) {
// console.log(`Формирование URL MinIO: ${minioBaseUrl} + ${imageUrl} -> ${fullUrl}`);
console.log(`Использование относительного пути для MinIO: ${cleanImageUrl}`);
}
return fullUrl;
return cleanImageUrl;
}
// Если базовый URL MinIO не задан или ключ пустой, возвращаем плейсхолдер
if (apiStatus.debugMode) {
// console.warn(`Не удалось сформировать URL MinIO (базовый URL: ${minioBaseUrl}, ключ: ${imageUrl}). Возвращаю плейсхолдер.`);
// Если это уже полный URL (http, https), но не MinIO
if (imageUrl.startsWith('http')) {
if (apiStatus.debugMode) {
console.log(`Изображение содержит полный URL (не MinIO): ${imageUrl}`);
}
return imageUrl;
}
return placeholder;
return 'http://45.129.128.113:9000/dressedforsuccess/' + imageUrl;
// Для всех остальных случаев, просто возвращаем путь как есть
if (apiStatus.debugMode) {
console.log(`Использование пути изображения без изменений: ${imageUrl}`);
}
return imageUrl;
}
/**
@ -989,14 +1003,14 @@ const catalogService = {
if (response.success === false) {
throw new Error(response.error || 'Ошибка при обновлении товара');
}
if (!response.product) {
throw new Error('Сервер не вернул данные об обновленном товаре');
}
return processProductImages(response.product);
}
// Формат прямого ответа ProductDetails
if ('id' in response && 'name' in response) {
return processProductImages(response as ProductDetails);
@ -1146,11 +1160,11 @@ export const collectionService = {
getCollections: async (): Promise<Collection[]> => {
try {
const response = await api.get<{success: boolean, collections: Collection[], total: number}>('/catalog/collections');
if (response && typeof response === 'object' && 'success' in response && response.success && 'collections' in response) {
return response.collections;
}
console.error('Неизвестный формат ответа API для коллекций:', response);
return [];
} catch (error) {
@ -1163,11 +1177,11 @@ export const collectionService = {
getCollectionById: async (id: number): Promise<Collection | null> => {
try {
const response = await api.get<{success: boolean, collection: Collection}>(`/catalog/collections/${id}`);
if (response && typeof response === 'object' && 'success' in response && response.success && 'collection' in response) {
return response.collection;
}
console.error('Неизвестный формат ответа API для коллекции:', response);
return null;
} catch (error) {
@ -1180,11 +1194,11 @@ export const collectionService = {
createCollection: async (data: CollectionCreate): Promise<Collection | null> => {
try {
const response = await api.post<{success: boolean, collection: Collection}>('/catalog/collections', data);
if (response && typeof response === 'object' && 'success' in response && response.success && 'collection' in response) {
return response.collection;
}
console.error('Неизвестный формат ответа API при создании коллекции:', response);
return null;
} catch (error) {
@ -1197,11 +1211,11 @@ export const collectionService = {
updateCollection: async (id: number, data: CollectionUpdate): Promise<Collection | null> => {
try {
const response = await api.put<{success: boolean, collection: Collection}>(`/catalog/collections/${id}`, data);
if (response && typeof response === 'object' && 'success' in response && response.success && 'collection' in response) {
return response.collection;
}
console.error('Неизвестный формат ответа API при обновлении коллекции:', response);
return null;
} catch (error) {

View File

@ -14,7 +14,8 @@ const nextConfig = {
ignoreBuildErrors: true,
},
images: {
unoptimized: false,
unoptimized: true,
domains: ['45.129.128.113', 'localhost', '0.0.0.0', '127.0.0.1'],
remotePatterns: [
{
protocol: 'http',
@ -22,6 +23,21 @@ const nextConfig = {
port: '9000',
pathname: '/dressedforsuccess/**',
},
{
protocol: 'http',
hostname: 'localhost',
pathname: '/dressedforsuccess/**',
},
{
protocol: 'http',
hostname: '0.0.0.0',
pathname: '/dressedforsuccess/**',
},
{
protocol: 'http',
hostname: '127.0.0.1',
pathname: '/dressedforsuccess/**',
},
],
},
experimental: {

View File

@ -1,7 +1,8 @@
'use client';
import { ReactNode } from 'react';
import { ReactNode, useEffect } from 'react';
import { useQueryClient } from '@tanstack/react-query';
import { cacheKeys } from '@/lib/api-cache';
interface AdminQueryProviderProps {
children: ReactNode;
@ -10,11 +11,63 @@ interface AdminQueryProviderProps {
/**
* Провайдер для react-query в админке
* Оборачивает все компоненты админки и предоставляет доступ к QueryClient
* и оптимизирует работу с кэшем
*/
export function AdminQueryProvider({ children }: AdminQueryProviderProps) {
// Используем существующий QueryClient из корневого провайдера
const queryClient = useQueryClient();
// Предварительная загрузка часто используемых данных
useEffect(() => {
// Предзагрузка категорий
queryClient.prefetchQuery({
queryKey: [cacheKeys.categories],
queryFn: async () => {
try {
const response = await fetch('/api/catalog/categories');
if (!response.ok) throw new Error('Failed to fetch categories');
return await response.json();
} catch (error) {
console.error('Error prefetching categories:', error);
return [];
}
},
staleTime: 5 * 60 * 1000, // 5 минут
});
// Предзагрузка коллекций
queryClient.prefetchQuery({
queryKey: [cacheKeys.collections],
queryFn: async () => {
try {
const response = await fetch('/api/catalog/collections');
if (!response.ok) throw new Error('Failed to fetch collections');
return await response.json();
} catch (error) {
console.error('Error prefetching collections:', error);
return [];
}
},
staleTime: 5 * 60 * 1000, // 5 минут
});
// Предзагрузка размеров
queryClient.prefetchQuery({
queryKey: [cacheKeys.sizes],
queryFn: async () => {
try {
const response = await fetch('/api/catalog/sizes');
if (!response.ok) throw new Error('Failed to fetch sizes');
return await response.json();
} catch (error) {
console.error('Error prefetching sizes:', error);
return [];
}
},
staleTime: 5 * 60 * 1000, // 5 минут
});
}, [queryClient]);
return <>{children}</>;
}

Binary file not shown.

Binary file not shown.

BIN
frontend/public/images/home/.DS_Store vendored Normal file

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

View File

@ -0,0 +1,56 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Creator: CorelDRAW 2021.5 -->
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" width="101.681mm" height="52.7148mm" version="1.1" style="shape-rendering:geometricPrecision; text-rendering:geometricPrecision; image-rendering:optimizeQuality; fill-rule:evenodd; clip-rule:evenodd"
viewBox="0 0 9795.77 5078.45"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:xodm="http://www.corel.com/coreldraw/odm/2003">
<defs>
<style type="text/css">
<![CDATA[
.fil0 {fill:#2B2B2A;fill-rule:nonzero}
]]>
</style>
</defs>
<g id="Слой_x0020_1">
<metadata id="CorelCorpID_0Corel-Layer"/>
<g id="_105553243841888">
<path class="fil0" d="M4908.27 805.19c54.07,19.97 154.75,32.69 273.42,7.79 134.29,-28.17 227.42,-126.87 210.47,-234.42 -15.58,-98.88 -78.5,-144.79 -183.21,-184.87 -5,-103.57 -48.42,-197.68 -117.48,-266.17 -25.62,-25.61 -54.57,-47.33 -86.3,-65.14 -19.36,-12.43 -50.13,-24.51 -52.91,-25.62 30.07,-7.24 62.93,-11.37 101.34,-12.25 160.91,-3.71 233.25,103.02 241.68,154.81l67.38 1.11c-5.58,-28.4 -59.6,-180.41 -318.52,-180.41 -47.89,0 -94.65,8.35 -136.41,22.27 -33.41,-8.91 -66.83,-13.92 -102.48,-13.92 -101.33,0 -403.71,0.56 -403.71,0.56l0 17.25c35.1,0 57.37,8.36 71.83,20.61 26.18,21.17 26.18,55.13 26.18,74.61 0,145.76 0,437.26 0,583.01 0,22.83 0,67.94 -45.67,86.87 -12.8,5.57 -30.08,9.47 -52.34,9.47l0 17.81c0,0 245.13,0 396.09,0 61.7,0 110.63,-13.36 110.63,-13.36zm-333.56 -11.14l0 0 0 -760.64 180.43 0c66.83,0 127.42,11.35 204.92,62.92 94.34,62.78 161.59,156.48 172.05,270.63 16.65,181.55 -83.66,312.64 -175.39,368.06 23.38,10.03 47.31,17.26 62.92,21.17 96.9,-61.25 165.06,-170.19 183.75,-279.54 84.09,39.54 123.05,81.3 129.2,142.55 5,52.9 -21.16,95.78 -63.49,126.95 -57.91,43.44 -148.19,64.63 -225.51,56.8 -162.62,-16.47 -250.22,-113.71 -270.64,-191.55l-70.14 0c12.85,56.55 58.58,124.7 147.55,169.83 0,0 -38.54,12.8 -97.45,12.8 -44.55,0 -178.2,0 -178.2,0z"/>
<path class="fil0" d="M5019.09 410.39c23.38,7.24 44.54,13.92 64.58,20.6 1.12,-21.71 1.69,-51.78 -3.34,-80.74 -187.66,-59.58 -275.07,-87.98 -273.39,-189.32 0.55,-23.94 12.23,-47.33 34.5,-68.49 -13.34,-5.01 -27.84,-9.47 -41.18,-12.25 -42.5,35.61 -54.6,75.9 -54.57,107.47 0.11,118.63 90.75,168.16 273.41,222.73z"/>
</g>
<g id="_105553243834656">
<g>
<path class="fil0" d="M2921.05 1914.15c14.17,-25.91 21.24,-57.16 21.24,-93.77 0,-54.25 -15.78,-95.9 -47.37,-124.97 -31.56,-29.06 -76.18,-43.59 -133.82,-43.59l-123.88 0 0 342.24 138.46 0c32.71,0 61.58,-6.84 86.58,-20.54 25.03,-13.67 44.62,-33.47 58.8,-59.37zm-98.13 10.09l0 0c-14.98,9.62 -32.76,14.44 -53.33,14.44l-60.71 0 0 -231.48 50.76 0c35.47,0 62.75,9.68 81.85,29.03 19.12,19.35 28.68,47.4 28.68,84.16 0,24.29 -4.13,45.26 -12.4,62.91 -8.25,17.66 -19.87,31.3 -34.85,40.94z"/>
<path class="fil0" d="M3167.62 1864.11l84.03 0 79.44 129.96 80.64 0 -92.53 -143.8c23.15,-5.49 41.54,-16.96 55.14,-34.36 13.59,-17.4 20.39,-38.1 20.39,-62.06 0,-32.87 -11.09,-58.09 -33.28,-75.67 -22.19,-17.56 -53.68,-26.34 -94.49,-26.34l-171 0 0 342.24 71.67 0 0 -129.96zm0 -156.67l0 0 91.8 0c21.07,0 36.85,4.12 47.37,12.39 10.53,8.26 15.79,20.57 15.79,36.93 0,16.35 -5.17,29.06 -15.55,38.13 -10.36,9.08 -25.57,13.6 -45.66,13.6l-93.75 0 0 -101.05z"/>
<polygon class="fil0" points="3833.78,1938.68 3626.36,1938.68 3626.36,1848.56 3809.02,1848.56 3809.02,1793.18 3626.36,1793.18 3626.36,1707.2 3823.83,1707.2 3823.83,1651.83 3554.7,1651.83 3554.7,1994.07 3833.78,1994.07 "/>
<path class="fil0" d="M4239.57 1829.39c-4.06,-3.88 -8.85,-7.53 -14.45,-10.94 -5.59,-3.39 -11.96,-6.64 -19.07,-9.72 -7.13,-2.91 -16.61,-5.93 -28.43,-9.1 -11.81,-3.16 -25.91,-6.51 -42.25,-10.07 -28.18,-5.99 -46.57,-10.94 -55.14,-14.83 -9.08,-4.04 -15.92,-8.89 -20.52,-14.58 -4.61,-5.66 -6.93,-12.95 -6.93,-21.85 0,-13.6 5.42,-23.81 16.28,-30.62 10.84,-6.8 27.11,-10.19 48.81,-10.19 20.4,0 36.08,3.72 47,11.17 10.92,7.46 18.17,18.55 21.74,33.28l69.46 -9.47c-6.16,-30.62 -19.87,-52.56 -41.17,-65.84 -21.28,-13.27 -53.14,-19.9 -95.58,-19.9 -44.36,0 -78.17,8.21 -101.4,24.63 -23.25,16.44 -34.86,40.13 -34.86,71.06 0,18.13 3.4,33.27 10.2,45.42 6.8,12.14 16.26,21.85 28.42,29.15 5.5,3.24 11.17,6.19 16.99,8.85 5.83,2.68 13.6,5.42 23.31,8.27 9.72,2.84 23.18,6.27 40.34,10.32 14.9,3.08 27.37,5.87 37.4,8.38 10.04,2.51 17.65,4.82 22.84,6.93 10.52,4.22 18.62,9.48 24.29,15.79 5.66,6.32 8.51,14.34 8.51,24.05 0,15.22 -6.23,26.64 -18.72,34.25 -12.47,7.6 -31.24,11.42 -56.36,11.42 -24.12,0 -42.99,-4.08 -56.59,-12.26 -13.6,-8.17 -22.75,-21.34 -27.44,-39.47l-69.23 11.41c14.25,62.68 64.7,94.01 151.32,94.01 48.1,0 84.59,-8.87 109.44,-26.6 24.84,-17.73 37.26,-43.36 37.26,-76.87 0,-14.26 -2.18,-26.86 -6.54,-37.78 -4.38,-10.92 -10.68,-20.37 -18.95,-28.28z"/>
<path class="fil0" d="M4549.65 1998.92c48.1,0 84.59,-8.87 109.44,-26.6 24.84,-17.73 37.27,-43.36 37.27,-76.87 0,-14.26 -2.19,-26.86 -6.55,-37.78 -4.38,-10.92 -10.68,-20.37 -18.95,-28.28 -4.06,-3.88 -8.85,-7.53 -14.45,-10.94 -5.59,-3.39 -11.96,-6.64 -19.07,-9.72 -7.13,-2.91 -16.61,-5.93 -28.43,-9.1 -11.81,-3.16 -25.91,-6.51 -42.25,-10.07 -28.18,-5.99 -46.57,-10.94 -55.14,-14.83 -9.08,-4.04 -15.92,-8.89 -20.52,-14.58 -4.61,-5.66 -6.93,-12.95 -6.93,-21.85 0,-13.6 5.42,-23.81 16.28,-30.62 10.84,-6.8 27.11,-10.19 48.81,-10.19 20.4,0 36.08,3.72 47,11.17 10.92,7.46 18.17,18.55 21.74,33.28l69.46 -9.47c-6.16,-30.62 -19.87,-52.56 -41.17,-65.84 -21.28,-13.27 -53.14,-19.9 -95.58,-19.9 -44.36,0 -78.17,8.21 -101.4,24.63 -23.25,16.44 -34.86,40.13 -34.86,71.06 0,18.13 3.4,33.27 10.2,45.42 6.8,12.14 16.26,21.85 28.42,29.15 5.5,3.24 11.17,6.19 16.99,8.85 5.83,2.68 13.6,5.42 23.31,8.27 9.72,2.84 23.18,6.27 40.34,10.32 14.9,3.08 27.37,5.87 37.4,8.38 10.04,2.51 17.65,4.82 22.84,6.93 10.52,4.22 18.62,9.48 24.29,15.79 5.66,6.32 8.51,14.34 8.51,24.05 0,15.22 -6.23,26.64 -18.72,34.25 -12.47,7.6 -31.24,11.42 -56.36,11.42 -24.12,0 -42.99,-4.08 -56.59,-12.26 -13.6,-8.17 -22.75,-21.34 -27.44,-39.47l-69.23 11.41c14.25,62.68 64.7,94.01 151.32,94.01z"/>
<polygon class="fil0" points="5117.7,1707.2 5117.7,1651.83 4848.57,1651.83 4848.57,1994.07 5127.66,1994.07 5127.66,1938.68 4920.23,1938.68 4920.23,1848.56 5102.89,1848.56 5102.89,1793.18 4920.23,1793.18 4920.23,1707.2 "/>
<path class="fil0" d="M5279.85 1994.07l138.47 0c32.7,0 61.57,-6.84 86.58,-20.54 25.02,-13.67 44.62,-33.47 58.8,-59.37 14.16,-25.91 21.24,-57.16 21.24,-93.77 0,-54.25 -15.78,-95.9 -47.37,-124.97 -31.57,-29.06 -76.18,-43.59 -133.82,-43.59l-123.89 0 0 342.24zm71.67 -286.87l0 0 50.76 0c35.47,0 62.75,9.68 81.85,29.03 19.12,19.35 28.67,47.4 28.67,84.16 0,24.29 -4.12,45.26 -12.39,62.91 -8.26,17.66 -19.87,31.3 -34.86,40.94 -14.97,9.62 -32.76,14.44 -53.32,14.44l-60.71 0 0 -231.48z"/>
<polygon class="fil0" points="6047.96,1868.49 6223.09,1868.49 6223.09,1813.11 6047.96,1813.11 6047.96,1707.2 6228.67,1707.2 6228.67,1651.83 5976.31,1651.83 5976.31,1994.07 6047.96,1994.07 "/>
<path class="fil0" d="M6446.68 1977.42c25.76,14.34 56.52,21.5 92.3,21.5 34.98,0 65.47,-7.37 91.46,-22.11 25.99,-14.72 46.14,-35.41 60.48,-62.05 14.34,-26.64 21.5,-57.76 21.5,-93.4 0,-36.26 -6.9,-67.45 -20.66,-93.52 -13.75,-26.07 -33.51,-46.11 -59.27,-60.12 -25.74,-14 -56.75,-21 -93.01,-21 -36.12,0 -67.1,6.91 -92.91,20.75 -25.83,13.85 -45.59,33.81 -59.27,59.89 -13.69,26.06 -20.53,57.39 -20.53,93.99 0,36.61 6.87,68.17 20.64,94.74 13.78,26.56 33.54,47 59.27,61.32zm18.47 -243.14l0 0c17.49,-20.81 42.27,-31.2 74.33,-31.2 31.57,0 56.1,10.49 73.58,31.44 17.49,20.97 26.23,49.91 26.23,86.84 0,38.71 -8.69,68.57 -26.11,89.64 -17.41,21.04 -42.14,31.56 -74.21,31.56 -20.57,0 -38.32,-4.89 -53.3,-14.7 -14.98,-9.79 -26.53,-23.76 -34.61,-41.89 -8.1,-18.13 -12.15,-39.67 -12.15,-64.61 0,-37.23 8.74,-66.27 26.23,-87.08z"/>
<path class="fil0" d="M6937.74 1864.11l84.04 0 79.43 129.96 80.64 0 -92.54 -143.8c23.15,-5.49 41.54,-16.96 55.14,-34.36 13.6,-17.4 20.4,-38.1 20.4,-62.06 0,-32.87 -11.1,-58.09 -33.29,-75.67 -22.18,-17.56 -53.68,-26.34 -94.48,-26.34l-171 0 0 342.24 71.66 0 0 -129.96zm0 -156.67l0 0 91.81 0c21.06,0 36.84,4.12 47.37,12.39 10.52,8.26 15.78,20.57 15.78,36.93 0,16.35 -5.17,29.06 -15.55,38.13 -10.36,9.08 -25.57,13.6 -45.66,13.6l-93.75 0 0 -101.05z"/>
</g>
<g>
<path class="fil0" d="M627.35 3128.5c-312.41,-99.91 -458.08,-146.06 -455.14,-313.6 1.93,-109.46 154.21,-225.53 405.68,-225.47 287.79,0.07 385.03,181.46 397.9,255.57l111.56 1.45c-9.62,-46.47 -97.83,-297.67 -524.58,-297.67 -259.34,0 -495.04,135.69 -491.46,309.29 4.01,196.05 149.13,276.7 449.94,366.97 355.51,106.69 500.8,187.23 515.93,345.01 19.96,208.26 -256.33,323.28 -475.22,301.64 -358.92,-35.49 -444.44,-293.58 -445.96,-314.95l-115.99 0c39.03,195.14 288.44,356.1 562.16,356.1 379.65,0 604.74,-178.57 573.21,-410.47 -30.07,-221.31 -214.33,-279.92 -508.03,-373.87z"/>
<path class="fil0" d="M9792.83 3502.37c-30.09,-221.31 -214.33,-279.92 -508.03,-373.87 -312.41,-99.91 -458.08,-146.06 -455.14,-313.6 1.9,-109.46 154.21,-225.53 405.68,-225.47 287.79,0.07 385,181.46 397.9,255.57l111.56 1.45c-9.62,-46.47 -97.83,-297.67 -524.58,-297.67 -259.34,0 -495.04,135.69 -491.46,309.29 4.01,196.05 149.13,276.7 449.95,366.97 355.48,106.69 500.8,187.23 515.92,345.01 19.96,208.26 -256.33,323.28 -475.22,301.64 -358.92,-35.49 -444.44,-293.58 -445.96,-314.95l-116.01 0c39.05,195.14 288.46,356.1 562.18,356.1 379.65,0 604.74,-178.57 573.21,-410.47z"/>
<path class="fil0" d="M5602.13 3400.62c-63.13,271.92 -276.29,471.98 -529.27,471.98 -303.48,0 -549.2,-287.46 -549.2,-641.93 0,-354.48 245.71,-641.94 549.2,-641.94 253.46,0 466.14,200.06 529.27,472.47l132.07 0c-75.26,-294.75 -343.3,-512.77 -661.34,-512.77 -376.82,0 -682.73,305.43 -682.73,682.24 0,377.29 305.91,682.72 682.73,682.72 318.04,0 585.59,-217.54 661.34,-512.77l-132.07 0z"/>
<path class="fil0" d="M7158.78 3127.72l54.39 0 78.2 0c-49.56,-327.76 -332.66,-579.29 -674.5,-579.29 -377.27,0 -682.7,305.43 -682.7,682.24 0,377.29 305.43,682.72 682.7,682.72 258.81,0 483.69,-143.73 599.24,-356.41l-126.28 0c-95.64,188.89 -271.91,315.62 -472.96,315.62 -303.48,0 -549.64,-287.46 -549.64,-641.93 0,-34.96 2.42,-69.44 7.25,-102.95l1084.3 0zm-541.91 -538.99l0 0c261.28,0 480.27,213.66 535.61,499.67l-1071.65 0c55.35,-286.01 274.32,-499.67 536.04,-499.67z"/>
<path class="fil0" d="M8004.3 3128.5c-312.38,-99.91 -458.08,-146.06 -455.14,-313.6 1.93,-109.46 154.21,-225.53 405.7,-225.47 287.79,0.07 385.01,181.46 397.88,255.57l111.56 1.45c-9.62,-46.47 -97.8,-297.67 -524.56,-297.67 -259.33,0 -495.03,135.69 -491.48,309.29 4.03,196.05 149.13,276.7 449.94,366.97 355.51,106.69 500.8,187.23 515.93,345.01 19.96,208.26 -256.3,323.28 -475.22,301.64 -358.9,-35.49 -444.45,-293.58 -445.96,-314.95l-115.99 0c39.03,195.14 288.46,356.1 562.16,356.1 379.64,0 604.74,-178.57 573.23,-410.47 -30.09,-221.31 -214.35,-279.92 -508.05,-373.87z"/>
<path class="fil0" d="M3529.31 2588.72c252.98,0 466.16,200.06 529.28,472.47l131.59 0c-75.26,-294.75 -342.82,-512.77 -660.87,-512.77 -377.29,0 -682.72,305.43 -682.72,682.24 0,377.29 305.43,682.72 682.72,682.72 318.05,0 585.12,-217.54 660.87,-512.77l-131.59 0c-63.12,271.92 -276.77,471.98 -529.28,471.98 -303.48,0 -549.67,-287.46 -549.67,-641.93 0,-354.48 246.18,-641.94 549.67,-641.94z"/>
<path class="fil0" d="M2453.98 2562.09l-0.19 0 0 849.19c0,254.83 -206.58,461.42 -461.41,461.42 -254.86,0 -461.44,-206.59 -461.44,-461.42l0 -849.19 -123.81 0 0 852.58c0,336.18 313.22,498.69 588.65,498.69 205.52,0 381.87,-124.37 458.21,-301.9l0 261.24 123.81 0 0 -1310.94 -123.81 0 0 0.33z"/>
</g>
</g>
<path class="fil0" d="M2863.3 4731.51c41.79,0 74.39,11.02 97.36,33.06 22.96,22.04 35.82,46.84 38.11,74.85l-43.17 0c-5.05,-21.13 -15.15,-38.11 -29.85,-50.51 -14.7,-12.4 -35.37,-18.37 -61.99,-18.37 -32.15,0 -58.32,11.02 -78.53,33.98 -19.75,22.97 -29.86,57.87 -29.86,104.71 0,38.57 9.19,69.8 27.1,94.13 17.91,23.88 45,35.82 80.36,35.82 33.06,0 58.32,-12.4 75.32,-37.65 9.18,-13.32 16.07,-30.77 20.2,-52.81l43.63 0c-3.68,34.9 -16.53,63.83 -38.57,87.25 -26.18,28.47 -61.54,42.71 -105.63,42.71 -38.57,0 -70.72,-11.95 -96.43,-34.9 -34.44,-30.77 -51.44,-78.06 -51.44,-141.9 0,-48.68 12.86,-88.63 38.57,-119.4 27.55,-33.98 66.13,-50.97 114.81,-50.97z"/>
<polygon id="_1" class="fil0" points="3146.94,4740.7 3191.94,4740.7 3191.94,4876.62 3363.23,4876.62 3363.23,4740.7 3408.23,4740.7 3408.23,5069.96 3363.23,5069.96 3363.23,4916.12 3191.94,4916.12 3191.94,5069.96 3146.94,5069.96 "/>
<polygon id="_2" class="fil0" points="3576.6,4740.7 3621.61,4740.7 3621.61,5069.96 3576.6,5069.96 "/>
<polygon id="_3" class="fil0" points="3783.39,4740.7 3827.01,4740.7 3827.01,4900.97 3987.74,4740.7 4049.27,4740.7 3911.96,4873.42 4052.94,5069.96 3995.08,5069.96 3879.82,4904.64 3827.01,4955.15 3827.01,5069.96 3783.39,5069.96 "/>
<path id="_4" class="fil0" d="M4492.31 4868.82c14.23,-10.11 23.87,-18.37 29.38,-24.8 8.73,-10.11 12.86,-21.13 12.86,-33.53 0,-10.11 -3.22,-18.37 -9.64,-25.26 -6.43,-6.89 -14.69,-10.56 -25.71,-10.56 -16.53,0 -28.02,5.51 -34.44,16.53 -3.68,5.51 -5.06,11.94 -5.06,18.82 0,8.73 2.3,17.46 7.35,26.18 5.05,8.28 13.31,19.3 25.26,32.61zm-10.57 173.12c16.53,0 30.77,-3.67 42.71,-11.02 11.95,-7.8 21.59,-16.53 28.02,-25.71l-74.85 -90.92c-21.12,14.24 -34.44,24.8 -40.87,32.15 -10.11,11.47 -15.15,25.25 -15.15,41.33 0,17.45 6.43,30.76 19.29,40.4 12.86,9.19 26.64,13.78 40.87,13.78zm-26.18 -155.21c-14.23,-16.07 -23.42,-29.39 -28.01,-40.41 -4.6,-11.02 -7.35,-21.58 -7.35,-31.69 0,-21.13 7.35,-38.57 21.58,-52.81 14.7,-13.78 33.53,-20.66 57.86,-20.66 22.97,0 40.41,6.44 53.28,19.29 12.85,12.86 19.29,28.48 19.29,46.84 0,21.13 -6.44,39.5 -19.75,55.12 -7.8,9.64 -20.66,20.2 -39.04,32.14l60.16 71.64c4.13,-11.94 6.89,-20.66 8.27,-26.63 1.83,-5.97 3.21,-14.24 5.05,-24.8l38.12 0c-2.3,21.12 -7.35,41.33 -15.15,60.62 -7.81,19.29 -11.48,27.09 -11.48,23.42l58.78 71.17 -52.35 0 -30.77 -37.66c-12.4,13.32 -23.42,22.97 -33.52,28.93 -17.91,11.02 -38.57,16.53 -61.54,16.53 -34.44,0 -59.24,-9.18 -74.85,-28.01 -15.62,-18.37 -23.42,-39.04 -23.42,-62.46 0,-24.8 7.8,-45.46 22.96,-62.46 9.18,-10.1 26.64,-22.96 51.89,-38.11z"/>
<path id="_5" class="fil0" d="M5152.26 4934.95l-50.06 -145.58 -52.8 145.58 102.86 0zm-73.01 -194.25l50.51 0 119.4 329.26 -49.13 0 -33.07 -98.73 -130.41 0 -35.82 98.73 -45.46 0 123.99 -329.26z"/>
<path id="_6" class="fil0" d="M5517.55 4731.51c41.79,0 74.39,11.02 97.36,33.06 22.96,22.04 35.82,46.84 38.11,74.85l-43.17 0c-5.05,-21.13 -15.15,-38.11 -29.85,-50.51 -14.7,-12.4 -35.37,-18.37 -61.99,-18.37 -32.15,0 -58.32,11.02 -78.53,33.98 -19.75,22.97 -29.86,57.87 -29.86,104.71 0,38.57 9.19,69.8 27.1,94.13 17.91,23.88 45,35.82 80.36,35.82 33.06,0 58.32,-12.4 75.32,-37.65 9.18,-13.32 16.07,-30.77 20.2,-52.81l43.63 0c-3.68,34.9 -16.53,63.83 -38.57,87.25 -26.18,28.47 -61.54,42.71 -105.63,42.71 -38.57,0 -70.72,-11.95 -96.43,-34.9 -34.44,-30.77 -51.44,-78.06 -51.44,-141.9 0,-48.68 12.86,-88.63 38.57,-119.4 27.55,-33.98 66.13,-50.97 114.81,-50.97z"/>
<polygon id="_7" class="fil0" points="6039.52,4740.7 6039.52,4779.73 5928.39,4779.73 5928.39,5069.96 5883.39,5069.96 5883.39,4779.73 5772.26,4779.73 5772.26,4740.7 "/>
<polygon id="_8" class="fil0" points="6179.72,4740.7 6224.73,4740.7 6224.73,5069.96 6179.72,5069.96 "/>
<polygon id="_9" class="fil0" points="6412.69,4740.7 6507.28,5021.28 6600.51,4740.7 6650.56,4740.7 6530.7,5069.96 6483.4,5069.96 6363.09,4740.7 "/>
<polygon id="_10" class="fil0" points="6786.25,4740.7 7026.42,4740.7 7026.42,4781.1 6829.87,4781.1 6829.87,4880.76 7011.72,4880.76 7011.72,4918.88 6829.87,4918.88 6829.87,5030.92 7030.09,5030.92 7030.09,5069.96 6786.25,5069.96 "/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 15 KiB

View File

@ -0,0 +1,55 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Creator: CorelDRAW 2021.5 -->
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" width="101.681mm" height="52.664mm" version="1.1" style="shape-rendering:geometricPrecision; text-rendering:geometricPrecision; image-rendering:optimizeQuality; fill-rule:evenodd; clip-rule:evenodd"
viewBox="0 0 9785.87 5068.43"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:xodm="http://www.corel.com/coreldraw/odm/2003">
<defs>
<style type="text/css">
<![CDATA[
.fil0 {fill:#2B2B2A;fill-rule:nonzero}
]]>
</style>
</defs>
<g id="Слой_x0020_1">
<metadata id="CorelCorpID_0Corel-Layer"/>
<g id="_105553243821280">
<path class="fil0" d="M4903.31 804.37c54.01,19.95 154.59,32.65 273.14,7.79 134.15,-28.14 227.19,-126.74 210.26,-234.18 -15.56,-98.78 -78.42,-144.64 -183.02,-184.69 -4.99,-103.47 -48.37,-197.48 -117.37,-265.9 -25.59,-25.58 -54.51,-47.28 -86.21,-65.08 -19.34,-12.42 -50.08,-24.48 -52.86,-25.59 30.04,-7.23 62.86,-11.36 101.24,-12.24 160.75,-3.71 233.02,102.92 241.44,154.65l67.31 1.11c-5.57,-28.37 -59.54,-180.23 -318.2,-180.23 -47.84,0 -94.56,8.34 -136.28,22.25 -33.38,-8.9 -66.76,-13.91 -102.37,-13.91 -101.23,0 -403.3,0.56 -403.3,0.56l0 17.24c35.06,0 57.31,8.35 71.76,20.59 26.16,21.14 26.16,55.08 26.16,74.54 0,145.61 0,436.82 0,582.42 0,22.81 0,67.87 -45.63,86.78 -12.79,5.56 -30.05,9.46 -52.29,9.46l0 17.79c0,0 244.88,0 395.69,0 61.64,0 110.52,-13.35 110.52,-13.35zm-333.22 -11.13l0 0 0 -759.87 180.25 0c66.76,0 127.29,11.34 204.71,62.85 94.25,62.72 161.42,156.32 171.88,270.36 16.63,181.37 -83.58,312.32 -175.22,367.69 23.36,10.02 47.26,17.25 62.85,21.14 96.8,-61.19 164.89,-170.02 183.56,-279.25 84.01,39.5 122.93,81.22 129.07,142.41 4.99,52.85 -21.13,95.68 -63.42,126.83 -57.85,43.4 -148.04,64.57 -225.28,56.74 -162.45,-16.46 -249.97,-113.59 -270.37,-191.36l-70.07 0c12.84,56.49 58.52,124.57 147.4,169.66 0,0 -38.51,12.79 -97.35,12.79 -44.5,0 -178.02,0 -178.02,0z"/>
<path class="fil0" d="M5014.02 409.98c23.36,7.23 44.49,13.91 64.51,20.58 1.12,-21.69 1.68,-51.73 -3.34,-80.66 -187.47,-59.52 -274.8,-87.89 -273.11,-189.13 0.55,-23.92 12.22,-47.28 34.46,-68.42 -13.33,-5 -27.81,-9.46 -41.14,-12.24 -42.46,35.57 -54.55,75.83 -54.51,107.37 0.11,118.51 90.66,167.99 273.13,222.51z"/>
</g>
<g id="_105553243768096">
<g>
<path class="fil0" d="M2918.1 1912.22c14.16,-25.88 21.22,-57.1 21.22,-93.67 0,-54.19 -15.76,-95.8 -47.32,-124.84 -31.53,-29.03 -76.11,-43.55 -133.69,-43.55l-123.76 0 0 341.9 138.32 0c32.67,0 61.52,-6.83 86.49,-20.52 25,-13.66 44.58,-33.43 58.74,-59.31zm-98.03 10.08l0 0c-14.97,9.61 -32.73,14.43 -53.28,14.43l-60.65 0 0 -231.25 50.71 0c35.44,0 62.69,9.67 81.77,29 19.1,19.33 28.65,47.35 28.65,84.08 0,24.26 -4.13,45.21 -12.39,62.85 -8.24,17.64 -19.85,31.27 -34.81,40.9z"/>
<path class="fil0" d="M3164.42 1862.22l83.94 0 79.36 129.83 80.55 0 -92.44 -143.66c23.13,-5.49 41.5,-16.94 55.09,-34.33 13.58,-17.38 20.37,-38.06 20.37,-62 0,-32.84 -11.08,-58.03 -33.25,-75.6 -22.16,-17.54 -53.63,-26.31 -94.39,-26.31l-170.83 0 0 341.9 71.59 0 0 -129.83zm0 -156.51l0 0 91.71 0c21.05,0 36.81,4.12 47.32,12.38 10.52,8.25 15.77,20.55 15.77,36.89 0,16.33 -5.17,29.03 -15.53,38.09 -10.35,9.07 -25.54,13.59 -45.62,13.59l-93.65 0 0 -100.95z"/>
<polygon class="fil0" points="3829.91,1936.72 3622.69,1936.72 3622.69,1846.69 3805.17,1846.69 3805.17,1791.37 3622.69,1791.37 3622.69,1705.48 3819.96,1705.48 3819.96,1650.16 3551.11,1650.16 3551.11,1992.05 3829.91,1992.05 "/>
<path class="fil0" d="M4235.28 1827.54c-4.05,-3.88 -8.84,-7.53 -14.44,-10.93 -5.58,-3.39 -11.94,-6.63 -19.05,-9.71 -7.12,-2.91 -16.59,-5.93 -28.4,-9.09 -11.8,-3.16 -25.88,-6.51 -42.21,-10.06 -28.15,-5.99 -46.52,-10.93 -55.09,-14.81 -9.07,-4.03 -15.9,-8.88 -20.5,-14.56 -4.61,-5.66 -6.92,-12.93 -6.92,-21.83 0,-13.59 5.42,-23.79 16.26,-30.59 10.83,-6.79 27.08,-10.18 48.77,-10.18 20.38,0 36.04,3.71 46.96,11.15 10.91,7.45 18.15,18.53 21.72,33.25l69.39 -9.46c-6.15,-30.59 -19.85,-52.51 -41.13,-65.77 -21.26,-13.25 -53.09,-19.88 -95.48,-19.88 -44.32,0 -78.09,8.2 -101.29,24.61 -23.22,16.43 -34.83,40.09 -34.83,70.99 0,18.11 3.4,33.23 10.19,45.38 6.79,12.13 16.25,21.83 28.39,29.12 5.5,3.23 11.15,6.19 16.98,8.84 5.82,2.68 13.59,5.42 23.29,8.26 9.71,2.84 23.16,6.27 40.3,10.31 14.89,3.08 27.34,5.86 37.36,8.37 10.03,2.51 17.63,4.81 22.82,6.92 10.51,4.22 18.6,9.47 24.26,15.77 5.66,6.31 8.5,14.32 8.5,24.02 0,15.21 -6.23,26.61 -18.7,34.21 -12.45,7.59 -31.21,11.4 -56.3,11.4 -24.1,0 -42.94,-4.08 -56.53,-12.25 -13.59,-8.16 -22.72,-21.32 -27.41,-39.43l-69.16 11.39c14.23,62.61 64.64,93.91 151.17,93.91 48.05,0 84.5,-8.86 109.33,-26.57 24.81,-17.71 37.23,-43.32 37.23,-76.79 0,-14.24 -2.18,-26.83 -6.53,-37.75 -4.38,-10.91 -10.67,-20.35 -18.93,-28.26z"/>
<path class="fil0" d="M4545.05 1996.9c48.05,0 84.5,-8.86 109.33,-26.57 24.81,-17.71 37.24,-43.32 37.24,-76.79 0,-14.24 -2.18,-26.83 -6.54,-37.75 -4.38,-10.91 -10.67,-20.35 -18.93,-28.26 -4.05,-3.88 -8.84,-7.53 -14.44,-10.93 -5.58,-3.39 -11.94,-6.63 -19.05,-9.71 -7.12,-2.91 -16.59,-5.93 -28.4,-9.09 -11.8,-3.16 -25.88,-6.51 -42.21,-10.06 -28.15,-5.99 -46.52,-10.93 -55.09,-14.81 -9.07,-4.03 -15.9,-8.88 -20.5,-14.56 -4.61,-5.66 -6.92,-12.93 -6.92,-21.83 0,-13.59 5.42,-23.79 16.26,-30.59 10.83,-6.79 27.08,-10.18 48.77,-10.18 20.38,0 36.04,3.71 46.96,11.15 10.91,7.45 18.15,18.53 21.72,33.25l69.39 -9.46c-6.15,-30.59 -19.85,-52.51 -41.13,-65.77 -21.26,-13.25 -53.09,-19.88 -95.48,-19.88 -44.32,0 -78.09,8.2 -101.29,24.61 -23.22,16.43 -34.83,40.09 -34.83,70.99 0,18.11 3.4,33.23 10.19,45.38 6.79,12.13 16.25,21.83 28.39,29.12 5.5,3.23 11.15,6.19 16.98,8.84 5.82,2.68 13.59,5.42 23.29,8.26 9.71,2.84 23.16,6.27 40.3,10.31 14.89,3.08 27.34,5.86 37.36,8.37 10.03,2.51 17.63,4.81 22.82,6.92 10.51,4.22 18.6,9.47 24.26,15.77 5.66,6.31 8.5,14.32 8.5,24.02 0,15.21 -6.23,26.61 -18.7,34.21 -12.45,7.59 -31.21,11.4 -56.3,11.4 -24.1,0 -42.94,-4.08 -56.53,-12.25 -13.59,-8.16 -22.72,-21.32 -27.41,-39.43l-69.16 11.39c14.23,62.61 64.64,93.91 151.17,93.91z"/>
<polygon class="fil0" points="5112.53,1705.48 5112.53,1650.16 4843.67,1650.16 4843.67,1992.05 5122.48,1992.05 5122.48,1936.72 4915.25,1936.72 4915.25,1846.69 5097.74,1846.69 5097.74,1791.37 4915.25,1791.37 4915.25,1705.48 "/>
<path class="fil0" d="M5274.51 1992.05l138.33 0c32.66,0 61.51,-6.83 86.49,-20.52 24.99,-13.66 44.58,-33.43 58.74,-59.31 14.15,-25.88 21.22,-57.1 21.22,-93.67 0,-54.19 -15.76,-95.8 -47.32,-124.84 -31.54,-29.03 -76.11,-43.55 -133.69,-43.55l-123.77 0 0 341.9zm71.59 -286.58l0 0 50.71 0c35.44,0 62.68,9.67 81.77,29 19.1,19.33 28.64,47.35 28.64,84.08 0,24.26 -4.12,45.21 -12.38,62.85 -8.25,17.64 -19.85,31.27 -34.82,40.9 -14.96,9.61 -32.72,14.43 -53.27,14.43l-60.65 0 0 -231.25z"/>
<polygon class="fil0" points="6041.85,1866.6 6216.8,1866.6 6216.8,1811.27 6041.85,1811.27 6041.85,1705.48 6222.38,1705.48 6222.38,1650.16 5970.26,1650.16 5970.26,1992.05 6041.85,1992.05 "/>
<path class="fil0" d="M6440.16 1975.42c25.73,14.32 56.46,21.48 92.21,21.48 34.95,0 65.41,-7.36 91.37,-22.09 25.97,-14.71 46.09,-35.38 60.42,-61.99 14.32,-26.61 21.48,-57.71 21.48,-93.31 0,-36.23 -6.89,-67.38 -20.64,-93.42 -13.73,-26.04 -33.47,-46.06 -59.21,-60.05 -25.72,-13.98 -56.7,-20.98 -92.92,-20.98 -36.08,0 -67.03,6.9 -92.81,20.73 -25.8,13.84 -45.54,33.77 -59.21,59.83 -13.68,26.03 -20.51,57.33 -20.51,93.89 0,36.57 6.86,68.1 20.61,94.64 13.76,26.53 33.5,46.96 59.21,61.26zm18.45 -242.89l0 0c17.47,-20.79 42.23,-31.17 74.26,-31.17 31.54,0 56.04,10.48 73.51,31.41 17.47,20.95 26.21,49.86 26.21,86.75 0,38.67 -8.68,68.5 -26.08,89.55 -17.39,21.02 -42.1,31.53 -74.13,31.53 -20.55,0 -38.28,-4.89 -53.25,-14.69 -14.97,-9.78 -26.5,-23.73 -34.58,-41.85 -8.09,-18.11 -12.14,-39.63 -12.14,-64.55 0,-37.2 8.73,-66.2 26.21,-86.99z"/>
<path class="fil0" d="M6930.73 1862.22l83.95 0 79.35 129.83 80.56 0 -92.45 -143.66c23.13,-5.49 41.5,-16.94 55.09,-34.33 13.59,-17.38 20.38,-38.06 20.38,-62 0,-32.84 -11.09,-58.03 -33.26,-75.6 -22.15,-17.54 -53.63,-26.31 -94.38,-26.31l-170.83 0 0 341.9 71.58 0 0 -129.83zm0 -156.51l0 0 91.72 0c21.04,0 36.8,4.12 47.32,12.38 10.51,8.25 15.76,20.55 15.76,36.89 0,16.33 -5.17,29.03 -15.53,38.09 -10.35,9.07 -25.54,13.59 -45.62,13.59l-93.65 0 0 -100.95z"/>
</g>
<g>
<path class="fil0" d="M626.71 3125.34c-312.09,-99.81 -457.62,-145.91 -454.68,-313.28 1.92,-109.35 154.05,-225.3 405.27,-225.24 287.5,0.07 384.64,181.28 397.49,255.31l111.45 1.45c-9.61,-46.43 -97.73,-297.37 -524.05,-297.37 -259.08,0 -494.54,135.56 -490.96,308.98 4,195.85 148.98,276.42 449.48,366.6 355.15,106.58 500.3,187.04 515.41,344.66 19.94,208.05 -256.07,322.96 -474.74,301.33 -358.56,-35.46 -443.99,-293.28 -445.51,-314.63l-115.87 0c38.99,194.95 288.15,355.74 561.59,355.74 379.27,0 604.13,-178.39 572.63,-410.05 -30.04,-221.08 -214.12,-279.64 -507.52,-373.49z"/>
<path class="fil0" d="M9782.93 3498.83c-30.06,-221.08 -214.12,-279.64 -507.52,-373.49 -312.09,-99.81 -457.62,-145.91 -454.68,-313.28 1.9,-109.35 154.05,-225.3 405.27,-225.24 287.5,0.07 384.61,181.28 397.49,255.31l111.45 1.45c-9.61,-46.43 -97.73,-297.37 -524.05,-297.37 -259.08,0 -494.54,135.56 -490.96,308.98 4,195.85 148.98,276.42 449.49,366.6 355.12,106.58 500.29,187.04 515.4,344.66 19.94,208.05 -256.07,322.96 -474.74,301.33 -358.56,-35.46 -443.99,-293.28 -445.51,-314.63l-115.89 0c39.01,194.95 288.16,355.74 561.61,355.74 379.27,0 604.13,-178.39 572.63,-410.05z"/>
<path class="fil0" d="M5596.47 3397.18c-63.07,271.65 -276.01,471.5 -528.74,471.5 -303.18,0 -548.64,-287.17 -548.64,-641.28 0,-354.12 245.46,-641.29 548.64,-641.29 253.2,0 465.67,199.85 528.74,471.99l131.94 0c-75.18,-294.45 -342.95,-512.25 -660.67,-512.25 -376.44,0 -682.04,305.12 -682.04,681.55 0,376.91 305.6,682.03 682.04,682.03 317.72,0 585,-217.32 660.67,-512.25l-131.94 0z"/>
<path class="fil0" d="M7151.54 3124.56l54.34 0 78.12 0c-49.51,-327.43 -332.32,-578.71 -673.82,-578.71 -376.89,0 -682.01,305.12 -682.01,681.55 0,376.91 305.12,682.03 682.01,682.03 258.55,0 483.2,-143.58 598.64,-356.05l-126.15 0c-95.55,188.7 -271.63,315.3 -472.49,315.3 -303.18,0 -549.08,-287.17 -549.08,-641.28 0,-34.93 2.42,-69.37 7.25,-102.84l1083.2 0zm-541.36 -538.45l0 0c261.01,0 479.78,213.44 535.07,499.16l-1070.56 0c55.29,-285.72 274.05,-499.16 535.49,-499.16z"/>
<path class="fil0" d="M7996.21 3125.34c-312.06,-99.81 -457.62,-145.91 -454.68,-313.28 1.92,-109.35 154.05,-225.3 405.29,-225.24 287.5,0.07 384.62,181.28 397.47,255.31l111.45 1.45c-9.61,-46.43 -97.7,-297.37 -524.03,-297.37 -259.07,0 -494.53,135.56 -490.98,308.98 4.02,195.85 148.98,276.42 449.48,366.6 355.15,106.58 500.3,187.04 515.41,344.66 19.94,208.05 -256.04,322.96 -474.74,301.33 -358.54,-35.46 -444,-293.28 -445.51,-314.63l-115.87 0c38.99,194.95 288.16,355.74 561.59,355.74 379.26,0 604.13,-178.39 572.65,-410.05 -30.06,-221.08 -214.14,-279.64 -507.54,-373.49z"/>
<path class="fil0" d="M3525.75 2586.11c252.73,0 465.69,199.85 528.75,471.99l131.46 0c-75.18,-294.45 -342.47,-512.25 -660.2,-512.25 -376.91,0 -682.03,305.12 -682.03,681.55 0,376.91 305.12,682.03 682.03,682.03 317.73,0 584.53,-217.32 660.2,-512.25l-131.46 0c-63.06,271.65 -276.49,471.5 -528.75,471.5 -303.18,0 -549.11,-287.17 -549.11,-641.28 0,-354.12 245.93,-641.29 549.11,-641.29z"/>
<path class="fil0" d="M2451.5 2559.5l-0.19 0 0 848.33c0,254.58 -206.37,460.96 -460.95,460.96 -254.61,0 -460.97,-206.38 -460.97,-460.96l0 -848.33 -123.69 0 0 851.72c0,335.84 312.91,498.18 588.05,498.18 205.31,0 381.48,-124.25 457.75,-301.59l0 260.98 123.69 0 0 -1309.62 -123.69 0 0 0.33z"/>
</g>
</g>
<polygon class="fil0" points="2756.34,4729.87 2801.29,4729.87 2801.29,4865.66 2972.41,4865.66 2972.41,4729.87 3017.36,4729.87 3017.36,5058.8 2972.41,5058.8 2972.41,4905.12 2801.29,4905.12 2801.29,5058.8 2756.34,5058.8 "/>
<path id="_1" class="fil0" d="M3317.68 4720.69c57.81,0 100.93,18.82 128.91,55.97 22.02,29.35 32.57,66.52 32.57,111.94 0,49.08 -12.39,89.91 -37.16,122.49 -29.35,38.07 -71.1,57.34 -125.24,57.34 -50.92,0 -90.37,-16.51 -119.27,-50.01 -26.15,-32.57 -39,-73.39 -39,-122.49 0,-44.49 11.01,-82.57 33.03,-114.23 28.44,-40.83 70.65,-61.02 126.15,-61.02zm4.59 307.83c39,0 67.44,-14.22 84.87,-42.2 17.89,-27.99 26.6,-60.55 26.6,-97.26 0,-38.53 -10.09,-69.73 -30.28,-93.12 -20.64,-23.86 -48.17,-35.33 -83.04,-35.33 -34.41,0 -61.93,11.47 -83.49,34.87 -21.56,23.4 -32.12,57.8 -32.12,103.22 0,36.71 9.18,67.44 27.52,92.21 18.35,25.23 48.17,37.62 89.92,37.62z"/>
<path id="_2" class="fil0" d="M3620.6 4729.87l63.77 0 94.5 278 94.05 -278 62.85 0 0 328.92 -42.21 0 0 -194.05c0,-6.88 0,-17.89 0.46,-33.49 0.46,-15.6 0.46,-32.12 0.46,-50.01l-94.05 277.55 -44.04 0 -94.51 -277.55 0 10.1c0,8.26 0,20.18 0.46,36.71 0.46,16.51 0.91,28.9 0.91,36.7l0 194.05 -42.66 0 0 -328.92z"/>
<polygon id="_3" class="fil0" points="4097.01,4729.87 4336.94,4729.87 4336.94,4770.24 4140.59,4770.24 4140.59,4869.79 4322.26,4869.79 4322.26,4907.87 4140.59,4907.87 4140.59,5019.8 4340.61,5019.8 4340.61,5058.8 4097.01,5058.8 "/>
<path id="_4" class="fil0" d="M5345.98 4952.83c0.92,18.35 5.5,33.49 12.85,45.42 15.14,21.56 40.83,32.57 78.45,32.57 16.51,0 32.12,-2.29 45.42,-7.33 26.61,-9.18 39.91,-25.7 39.91,-49.54 0,-17.89 -5.5,-30.74 -16.51,-38.53 -11.47,-7.34 -29.36,-13.76 -53.21,-19.27l-44.96 -10.1c-28.9,-6.42 -49.54,-13.76 -61.47,-21.56 -21.11,-13.76 -31.65,-34.41 -31.65,-61.94 0,-29.35 10.55,-53.67 30.74,-72.48 20.64,-19.27 49.54,-28.44 87.16,-28.44 34.41,0 63.77,8.26 87.63,24.77 24.31,16.51 36.24,43.12 36.24,79.82l-41.75 0c-2.3,-17.44 -6.88,-31.2 -14.22,-40.37 -13.76,-17.43 -36.7,-25.69 -69.27,-25.69 -26.6,0 -45.42,5.5 -56.88,16.51 -11.47,11.01 -16.98,23.85 -16.98,38.53 0,16.05 6.42,27.99 19.73,35.32 9.17,4.59 28.9,10.56 60.09,17.89l45.88 10.56c22.47,5.04 39.45,11.92 51.84,20.64 20.64,15.59 31.2,38.07 31.2,66.97 0,36.7 -13.31,62.85 -39.91,78.45 -26.16,15.6 -56.89,23.4 -92.22,23.4 -40.83,0 -72.94,-10.55 -95.88,-31.19 -23.4,-21.11 -34.87,-49.09 -34.41,-84.41l42.2 0z"/>
<polygon id="_5" class="fil0" points="5711.83,4729.87 5756.32,4729.87 5756.32,5019.8 5923.31,5019.8 5923.31,5058.8 5711.83,5058.8 "/>
<polygon id="_6" class="fil0" points="6060.32,4729.87 6300.25,4729.87 6300.25,4770.24 6103.9,4770.24 6103.9,4869.79 6285.56,4869.79 6285.56,4907.87 6103.9,4907.87 6103.9,5019.8 6303.91,5019.8 6303.91,5058.8 6060.32,5058.8 "/>
<polygon id="_7" class="fil0" points="6455.52,4729.87 6695.45,4729.87 6695.45,4770.24 6499.1,4770.24 6499.1,4869.79 6680.76,4869.79 6680.76,4907.87 6499.1,4907.87 6499.1,5019.8 6699.12,5019.8 6699.12,5058.8 6455.52,5058.8 "/>
<path id="_8" class="fil0" d="M6850.72 4729.87l148.17 0c29.36,0 52.76,8.26 71.11,24.77 17.89,16.51 26.6,39.45 26.6,69.26 0,25.7 -7.8,48.18 -23.85,67.45 -16.06,18.81 -40.84,28.44 -73.86,28.44l-103.22 0 0 139 -44.95 0 0 -328.92zm200.93 94.5c0,-24.31 -9.18,-40.83 -27.07,-49.54 -9.63,-4.59 -23.4,-6.88 -40.36,-6.88l-88.54 0 0 114.23 88.54 0c20.18,0 36.23,-4.58 48.62,-12.84 12.4,-8.72 18.82,-23.4 18.82,-44.96z"/>
<path id="_9" class="fil0" d="M4801.09 4857.86c14.21,-10.1 23.85,-18.35 29.35,-24.77 8.72,-10.1 12.85,-21.11 12.85,-33.49 0,-10.1 -3.21,-18.35 -9.63,-25.23 -6.42,-6.88 -14.68,-10.55 -25.69,-10.55 -16.51,0 -27.99,5.5 -34.41,16.51 -3.68,5.5 -5.05,11.92 -5.05,18.81 0,8.72 2.3,17.44 7.34,26.15 5.04,8.27 13.3,19.28 25.23,32.58zm-10.56 172.94c16.51,0 30.74,-3.67 42.66,-11.01 11.93,-7.8 21.57,-16.51 27.99,-25.69l-74.78 -90.83c-21.1,14.22 -34.41,24.77 -40.83,32.12 -10.1,11.46 -15.14,25.22 -15.14,41.29 0,17.43 6.42,30.73 19.27,40.36 12.85,9.18 26.61,13.76 40.83,13.76zm-26.15 -155.05c-14.21,-16.05 -23.4,-29.36 -27.98,-40.37 -4.59,-11.01 -7.34,-21.56 -7.34,-31.65 0,-21.11 7.34,-38.53 21.56,-52.76 14.69,-13.76 33.49,-20.64 57.8,-20.64 22.94,0 40.37,6.43 53.22,19.27 12.84,12.85 19.27,28.45 19.27,46.79 0,21.11 -6.43,39.46 -19.73,55.06 -7.8,9.63 -20.64,20.18 -39,32.11l60.1 71.56c4.13,-11.92 6.88,-20.64 8.26,-26.6 1.83,-5.97 3.2,-14.22 5.04,-24.77l38.08 0c-2.3,21.1 -7.34,41.29 -15.14,60.55 -7.81,19.27 -11.47,27.06 -11.47,23.4l58.72 71.1 -52.3 0 -30.74 -37.62c-12.39,13.31 -23.4,22.94 -33.48,28.9 -17.89,11.01 -38.53,16.51 -61.48,16.51 -34.41,0 -59.18,-9.17 -74.77,-27.98 -15.6,-18.35 -23.4,-39 -23.4,-62.39 0,-24.77 7.8,-45.42 22.93,-62.39 9.17,-10.09 26.61,-22.93 51.84,-38.07z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 16 KiB

View File

@ -1 +0,0 @@
placeholder

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 MiB

View File

@ -1,37 +1,148 @@
user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log notice;
pid /var/run/nginx.pid;
# Настройки воркеров и событий (можно оставить по умолчанию для начала)
worker_processes auto;
events {
worker_connections 1024;
}
http {
include mime.types;
include /etc/nginx/mime.types;
default_type application/octet-stream;
# Настройки логов (пути внутри контейнера Nginx)
access_log /var/log/nginx/access.log;
error_log /var/log/nginx/error.log;
# Настройки отправки файлов и keepalive
sendfile on;
keepalive_timeout 65;
upstream fastapi {
server fastapi:8000; # контейнер FastAPI по DNS-имени
}
upstream php {
server php:80; # контейнер PHP
# Сжатие (опционально, но рекомендуется)
gzip on;
gzip_disable "msie6";
gzip_vary on;
gzip_proxied any;
gzip_comp_level 6;
gzip_buffers 16 8k;
gzip_http_version 1.1;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript image/svg+xml;
# Увеличиваем максимальный размер тела запроса (например, для загрузки файлов)
client_max_body_size 50m; # Установите подходящее значение
# Определяем upstream серверы для проксирования
# Используем имена сервисов из docker-compose.yml
upstream fastapi_backend {
# Используем имя сервиса и его внутренний порт
server fastapi:8000;
}
upstream frontend_app {
# Используем имя сервиса и его внутренний порт
server frontend:3000;
}
upstream php_backend {
# Apache внутри php контейнера слушает порт 80
server php:80;
}
# Основной сервер, слушающий порт 80
server {
listen 80;
server_name localhost yourdomain.com; # Замените на ваш домен или оставьте localhost
location /api/fastapi/ {
proxy_pass http://fastapi/; # проксируем на FastAPI
# Проксируем запросы к API на бэкенд FastAPI
location /api/ {
proxy_pass http://fastapi_backend/api/; # Обратите внимание на слэш в конце
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 300s; # Увеличить таймаут для долгих запросов (опционально)
proxy_connect_timeout 75s;
}
location /api/php/ {
rewrite ^/api/php/(.*)$ /$1 break;
proxy_pass http://php/; # проксируем на PHP
# Проксируем запросы к PHP скрипту (например, /php/service.php)
location /php/service.php { # Вы можете выбрать другой префикс
proxy_pass http://php_backend/service.php; # Проксируем на корень Apache внутри PHP контейнера
# Если ваш PHP скрипт лежит глубже, настройте путь соответственно
# Например, если скрипт в /script.php, то location /my-php-script { proxy_pass http://php_backend/script.php; }
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# Проксирование запросов к MinIO для изображений
location ~ ^/dressedforsuccess/ {
proxy_pass http://45.129.128.113:9000;
proxy_http_version 1.1;
proxy_set_header Host 45.129.128.113:9000;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Connection "";
proxy_buffering off;
proxy_request_buffering off;
proxy_read_timeout 300s;
proxy_connect_timeout 75s;
proxy_send_timeout 300s;
# Добавляем заголовки CORS для изображений
add_header Access-Control-Allow-Origin *;
add_header Access-Control-Allow-Methods 'GET, OPTIONS';
add_header Access-Control-Allow-Headers 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range';
add_header Access-Control-Expose-Headers 'Content-Length,Content-Range';
}
# Обработка запросов Next.js Image Optimization API
location /_next/image {
proxy_pass http://frontend_app;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Port $server_port;
proxy_buffer_size 128k;
proxy_buffers 4 256k;
proxy_busy_buffers_size 256k;
proxy_read_timeout 300s;
proxy_connect_timeout 75s;
}
# Все остальные запросы проксируем на фронтенд Next.js
location / {
proxy_pass http://frontend_app;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade; # Для поддержки WebSocket (если используется Next.js)
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache_bypass $http_upgrade;
}
# Обработка ошибок (опционально)
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html; # Стандартная папка Nginx
}
}
# Если вы настраиваете HTTPS, добавьте здесь еще один server блок для порта 443
# server {
# listen 443 ssl http2;
# server_name yourdomain.com;
#
# ssl_certificate /etc/nginx/certs/yourdomain.com.crt; # Путь к сертификату внутри контейнера
# ssl_certificate_key /etc/nginx/certs/yourdomain.com.key; # Путь к ключу
#
# # ... остальные настройки SSL ...
#
# # ... те же location блоки, что и для порта 80 ...
# }
}

View File

@ -1,5 +1,7 @@
<?php
header("Access-Control-Allow-Origin: http://localhost:3000");
ini_set('memory_limit', '256M');
// header("Access-Control-Allow-Origin: http://localhost:3000");
header("Access-Control-Allow-Origin: *");
header("Access-Control-Allow-Methods: GET, POST, OPTIONS");
header("Access-Control-Allow-Headers: Content-Type, Authorization, X-Requested-With");
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
@ -7,15 +9,17 @@ if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
exit();
}
$service = new service(/**
* Вставьте свой аккаунт\идентификатор для интеграции
* Put your account for integration here
*/ 'mlJIctcTrFe47qb5rvoBaqk8Keay8RTB',
$service = new service(
/**
* Вставьте свой аккаунт\идентификатор для интеграции
* Put your account for integration here
*/ 'mlJIctcTrFe47qb5rvoBaqk8Keay8RTB',
/**
* Вставьте свой пароль для интеграци
* Put your password for integration here
*/ 'q8AQtmLL7kPg6TuDo1eB2uqelJS4tHn2');
*/ 'q8AQtmLL7kPg6TuDo1eB2uqelJS4tHn2'
);
$service->process($_GET, file_get_contents('php://input'));
class service
@ -285,17 +289,23 @@ class service
));
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) {
throw new RuntimeException(curl_error($ch), curl_errno($ch));
}
$headerSize = curl_getinfo($ch, CURLINFO_HEADER_SIZE);
$headers = substr($response, 0, $headerSize);
$result = substr($response, $headerSize);
$addedHeaders = $this->getHeaderValue($headers);
if ($result === false) {
throw new RuntimeException(curl_error($ch), curl_errno($ch));
}
return array('result' => $result, 'addedHeaders' => $addedHeaders);
return array(
'result' => $result,
'addedHeaders' => $addedHeaders,
'httpCode' => $httpCode
);
}
private function getHeaderValue($headers)
@ -342,26 +352,41 @@ class service
protected function getOffices()
{
// ---------- КЭШ ----------
$cacheKey = md5(json_encode($this->requestData));
$cacheFile = "{$this->cacheDir}/{$cacheKey}.json.gz";
// HIT — отдаём из кэша
// 1) Отдаём из кэша, если не протухло
if (is_file($cacheFile) && (time() - filemtime($cacheFile)) < $this->cacheTtl) {
return array(
return [
'result' => gzdecode(file_get_contents($cacheFile)),
'addedHeaders' => array('X-Cache: HIT')
);
'addedHeaders' => ['X-Cache: HIT']
];
}
// MISS — делаем запрос
// 2) MISS — делаем запрос
$time = $this->startMetrics();
$result = $this->httpRequest('deliverypoints', $this->requestData);
$this->endMetrics('office', 'Offices Request', $time);
// сохраняем сжатую копию
// 3) Проверяем HTTP-код
if ($result['httpCode'] !== 200) {
$result['addedHeaders'][] = 'X-Cache: SKIP_HTTP_' . $result['httpCode'];
return $result;
}
// 4) Проверяем, что в теле пришли данные (пример: ждём непустой массив deliveryPoints)
$bodyData = json_decode($result['result'], true);
if (
json_last_error() !== JSON_ERROR_NONE
) {
$result['addedHeaders'][] = 'X-Cache: SKIP_INVALID_DATA';
return $result;
}
// 5) Наконец — сохраняем в кэш
file_put_contents($cacheFile, gzencode($result['result'], 6), LOCK_EX);
$result['addedHeaders'][] = 'X-Cache: MISS';
return $result;
}