Bläddra i källkod

feat: home页面调整

BaiLuoYan 1 månad sedan
förälder
incheckning
02913443ca
58 ändrade filer med 869 tillägg och 768 borttagningar
  1. BIN
      src/assets/fonts/barlow-latin-normal.woff2
  2. BIN
      src/assets/fonts/rem-latin-italic.woff2
  3. BIN
      src/assets/fonts/rem-latin-normal.woff2
  4. 21 0
      src/assets/iconify/multi-color/feature-connection.svg
  5. 0 12
      src/assets/iconify/multi-color/feature-coverage.svg
  6. 0 10
      src/assets/iconify/multi-color/feature-encryption.svg
  7. 20 0
      src/assets/iconify/multi-color/feature-private.svg
  8. 20 0
      src/assets/iconify/multi-color/feature-speed-new.svg
  9. 0 10
      src/assets/iconify/multi-color/feature-speed.svg
  10. 23 0
      src/assets/iconify/multi-color/feature-trial.svg
  11. 9 0
      src/assets/iconify/multi-color/logo-union.svg
  12. BIN
      src/assets/images/home/app-access-section.png
  13. BIN
      src/assets/images/home/app-logos-strip.png
  14. 4 0
      src/assets/images/home/btn-android.svg
  15. 27 0
      src/assets/images/home/btn-app-store.svg
  16. 14 0
      src/assets/images/home/btn-google-play.svg
  17. 4 0
      src/assets/images/home/btn-macos.svg
  18. 6 0
      src/assets/images/home/btn-windows.svg
  19. BIN
      src/assets/images/home/devices-mockup.png
  20. BIN
      src/assets/images/home/feature-detail-ip.png
  21. BIN
      src/assets/images/home/feature-detail-privacy.png
  22. BIN
      src/assets/images/home/feature-detail-speed.png
  23. BIN
      src/assets/images/home/hero-app-store.png
  24. BIN
      src/assets/images/home/hero-bg.png
  25. BIN
      src/assets/images/home/hero-devices.png
  26. BIN
      src/assets/images/home/hero-earth.png
  27. BIN
      src/assets/images/home/hero-google-play.png
  28. 43 91
      src/components/Footerbar/index.tsx
  29. 55 35
      src/components/Topbar/index.tsx
  30. 36 25
      src/components/Topbar/useAction.ts
  31. 32 0
      src/hooks/useAuth.ts
  32. 86 0
      src/hooks/useLoginDialog.tsx
  33. 8 3
      src/hooks/useSize.ts
  34. 7 1
      src/locales/en-US/common.ts
  35. 3 2
      src/locales/en-US/components.ts
  36. 38 26
      src/locales/en-US/pages.ts
  37. 7 1
      src/locales/fa-IR/common.ts
  38. 2 1
      src/locales/fa-IR/components.ts
  39. 38 25
      src/locales/fa-IR/pages.ts
  40. 7 1
      src/locales/zh-CN/common.ts
  41. 2 1
      src/locales/zh-CN/components.ts
  42. 38 26
      src/locales/zh-CN/pages.ts
  43. 46 0
      src/pages/home/components/AppAccess.tsx
  44. 71 0
      src/pages/home/components/ChoosePlatform.tsx
  45. 0 62
      src/pages/home/components/Download.tsx
  46. 0 23
      src/pages/home/components/DownloadButton.tsx
  47. 72 0
      src/pages/home/components/FeatureDetails.tsx
  48. 0 69
      src/pages/home/components/Features.tsx
  49. 81 37
      src/pages/home/components/Hero.tsx
  50. 0 76
      src/pages/home/components/Pricing/PlanCard.tsx
  51. 0 54
      src/pages/home/components/Pricing/index.tsx
  52. 0 63
      src/pages/home/components/Pricing/useService.tsx
  53. 17 0
      src/pages/home/components/SectionDivider.tsx
  54. 1 1
      src/pages/home/components/Wrapper.tsx
  55. 16 12
      src/pages/home/index.tsx
  56. 0 26
      src/pages/home/useScrollToCenter.ts
  57. 12 72
      src/pages/pricing/useAction.tsx
  58. 3 3
      src/router/routes.tsx

BIN
src/assets/fonts/barlow-latin-normal.woff2


BIN
src/assets/fonts/rem-latin-italic.woff2


BIN
src/assets/fonts/rem-latin-normal.woff2


+ 21 - 0
src/assets/iconify/multi-color/feature-connection.svg

@@ -0,0 +1,21 @@
+<svg width="72" height="72" viewBox="0 0 72 72" fill="none" xmlns="http://www.w3.org/2000/svg">
+<rect x="0.5" y="0.5" width="71" height="71" rx="7.5" fill="url(#paint0_linear_3835_1337)"/>
+<rect x="0.5" y="0.5" width="71" height="71" rx="7.5" fill="url(#paint1_linear_3835_1337)" fill-opacity="0.2"/>
+<rect x="0.5" y="0.5" width="71" height="71" rx="7.5" stroke="url(#paint2_linear_3835_1337)"/>
+<path d="M36 49.3333C43.3638 49.3333 49.3333 43.3638 49.3333 36C49.3333 28.6362 43.3638 22.6667 36 22.6667C28.6362 22.6667 22.6667 28.6362 22.6667 36C22.6667 43.3638 28.6362 49.3333 36 49.3333Z" stroke="#BEDBFF" style="stroke:#BEDBFF;stroke:color(display-p3 0.7451 0.8588 1.0000);stroke-opacity:1;" stroke-width="2.66667" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M36 28V36L41.3333 38.6667" stroke="#BEDBFF" style="stroke:#BEDBFF;stroke:color(display-p3 0.7451 0.8588 1.0000);stroke-opacity:1;" stroke-width="2.66667" stroke-linecap="round" stroke-linejoin="round"/>
+<defs>
+<linearGradient id="paint0_linear_3835_1337" x1="36" y1="0" x2="36" y2="72" gradientUnits="userSpaceOnUse">
+<stop stop-color="#3C80F6" stop-opacity="0.3" style="stop-color:#3C80F6;stop-color:color(display-p3 0.2353 0.5011 0.9647);stop-opacity:0.3;"/>
+<stop offset="1" stop-color="#242424" stop-opacity="0" style="stop-color:none;stop-opacity:0;"/>
+</linearGradient>
+<linearGradient id="paint1_linear_3835_1337" x1="166.909" y1="-71.5909" x2="6.76648e-06" y2="72" gradientUnits="userSpaceOnUse">
+<stop offset="0.224072" stop-color="#0FA4E9" style="stop-color:#0FA4E9;stop-color:color(display-p3 0.0588 0.6431 0.9137);stop-opacity:1;"/>
+<stop offset="0.687143" stop-color="#0FA4E9" stop-opacity="0" style="stop-color:none;stop-opacity:0;"/>
+</linearGradient>
+<linearGradient id="paint2_linear_3835_1337" x1="36" y1="0" x2="36" y2="72" gradientUnits="userSpaceOnUse">
+<stop stop-color="#3C80F6" style="stop-color:#3C80F6;stop-color:color(display-p3 0.2353 0.5020 0.9647);stop-opacity:1;"/>
+<stop offset="1" stop-color="#2E2E2E" stop-opacity="0" style="stop-color:none;stop-opacity:0;"/>
+</linearGradient>
+</defs>
+</svg>

+ 0 - 12
src/assets/iconify/multi-color/feature-coverage.svg

@@ -1,12 +0,0 @@
-<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M0 14C0 6.26801 6.26801 0 14 0H34C41.732 0 48 6.26801 48 14V34C48 41.732 41.732 48 34 48H14C6.26801 48 0 41.732 0 34V14Z" fill="url(#paint0_linear_1769_2518)"/>
-<path d="M24 34C29.5228 34 34 29.5228 34 24C34 18.4772 29.5228 14 24 14C18.4772 14 14 18.4772 14 24C14 29.5228 18.4772 34 24 34Z" stroke="white" style="stroke:white;stroke-opacity:1;" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M24 14C21.4322 16.6962 20 20.2767 20 24C20 27.7233 21.4322 31.3038 24 34C26.5678 31.3038 28 27.7233 28 24C28 20.2767 26.5678 16.6962 24 14Z" stroke="white" style="stroke:white;stroke-opacity:1;" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M14 24H34" stroke="white" style="stroke:white;stroke-opacity:1;" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
-<defs>
-<linearGradient id="paint0_linear_1769_2518" x1="0" y1="0" x2="48" y2="48" gradientUnits="userSpaceOnUse">
-<stop stop-color="#AD46FF" style="stop-color:#AD46FF;stop-color:color(display-p3 0.6779 0.2759 1.0000);stop-opacity:1;"/>
-<stop offset="1" stop-color="#F6339A" style="stop-color:#F6339A;stop-color:color(display-p3 0.9658 0.1981 0.6043);stop-opacity:1;"/>
-</linearGradient>
-</defs>
-</svg>

+ 0 - 10
src/assets/iconify/multi-color/feature-encryption.svg

@@ -1,10 +0,0 @@
-<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M0 14C0 6.26801 6.26801 0 14 0H34C41.732 0 48 6.26801 48 14V34C48 41.732 41.732 48 34 48H14C6.26801 48 0 41.732 0 34V14Z" fill="url(#paint0_linear_1769_2528)"/>
-<path d="M32 25C32 30 28.5 32.5 24.34 33.95C24.1222 34.0238 23.8855 34.0202 23.67 33.94C19.5 32.5 16 30 16 25V18C16 17.7347 16.1054 17.4804 16.2929 17.2929C16.4804 17.1053 16.7348 17 17 17C19 17 21.5 15.8 23.24 14.28C23.4519 14.099 23.7214 13.9995 24 13.9995C24.2786 13.9995 24.5481 14.099 24.76 14.28C26.51 15.81 29 17 31 17C31.2652 17 31.5196 17.1053 31.7071 17.2929C31.8946 17.4804 32 17.7347 32 18V25Z" stroke="white" style="stroke:white;stroke-opacity:1;" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
-<defs>
-<linearGradient id="paint0_linear_1769_2528" x1="0" y1="0" x2="48" y2="48" gradientUnits="userSpaceOnUse">
-<stop stop-color="#46FFBE" style="stop-color:#46FFBE;stop-color:color(display-p3 0.2759 1.0000 0.7466);stop-opacity:1;"/>
-<stop offset="1" stop-color="#F6D233" style="stop-color:#F6D233;stop-color:color(display-p3 0.9658 0.8251 0.1981);stop-opacity:1;"/>
-</linearGradient>
-</defs>
-</svg>

+ 20 - 0
src/assets/iconify/multi-color/feature-private.svg

@@ -0,0 +1,20 @@
+<svg width="72" height="72" viewBox="0 0 72 72" fill="none" xmlns="http://www.w3.org/2000/svg">
+<rect x="0.5" y="0.5" width="71" height="71" rx="7.5" fill="url(#paint0_linear_3835_1368)"/>
+<rect x="0.5" y="0.5" width="71" height="71" rx="7.5" fill="url(#paint1_linear_3835_1368)" fill-opacity="0.2"/>
+<rect x="0.5" y="0.5" width="71" height="71" rx="7.5" stroke="url(#paint2_linear_3835_1368)"/>
+<path d="M46.6666 37.3333C46.6666 44 42 47.3333 36.4533 49.2667C36.1629 49.3651 35.8474 49.3604 35.56 49.2533C30 47.3333 25.3333 44 25.3333 37.3333V28C25.3333 27.6464 25.4738 27.3072 25.7238 27.0572C25.9739 26.8071 26.313 26.6667 26.6666 26.6667C29.3333 26.6667 32.6666 25.0667 34.9866 23.04C35.2691 22.7987 35.6285 22.6661 36 22.6661C36.3715 22.6661 36.7308 22.7987 37.0133 23.04C39.3466 25.08 42.6666 26.6667 45.3333 26.6667C45.6869 26.6667 46.0261 26.8071 46.2761 27.0572C46.5262 27.3072 46.6666 27.6464 46.6666 28V37.3333Z" stroke="#BEDBFF" style="stroke:#BEDBFF;stroke:color(display-p3 0.7451 0.8588 1.0000);stroke-opacity:1;" stroke-width="2.66667" stroke-linecap="round" stroke-linejoin="round"/>
+<defs>
+<linearGradient id="paint0_linear_3835_1368" x1="36" y1="0" x2="36" y2="72" gradientUnits="userSpaceOnUse">
+<stop stop-color="#3C80F6" stop-opacity="0.3" style="stop-color:#3C80F6;stop-color:color(display-p3 0.2353 0.5011 0.9647);stop-opacity:0.3;"/>
+<stop offset="1" stop-color="#242424" stop-opacity="0" style="stop-color:none;stop-opacity:0;"/>
+</linearGradient>
+<linearGradient id="paint1_linear_3835_1368" x1="166.909" y1="-71.5909" x2="6.76648e-06" y2="72" gradientUnits="userSpaceOnUse">
+<stop offset="0.224072" stop-color="#0FA4E9" style="stop-color:#0FA4E9;stop-color:color(display-p3 0.0588 0.6431 0.9137);stop-opacity:1;"/>
+<stop offset="0.687143" stop-color="#0FA4E9" stop-opacity="0" style="stop-color:none;stop-opacity:0;"/>
+</linearGradient>
+<linearGradient id="paint2_linear_3835_1368" x1="36" y1="0" x2="36" y2="72" gradientUnits="userSpaceOnUse">
+<stop stop-color="#3C80F6" style="stop-color:#3C80F6;stop-color:color(display-p3 0.2353 0.5020 0.9647);stop-opacity:1;"/>
+<stop offset="1" stop-color="#2E2E2E" stop-opacity="0" style="stop-color:none;stop-opacity:0;"/>
+</linearGradient>
+</defs>
+</svg>

+ 20 - 0
src/assets/iconify/multi-color/feature-speed-new.svg

@@ -0,0 +1,20 @@
+<svg width="72" height="72" viewBox="0 0 72 72" fill="none" xmlns="http://www.w3.org/2000/svg">
+<rect x="0.5" y="0.5" width="71" height="71" rx="7.5" fill="url(#paint0_linear_3835_1359)"/>
+<rect x="0.5" y="0.5" width="71" height="71" rx="7.5" fill="url(#paint1_linear_3835_1359)" fill-opacity="0.2"/>
+<rect x="0.5" y="0.5" width="71" height="71" rx="7.5" stroke="url(#paint2_linear_3835_1359)"/>
+<path d="M25.3333 38.6667C25.081 38.6675 24.8336 38.5968 24.6199 38.4626C24.4062 38.3285 24.235 38.1365 24.1261 37.9089C24.0172 37.6813 23.9751 37.4274 24.0047 37.1769C24.0343 36.9263 24.1344 36.6893 24.2933 36.4933L37.4933 22.8933C37.5923 22.779 37.7273 22.7018 37.876 22.6743C38.0247 22.6468 38.1783 22.6707 38.3116 22.742C38.445 22.8133 38.5501 22.9279 38.6098 23.0668C38.6694 23.2058 38.6801 23.3609 38.64 23.5067L36.08 31.5333C36.0045 31.7354 35.9792 31.9527 36.0061 32.1667C36.0331 32.3807 36.1115 32.5849 36.2348 32.7619C36.358 32.9389 36.5224 33.0834 36.7137 33.1829C36.905 33.2824 37.1177 33.334 37.3333 33.3333H46.6667C46.919 33.3325 47.1664 33.4032 47.3801 33.5374C47.5938 33.6715 47.765 33.8635 47.8739 34.0911C47.9828 34.3187 48.0249 34.5726 47.9953 34.8231C47.9657 35.0737 47.8656 35.3107 47.7067 35.5067L34.5067 49.1067C34.4077 49.221 34.2727 49.2982 34.124 49.3257C33.9753 49.3532 33.8217 49.3293 33.6884 49.258C33.555 49.1867 33.4499 49.0721 33.3902 48.9332C33.3306 48.7942 33.3199 48.6391 33.36 48.4933L35.92 40.4667C35.9955 40.2646 36.0208 40.0473 35.9939 39.8333C35.9669 39.6193 35.8885 39.4151 35.7652 39.2381C35.642 39.0611 35.4776 38.9166 35.2863 38.8171C35.095 38.7176 34.8823 38.666 34.6667 38.6667H25.3333Z" stroke="#BEDBFF" style="stroke:#BEDBFF;stroke:color(display-p3 0.7451 0.8588 1.0000);stroke-opacity:1;" stroke-width="2.66667" stroke-linecap="round" stroke-linejoin="round"/>
+<defs>
+<linearGradient id="paint0_linear_3835_1359" x1="36" y1="0" x2="36" y2="72" gradientUnits="userSpaceOnUse">
+<stop stop-color="#3C80F6" stop-opacity="0.3" style="stop-color:#3C80F6;stop-color:color(display-p3 0.2353 0.5011 0.9647);stop-opacity:0.3;"/>
+<stop offset="1" stop-color="#242424" stop-opacity="0" style="stop-color:none;stop-opacity:0;"/>
+</linearGradient>
+<linearGradient id="paint1_linear_3835_1359" x1="166.909" y1="-71.5909" x2="6.76648e-06" y2="72" gradientUnits="userSpaceOnUse">
+<stop offset="0.224072" stop-color="#0FA4E9" style="stop-color:#0FA4E9;stop-color:color(display-p3 0.0588 0.6431 0.9137);stop-opacity:1;"/>
+<stop offset="0.687143" stop-color="#0FA4E9" stop-opacity="0" style="stop-color:none;stop-opacity:0;"/>
+</linearGradient>
+<linearGradient id="paint2_linear_3835_1359" x1="36" y1="0" x2="36" y2="72" gradientUnits="userSpaceOnUse">
+<stop stop-color="#3C80F6" style="stop-color:#3C80F6;stop-color:color(display-p3 0.2353 0.5020 0.9647);stop-opacity:1;"/>
+<stop offset="1" stop-color="#2E2E2E" stop-opacity="0" style="stop-color:none;stop-opacity:0;"/>
+</linearGradient>
+</defs>
+</svg>

+ 0 - 10
src/assets/iconify/multi-color/feature-speed.svg

@@ -1,10 +0,0 @@
-<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M0 14C0 6.26801 6.26801 0 14 0H34C41.732 0 48 6.26801 48 14V34C48 41.732 41.732 48 34 48H14C6.26801 48 0 41.732 0 34V14Z" fill="url(#paint0_linear_1769_2510)"/>
-<path d="M16.0005 26C15.8112 26.0007 15.6257 25.9476 15.4654 25.847C15.3052 25.7464 15.1767 25.6024 15.095 25.4317C15.0133 25.261 14.9818 25.0706 15.004 24.8827C15.0262 24.6948 15.1013 24.517 15.2205 24.37L25.1205 14.17C25.1947 14.0843 25.2959 14.0264 25.4075 14.0058C25.519 13.9852 25.6342 14.0031 25.7342 14.0565C25.8342 14.11 25.9131 14.1959 25.9578 14.3001C26.0026 14.4044 26.0106 14.5207 25.9805 14.63L24.0605 20.65C24.0039 20.8016 23.9849 20.9646 24.0051 21.125C24.0253 21.2855 24.0841 21.4387 24.1766 21.5715C24.269 21.7042 24.3923 21.8126 24.5358 21.8872C24.6793 21.9618 24.8387 22.0006 25.0005 22H32.0005C32.1897 21.9994 32.3752 22.0525 32.5355 22.1531C32.6958 22.2537 32.8242 22.3977 32.9059 22.5684C32.9876 22.7391 33.0192 22.9295 32.997 23.1174C32.9748 23.3053 32.8997 23.4831 32.7805 23.63L22.8805 33.83C22.8062 33.9158 22.705 33.9737 22.5935 33.9943C22.482 34.0149 22.3668 33.997 22.2668 33.9435C22.1667 33.89 22.0879 33.8041 22.0431 33.6999C21.9984 33.5957 21.9904 33.4794 22.0205 33.37L23.9405 27.35C23.9971 27.1985 24.0161 27.0355 23.9959 26.875C23.9757 26.7145 23.9168 26.5614 23.8244 26.4286C23.732 26.2959 23.6087 26.1875 23.4652 26.1129C23.3217 26.0382 23.1622 25.9995 23.0005 26H16.0005Z" stroke="white" style="stroke:white;stroke-opacity:1;" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
-<defs>
-<linearGradient id="paint0_linear_1769_2510" x1="0" y1="0" x2="48" y2="48" gradientUnits="userSpaceOnUse">
-<stop stop-color="#467BFF" style="stop-color:#467BFF;stop-color:color(display-p3 0.2759 0.4811 1.0000);stop-opacity:1;"/>
-<stop offset="1" stop-color="#33DFF6" style="stop-color:#33DFF6;stop-color:color(display-p3 0.1981 0.8762 0.9658);stop-opacity:1;"/>
-</linearGradient>
-</defs>
-</svg>

+ 23 - 0
src/assets/iconify/multi-color/feature-trial.svg

@@ -0,0 +1,23 @@
+<svg width="72" height="72" viewBox="0 0 72 72" fill="none" xmlns="http://www.w3.org/2000/svg">
+<rect x="0.5" y="0.5" width="71" height="71" rx="7.5" fill="url(#paint0_linear_3835_1347)"/>
+<rect x="0.5" y="0.5" width="71" height="71" rx="7.5" fill="url(#paint1_linear_3835_1347)" fill-opacity="0.2"/>
+<rect x="0.5" y="0.5" width="71" height="71" rx="7.5" stroke="url(#paint2_linear_3835_1347)"/>
+<path d="M46.6667 30.6667H25.3333C24.597 30.6667 24 31.2636 24 32V34.6667C24 35.403 24.597 36 25.3333 36H46.6667C47.403 36 48 35.403 48 34.6667V32C48 31.2636 47.403 30.6667 46.6667 30.6667Z" stroke="#BEDBFF" style="stroke:#BEDBFF;stroke:color(display-p3 0.7451 0.8588 1.0000);stroke-opacity:1;" stroke-width="2.66667" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M36 30.6667V48" stroke="#BEDBFF" style="stroke:#BEDBFF;stroke:color(display-p3 0.7451 0.8588 1.0000);stroke-opacity:1;" stroke-width="2.66667" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M45.3333 36V45.3333C45.3333 46.0406 45.0524 46.7189 44.5523 47.219C44.0522 47.719 43.3739 48 42.6667 48H29.3333C28.6261 48 27.9478 47.719 27.4477 47.219C26.9476 46.7189 26.6667 46.0406 26.6667 45.3333V36" stroke="#BEDBFF" style="stroke:#BEDBFF;stroke:color(display-p3 0.7451 0.8588 1.0000);stroke-opacity:1;" stroke-width="2.66667" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M30 30.6667C29.116 30.6667 28.2681 30.3155 27.643 29.6904C27.0179 29.0652 26.6667 28.2174 26.6667 27.3333C26.6667 26.4493 27.0179 25.6014 27.643 24.9763C28.2681 24.3512 29.116 24 30 24C31.2863 23.9776 32.5467 24.6017 33.617 25.7909C34.6873 26.9801 35.5177 28.6792 36 30.6667C36.4823 28.6792 37.3127 26.9801 38.383 25.7909C39.4533 24.6017 40.7138 23.9776 42 24C42.8841 24 43.7319 24.3512 44.357 24.9763C44.9821 25.6014 45.3333 26.4493 45.3333 27.3333C45.3333 28.2174 44.9821 29.0652 44.357 29.6904C43.7319 30.3155 42.8841 30.6667 42 30.6667" stroke="#BEDBFF" style="stroke:#BEDBFF;stroke:color(display-p3 0.7451 0.8588 1.0000);stroke-opacity:1;" stroke-width="2.66667" stroke-linecap="round" stroke-linejoin="round"/>
+<defs>
+<linearGradient id="paint0_linear_3835_1347" x1="36" y1="0" x2="36" y2="72" gradientUnits="userSpaceOnUse">
+<stop stop-color="#3C80F6" stop-opacity="0.3" style="stop-color:#3C80F6;stop-color:color(display-p3 0.2353 0.5011 0.9647);stop-opacity:0.3;"/>
+<stop offset="1" stop-color="#242424" stop-opacity="0" style="stop-color:none;stop-opacity:0;"/>
+</linearGradient>
+<linearGradient id="paint1_linear_3835_1347" x1="166.909" y1="-71.5909" x2="6.76648e-06" y2="72" gradientUnits="userSpaceOnUse">
+<stop offset="0.224072" stop-color="#0FA4E9" style="stop-color:#0FA4E9;stop-color:color(display-p3 0.0588 0.6431 0.9137);stop-opacity:1;"/>
+<stop offset="0.687143" stop-color="#0FA4E9" stop-opacity="0" style="stop-color:none;stop-opacity:0;"/>
+</linearGradient>
+<linearGradient id="paint2_linear_3835_1347" x1="36" y1="0" x2="36" y2="72" gradientUnits="userSpaceOnUse">
+<stop stop-color="#3C80F6" style="stop-color:#3C80F6;stop-color:color(display-p3 0.2353 0.5020 0.9647);stop-opacity:1;"/>
+<stop offset="1" stop-color="#2E2E2E" stop-opacity="0" style="stop-color:none;stop-opacity:0;"/>
+</linearGradient>
+</defs>
+</svg>

+ 9 - 0
src/assets/iconify/multi-color/logo-union.svg

@@ -0,0 +1,9 @@
+<svg width="33" height="32" viewBox="0 0 33 32" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path fill-rule="evenodd" clip-rule="evenodd" d="M17.2764 0C25.5409 0.638812 32.0477 7.54724 32.0479 15.9756C32.0479 24.8254 24.8742 31.9999 16.0244 32C7.17452 32 6.01674e-08 24.8255 0 15.9756C0.00013378 9.30827 4.07244 3.59257 9.86523 1.17871V6.83984C6.93442 8.81948 5.00792 12.1727 5.00781 15.9756C5.00781 19.7785 6.93451 23.1316 9.86523 25.1113V9.08789L17.2764 13.4053V0ZM22.2842 22.1172L14.873 17.7998V26.9277C15.2515 26.9681 15.6354 26.9922 16.0244 26.9922C22.1086 26.992 27.041 22.0598 27.041 15.9756C27.0409 12.2171 25.1576 8.90043 22.2842 6.91309V22.1172Z" fill="url(#paint0_linear_3842_195)"/>
+<defs>
+<linearGradient id="paint0_linear_3842_195" x1="16.0239" y1="1.61616" x2="16.0239" y2="8.80807" gradientUnits="userSpaceOnUse">
+<stop stop-color="#0EA5E9" style="stop-color:#0EA5E9;stop-color:color(display-p3 0.0549 0.6471 0.9137);stop-opacity:1;"/>
+<stop offset="1" stop-color="#3B82F6" style="stop-color:#3B82F6;stop-color:color(display-p3 0.2314 0.5098 0.9647);stop-opacity:1;"/>
+</linearGradient>
+</defs>
+</svg>

BIN
src/assets/images/home/app-access-section.png


BIN
src/assets/images/home/app-logos-strip.png


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 4 - 0
src/assets/images/home/btn-android.svg


+ 27 - 0
src/assets/images/home/btn-app-store.svg

@@ -0,0 +1,27 @@
+<svg width="150" height="51" viewBox="0 0 150 51" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M138.055 0.000162956H11.9518C11.4921 0.000162956 11.038 0.000162956 10.5796 0.00266997C10.1958 0.00517699 9.81509 0.0124599 9.42764 0.0185895C8.58593 0.0284945 7.74619 0.102545 6.91573 0.240097C6.08644 0.380644 5.28313 0.645615 4.53296 1.02605C3.78372 1.4097 3.09911 1.90821 2.50397 2.50351C1.90571 3.09713 1.40702 3.78327 1.02706 4.53556C0.646059 5.28635 0.38187 6.09089 0.243619 6.92139C0.104055 7.75084 0.028954 8.58986 0.0189907 9.43091C0.0073581 9.81524 0.00612966 10.2008 0 10.5852V39.5592C0.00612966 39.9484 0.0073581 40.3254 0.0189907 40.7148C0.0289571 41.5558 0.104058 42.3947 0.243619 43.2242C0.381488 44.0551 0.645693 44.8601 1.02706 45.6112C1.40685 46.3611 1.90561 47.0444 2.50397 47.6348C3.09685 48.2327 3.7819 48.7316 4.53296 49.1123C5.28312 49.4937 6.08635 49.7603 6.91573 49.903C7.74633 50.0394 8.58598 50.1135 9.42764 50.1246C9.81509 50.1331 10.1958 50.138 10.5796 50.138C11.038 50.1405 11.4922 50.1405 11.9518 50.1405H138.055C138.505 50.1405 138.963 50.1405 139.414 50.138C139.796 50.138 140.187 50.1331 140.569 50.1246C141.409 50.1141 142.247 50.04 143.076 49.903C143.908 49.7593 144.715 49.4928 145.468 49.1123C146.219 48.7313 146.903 48.2325 147.495 47.6348C148.092 47.0421 148.592 46.3593 148.977 45.6112C149.355 44.8596 149.617 44.0547 149.753 43.2242C149.892 42.3946 149.97 41.5558 149.985 40.7148C149.99 40.3254 149.99 39.9484 149.99 39.5592C150 39.1038 150 38.6509 150 38.1881V11.9538C150 11.4947 150 11.0394 149.99 10.5852C149.99 10.2008 149.99 9.81524 149.985 9.43086C149.97 8.58974 149.892 7.75091 149.753 6.92134C149.616 6.09132 149.355 5.28684 148.977 4.53551C148.203 3.02763 146.976 1.80023 145.468 1.02593C144.715 0.646431 143.908 0.381531 143.076 0.239984C142.248 0.101826 141.409 0.0277499 140.569 0.018414C140.187 0.0122969 139.796 0.00495136 139.414 0.00250702C138.963 0 138.505 0 138.055 0V0.000162956Z" fill="#A6A6A6" style="fill:#A6A6A6;fill:color(display-p3 0.6510 0.6510 0.6510);fill-opacity:1;"/>
+<path d="M10.5851 49.0411C10.2032 49.0411 9.83048 49.0362 9.45156 49.0277C8.66659 49.0174 7.88345 48.9491 7.10857 48.8232C6.38604 48.6988 5.6861 48.4673 5.03184 48.1364C4.38357 47.8083 3.79231 47.378 3.28068 46.8621C2.76166 46.3523 2.32954 45.761 2.00147 45.1116C1.66977 44.458 1.44022 43.7574 1.32081 43.0343C1.19186 42.2572 1.12208 41.4715 1.1121 40.6839C1.10415 40.4196 1.09375 39.5394 1.09375 39.5394V10.5826C1.09375 10.5826 1.10483 9.71591 1.11216 9.46128C1.12172 8.67493 1.19109 7.89045 1.31966 7.11463C1.43929 6.3895 1.66902 5.6869 2.00089 5.03117C2.32776 4.38224 2.75748 3.79048 3.27338 3.27883C3.7887 2.76231 4.38186 2.32982 5.03122 1.99715C5.68398 1.66733 6.3826 1.43749 7.10367 1.31532C7.8811 1.18817 8.66695 1.11943 9.45464 1.10967L10.5857 1.09436H139.407L140.552 1.11028C141.332 1.11955 142.111 1.18768 142.881 1.31409C143.609 1.43779 144.315 1.66924 144.976 2.00082C146.277 2.67126 147.335 3.73195 148.003 5.03423C148.329 5.68544 148.556 6.3823 148.674 7.10116C148.804 7.88336 148.877 8.674 148.892 9.4668C148.895 9.82179 148.895 10.2031 148.895 10.5826C148.905 11.0527 148.905 11.5001 148.905 11.9512V38.1854C148.905 38.6408 148.905 39.0852 148.895 39.5332C148.895 39.9409 148.895 40.3143 148.89 40.6986C148.876 41.4773 148.804 42.2538 148.676 43.022C148.559 43.7504 148.331 44.4564 147.999 45.1153C147.669 45.7577 147.239 46.3439 146.726 46.8523C146.214 47.371 145.622 47.8038 144.972 48.134C144.314 48.4675 143.609 48.6998 142.881 48.8232C142.106 48.9498 141.323 49.0181 140.538 49.0277C140.171 49.0362 139.786 49.0411 139.413 49.0411L138.054 49.0436L10.5851 49.0411Z" fill="black" style="fill:black;fill-opacity:1;"/>
+<path d="M31.0486 25.4474C31.062 24.4011 31.34 23.3753 31.8564 22.4653C32.3729 21.5553 33.1112 20.7907 34.0025 20.2427C33.4363 19.434 32.6893 18.7685 31.8208 18.299C30.9523 17.8295 29.9863 17.5689 28.9996 17.538C26.8946 17.3171 24.854 18.7976 23.7813 18.7976C22.688 18.7976 21.0365 17.56 19.2581 17.5966C18.1077 17.6337 16.9867 17.9682 16.0041 18.5675C15.0215 19.1667 14.2109 20.0103 13.6512 21.016C11.2269 25.2133 13.0352 31.3818 15.3575 34.7746C16.5194 36.4359 17.8773 38.2917 19.6541 38.2259C21.3927 38.1538 22.0421 37.1172 24.1409 37.1172C26.2201 37.1172 26.8294 38.2259 28.6422 38.184C30.5079 38.1538 31.6834 36.5153 32.8045 34.8382C33.6393 33.6545 34.2817 32.3462 34.7079 30.9618C33.6239 30.5033 32.699 29.7359 32.0482 28.7553C31.3975 27.7747 31.0498 26.6242 31.0486 25.4474Z" fill="white" style="fill:white;fill-opacity:1;"/>
+<path d="M27.6243 15.3069C28.6415 14.0857 29.1427 12.5161 29.0213 10.9314C27.4672 11.0946 26.0316 11.8374 25.0006 13.0117C24.4965 13.5854 24.1104 14.2528 23.8644 14.9758C23.6185 15.6988 23.5174 16.4632 23.567 17.2253C24.3443 17.2333 25.1133 17.0648 25.8161 16.7325C26.5189 16.4002 27.1372 15.9128 27.6243 15.3069Z" fill="white" style="fill:white;fill-opacity:1;"/>
+<path d="M53.0281 34.0168H47.0948L45.6699 38.2242H43.1567L48.7767 22.6581H51.3878L57.0078 38.2242H54.4518L53.0281 34.0168ZM47.7093 32.0753H52.4124L50.0939 25.2471H50.0291L47.7093 32.0753Z" fill="white" style="fill:white;fill-opacity:1;"/>
+<path d="M69.1474 32.5482C69.1474 36.075 67.2598 38.3408 64.4112 38.3408C63.6896 38.3786 62.972 38.2123 62.3405 37.8612C61.709 37.51 61.1891 36.9881 60.8405 36.3553H60.7866V41.9765H58.457V26.8732H60.7119V28.7608H60.7548C61.1195 28.1309 61.6481 27.6117 62.2844 27.2584C62.9208 26.9051 63.641 26.731 64.3684 26.7544C67.2487 26.7544 69.1474 29.0313 69.1474 32.5482ZM66.753 32.5482C66.753 30.2505 65.5656 28.74 63.7539 28.74C61.974 28.74 60.7768 30.2824 60.7768 32.5482C60.7768 34.8349 61.974 36.3663 63.7539 36.3663C65.5656 36.3663 66.753 34.8667 66.753 32.5482Z" fill="white" style="fill:white;fill-opacity:1;"/>
+<path d="M81.6376 32.5482C81.6376 36.0749 79.75 38.3408 76.9015 38.3408C76.1799 38.3785 75.4623 38.2123 74.8307 37.8612C74.1992 37.51 73.6794 36.9881 73.3307 36.3552H73.2768V41.9765H70.9473V26.8731H73.2021V28.7608H73.2449C73.6097 28.1309 74.1383 27.6117 74.7746 27.2584C75.4109 26.9051 76.1311 26.7309 76.8586 26.7544C79.739 26.7544 81.6376 29.0313 81.6376 32.5482ZM79.2432 32.5482C79.2432 30.2505 78.0558 28.7399 76.2441 28.7399C74.4642 28.7399 73.267 30.2824 73.267 32.5482C73.267 34.8349 74.4642 36.3663 76.2441 36.3663C78.0558 36.3663 79.2432 34.8667 79.2432 32.5482H79.2432Z" fill="white" style="fill:white;fill-opacity:1;"/>
+<path d="M89.892 33.8866C90.0646 35.4302 91.5641 36.4438 93.6133 36.4438C95.5768 36.4438 96.9895 35.4302 96.9895 34.0383C96.9895 32.8301 96.1375 32.1067 94.1201 31.6109L92.1028 31.1249C89.2444 30.4345 87.9174 29.0977 87.9174 26.9286C87.9174 24.2428 90.258 22.3981 93.5815 22.3981C96.8707 22.3981 99.1256 24.2428 99.2015 26.9286H96.8499C96.7091 25.3752 95.425 24.4375 93.5484 24.4375C91.6718 24.4375 90.3877 25.3862 90.3877 26.767C90.3877 27.8675 91.2079 28.5151 93.2142 29.0108L94.9292 29.4319C98.123 30.1872 99.4499 31.4701 99.4499 33.7469C99.4499 36.6591 97.1302 38.4831 93.4407 38.4831C89.9886 38.4831 87.6579 36.702 87.5073 33.8865L89.892 33.8866Z" fill="white" style="fill:white;fill-opacity:1;"/>
+<path d="M104.476 24.1873V26.8731H106.635V28.7178H104.476V34.9743C104.476 35.9463 104.909 36.3992 105.857 36.3992C106.114 36.3948 106.369 36.3768 106.624 36.3453V38.1791C106.197 38.2588 105.764 38.2949 105.33 38.2868C103.032 38.2868 102.136 37.4238 102.136 35.2228V28.7178H100.486V26.8731H102.136V24.1873H104.476Z" fill="white" style="fill:white;fill-opacity:1;"/>
+<path d="M107.886 32.5483C107.886 28.9775 109.989 26.7337 113.268 26.7337C116.559 26.7337 118.652 28.9775 118.652 32.5483C118.652 36.1289 116.57 38.3629 113.268 38.3629C109.968 38.3629 107.886 36.1289 107.886 32.5483ZM116.278 32.5483C116.278 30.0988 115.156 28.6531 113.268 28.6531C111.381 28.6531 110.259 30.1099 110.259 32.5483C110.259 35.0076 111.381 36.4423 113.268 36.4423C115.156 36.4423 116.278 35.0076 116.278 32.5483H116.278Z" fill="white" style="fill:white;fill-opacity:1;"/>
+<path d="M120.574 26.8731H122.796V28.8047H122.849C123 28.2014 123.353 27.6683 123.85 27.2949C124.347 26.9215 124.958 26.7306 125.579 26.7543C125.848 26.7534 126.115 26.7825 126.377 26.8412V29.0202C126.038 28.9166 125.685 28.8691 125.331 28.8794C124.992 28.8657 124.655 28.9253 124.342 29.0543C124.029 29.1833 123.747 29.3785 123.516 29.6265C123.286 29.8746 123.112 30.1696 123.006 30.4914C122.9 30.8131 122.865 31.1539 122.903 31.4905V38.2219H120.574L120.574 26.8731Z" fill="white" style="fill:white;fill-opacity:1;"/>
+<path d="M137.115 34.8889C136.801 36.9491 134.795 38.3629 132.228 38.3629C128.927 38.3629 126.877 36.1509 126.877 32.6022C126.877 29.0424 128.938 26.7337 132.13 26.7337C135.27 26.7337 137.245 28.8906 137.245 32.3316V33.1298H129.229V33.2706C129.192 33.6882 129.244 34.109 129.382 34.5049C129.521 34.9008 129.741 35.2628 130.03 35.5668C130.319 35.8709 130.669 36.11 131.057 36.2683C131.445 36.4266 131.863 36.5005 132.282 36.4851C132.832 36.5367 133.385 36.4092 133.857 36.1216C134.329 35.834 134.696 35.4016 134.903 34.8888L137.115 34.8889ZM129.24 31.5017H134.914C134.935 31.1261 134.878 30.7503 134.747 30.3978C134.615 30.0453 134.413 29.7238 134.151 29.4533C133.89 29.1828 133.576 28.9693 133.228 28.8261C132.88 28.683 132.506 28.6132 132.13 28.6213C131.751 28.619 131.375 28.6919 131.024 28.8358C130.673 28.9796 130.354 29.1915 130.085 29.4593C129.816 29.7271 129.603 30.0455 129.458 30.396C129.313 30.7465 129.239 31.1223 129.24 31.5017V31.5017Z" fill="white" style="fill:white;fill-opacity:1;"/>
+<path d="M47.4184 10.946C47.9068 10.9109 48.3969 10.9847 48.8533 11.162C49.3097 11.3393 49.7211 11.6157 50.0578 11.9712C50.3945 12.3267 50.648 12.7525 50.8002 13.2179C50.9524 13.6833 50.9994 14.1767 50.9378 14.6624C50.9378 17.0519 49.6464 18.4255 47.4184 18.4255H44.7168V10.946H47.4184ZM45.8785 17.3677H47.2887C47.6377 17.3886 47.9869 17.3314 48.311 17.2002C48.6351 17.0691 48.9258 16.8673 49.1621 16.6097C49.3984 16.352 49.5742 16.0449 49.6769 15.7106C49.7795 15.3764 49.8063 15.0236 49.7553 14.6777C49.8026 14.3332 49.7731 13.9825 49.6689 13.6507C49.5647 13.319 49.3884 13.0144 49.1527 12.7587C48.9169 12.5031 48.6275 12.3028 48.3053 12.1721C47.983 12.0415 47.6359 11.9837 47.2887 12.003H45.8785V17.3677Z" fill="white" style="fill:white;fill-opacity:1;"/>
+<path d="M52.2489 15.5987C52.2134 15.2277 52.2559 14.8535 52.3735 14.4999C52.4911 14.1463 52.6814 13.8212 52.932 13.5455C53.1826 13.2697 53.4881 13.0494 53.8289 12.8986C54.1696 12.7479 54.5382 12.67 54.9108 12.67C55.2834 12.67 55.6519 12.7479 55.9927 12.8986C56.3335 13.0494 56.639 13.2697 56.8896 13.5455C57.1402 13.8212 57.3305 14.1463 57.4481 14.4999C57.5657 14.8535 57.6081 15.2277 57.5727 15.5987C57.6088 15.97 57.5669 16.3447 57.4496 16.6989C57.3323 17.053 57.1421 17.3787 56.8914 17.655C56.6408 17.9313 56.335 18.1521 55.9939 18.3032C55.6528 18.4543 55.2839 18.5323 54.9108 18.5323C54.5377 18.5323 54.1688 18.4543 53.8277 18.3032C53.4866 18.1521 53.1808 17.9313 52.9301 17.655C52.6794 17.3787 52.4893 17.053 52.372 16.6989C52.2547 16.3447 52.2128 15.97 52.2489 15.5987ZM56.4269 15.5987C56.4269 14.3751 55.8772 13.6596 54.9126 13.6596C53.9443 13.6596 53.3996 14.3751 53.3996 15.5987C53.3996 16.832 53.9444 17.542 54.9126 17.542C55.8773 17.542 56.4269 16.8271 56.4269 15.5987H56.4269Z" fill="white" style="fill:white;fill-opacity:1;"/>
+<path d="M64.6513 18.4237H63.4958L62.3292 14.2665H62.241L61.0793 18.4237H59.9348L58.3789 12.7792H59.5088L60.5199 17.0863H60.6032L61.7636 12.7792H62.8323L63.9928 17.0863H64.0809L65.0871 12.7792H66.2011L64.6513 18.4237Z" fill="white" style="fill:white;fill-opacity:1;"/>
+<path d="M67.5073 12.7805H68.5797V13.6772H68.6629C68.8041 13.3551 69.0423 13.0852 69.3442 12.9049C69.6462 12.7247 69.9968 12.6431 70.3473 12.6716C70.622 12.6509 70.8977 12.6923 71.1542 12.7927C71.4107 12.8931 71.6412 13.05 71.8289 13.2516C72.0165 13.4532 72.1564 13.6944 72.2381 13.9575C72.3198 14.2205 72.3413 14.4985 72.301 14.7709V18.4249H71.1871V15.0507C71.1871 14.1436 70.7929 13.6925 69.9691 13.6925C69.7826 13.6838 69.5964 13.7156 69.4234 13.7855C69.2503 13.8555 69.0945 13.9621 68.9664 14.098C68.8384 14.2338 68.7413 14.3958 68.6817 14.5727C68.6221 14.7496 68.6015 14.9373 68.6213 15.1229V18.425H67.5073L67.5073 12.7805Z" fill="white" style="fill:white;fill-opacity:1;"/>
+<path d="M74.0771 10.5756H75.1911V18.4235H74.0771V10.5756Z" fill="white" style="fill:white;fill-opacity:1;"/>
+<path d="M76.7386 15.5988C76.7032 15.2278 76.7456 14.8535 76.8633 14.4999C76.981 14.1463 77.1712 13.8212 77.4219 13.5455C77.6725 13.2697 77.978 13.0494 78.3188 12.8986C78.6596 12.7479 79.0282 12.67 79.4008 12.67C79.7735 12.67 80.142 12.7479 80.4828 12.8986C80.8236 13.0494 81.1291 13.2697 81.3798 13.5455C81.6304 13.8212 81.8207 14.1463 81.9383 14.4999C82.056 14.8535 82.0984 15.2278 82.063 15.5988C82.0991 15.9701 82.0571 16.3449 81.9398 16.699C81.8224 17.0532 81.6323 17.3788 81.3816 17.6551C81.1308 17.9314 80.8251 18.1522 80.484 18.3033C80.1429 18.4544 79.7739 18.5324 79.4008 18.5324C79.0277 18.5324 78.6588 18.4544 78.3176 18.3033C77.9765 18.1522 77.6708 17.9314 77.4201 17.6551C77.1693 17.3788 76.9792 17.0532 76.8618 16.699C76.7445 16.3449 76.7025 15.9701 76.7386 15.5988ZM80.9166 15.5988C80.9166 14.3752 80.3669 13.6597 79.4023 13.6597C78.4341 13.6597 77.8894 14.3752 77.8894 15.5988C77.8894 16.8321 78.4341 17.5421 79.4023 17.5421C80.367 17.5421 80.9166 16.8272 80.9166 15.5988H80.9166Z" fill="white" style="fill:white;fill-opacity:1;"/>
+<path d="M83.2397 16.8271C83.2397 15.8111 83.9963 15.2253 85.3391 15.1421L86.8681 15.054V14.5668C86.8681 13.9706 86.4739 13.634 85.7125 13.634C85.0906 13.634 84.6597 13.8623 84.5361 14.2614H83.4576C83.5715 13.2918 84.4835 12.67 85.7639 12.67C87.179 12.67 87.9771 13.3745 87.9771 14.5668V18.4234H86.9048V17.6302H86.8167C86.6378 17.9147 86.3866 18.1467 86.0887 18.3024C85.7908 18.4581 85.457 18.5319 85.1212 18.5164C84.8843 18.541 84.6448 18.5158 84.4182 18.4422C84.1916 18.3686 83.983 18.2484 83.8057 18.0892C83.6284 17.93 83.4865 17.7355 83.389 17.5181C83.2915 17.3007 83.2407 17.0654 83.2397 16.8271ZM86.8681 16.3448V15.8729L85.4897 15.961C84.7124 16.0131 84.3598 16.2775 84.3598 16.7751C84.3598 17.2831 84.8005 17.5787 85.4065 17.5787C85.584 17.5967 85.7634 17.5788 85.9339 17.526C86.1044 17.4732 86.2625 17.3867 86.3989 17.2716C86.5352 17.1565 86.647 17.0151 86.7276 16.8559C86.8083 16.6967 86.856 16.5229 86.8681 16.3448Z" fill="white" style="fill:white;fill-opacity:1;"/>
+<path d="M89.4409 15.5988C89.4409 13.8152 90.3578 12.6854 91.7839 12.6854C92.1367 12.6691 92.4868 12.7536 92.7933 12.929C93.0998 13.1043 93.3501 13.3633 93.5148 13.6756H93.5981V10.5756H94.712V18.4235H93.6446V17.5317H93.5565C93.3789 17.8419 93.12 18.0977 92.8075 18.2713C92.4951 18.4448 92.1411 18.5296 91.7839 18.5165C90.348 18.5165 89.4409 17.3866 89.4409 15.5988ZM90.5916 15.5988C90.5916 16.796 91.156 17.5164 92.0998 17.5164C93.0387 17.5164 93.6189 16.7856 93.6189 15.6037C93.6189 14.4273 93.0326 13.6861 92.0998 13.6861C91.1621 13.6861 90.5916 14.4114 90.5916 15.5988H90.5916Z" fill="white" style="fill:white;fill-opacity:1;"/>
+<path d="M99.3168 15.5987C99.2813 15.2277 99.3237 14.8535 99.4414 14.4999C99.559 14.1463 99.7492 13.8212 99.9999 13.5455C100.25 13.2697 100.556 13.0494 100.897 12.8986C101.238 12.7479 101.606 12.67 101.979 12.67C102.351 12.67 102.72 12.7479 103.061 12.8986C103.401 13.0494 103.707 13.2697 103.957 13.5455C104.208 13.8212 104.398 14.1463 104.516 14.4999C104.634 14.8535 104.676 15.2277 104.641 15.5987C104.677 15.97 104.635 16.3447 104.517 16.6989C104.4 17.053 104.21 17.3787 103.959 17.655C103.709 17.9313 103.403 18.1521 103.062 18.3032C102.721 18.4543 102.352 18.5323 101.979 18.5323C101.606 18.5323 101.237 18.4543 100.896 18.3032C100.554 18.1521 100.249 17.9313 99.998 17.655C99.7473 17.3787 99.5572 17.053 99.4399 16.6989C99.3226 16.3447 99.2806 15.97 99.3168 15.5987ZM103.495 15.5987C103.495 14.3751 102.945 13.6596 101.98 13.6596C101.012 13.6596 100.468 14.3751 100.468 15.5987C100.468 16.832 101.012 17.542 101.98 17.542C102.945 17.542 103.495 16.8271 103.495 15.5987Z" fill="white" style="fill:white;fill-opacity:1;"/>
+<path d="M106.138 12.7805H107.21V13.6772H107.293C107.434 13.3551 107.673 13.0852 107.975 12.9049C108.277 12.7247 108.627 12.6431 108.978 12.6716C109.252 12.6509 109.528 12.6923 109.785 12.7927C110.041 12.8931 110.272 13.05 110.459 13.2516C110.647 13.4532 110.787 13.6944 110.868 13.9575C110.95 14.2205 110.972 14.4985 110.931 14.7709V18.4249H109.817V15.0507C109.817 14.1436 109.423 13.6925 108.599 13.6925C108.413 13.6838 108.227 13.7156 108.054 13.7855C107.881 13.8555 107.725 13.9621 107.597 14.098C107.469 14.2338 107.372 14.3958 107.312 14.5727C107.253 14.7496 107.232 14.9373 107.252 15.1229V18.425H106.138V12.7805Z" fill="white" style="fill:white;fill-opacity:1;"/>
+<path d="M117.224 11.3739V12.8049H118.447V13.7432H117.224V16.6456C117.224 17.2369 117.467 17.4958 118.022 17.4958C118.164 17.4953 118.306 17.4867 118.447 17.4701V18.398C118.247 18.4337 118.044 18.4528 117.841 18.4549C116.602 18.4549 116.108 18.0191 116.108 16.9308V13.7432H115.212V12.8049H116.108V11.3739H117.224Z" fill="white" style="fill:white;fill-opacity:1;"/>
+<path d="M119.968 10.5756H121.072V13.6861H121.161C121.309 13.361 121.553 13.0895 121.861 12.9086C122.169 12.7276 122.526 12.6461 122.882 12.675C123.155 12.6601 123.428 12.7057 123.682 12.8085C123.935 12.9114 124.163 13.0689 124.348 13.2699C124.534 13.4709 124.673 13.7104 124.756 13.9713C124.838 14.2322 124.862 14.508 124.826 14.7792V18.4235H123.71V15.054C123.71 14.1525 123.291 13.6959 122.503 13.6959C122.312 13.6802 122.119 13.7065 121.939 13.773C121.759 13.8395 121.595 13.9446 121.46 14.0809C121.325 14.2172 121.221 14.3814 121.156 14.5621C121.09 14.7428 121.065 14.9355 121.082 15.1269V18.4234H119.968L119.968 10.5756Z" fill="white" style="fill:white;fill-opacity:1;"/>
+<path d="M131.325 16.8994C131.173 17.4153 130.846 17.8615 130.398 18.1601C129.951 18.4586 129.414 18.5904 128.879 18.5324C128.507 18.5422 128.138 18.471 127.796 18.3237C127.454 18.1764 127.149 17.9565 126.901 17.6793C126.653 17.4021 126.468 17.0742 126.359 16.7184C126.251 16.3627 126.221 15.9875 126.272 15.619C126.222 15.2494 126.253 14.8734 126.361 14.5166C126.469 14.1597 126.653 13.8303 126.9 13.5507C127.147 13.2711 127.451 13.0477 127.791 12.8958C128.132 12.7439 128.501 12.6669 128.874 12.6701C130.445 12.6701 131.392 13.7431 131.392 15.5155V15.9042H127.406V15.9667C127.389 16.1738 127.415 16.3823 127.483 16.5788C127.551 16.7753 127.659 16.9554 127.8 17.1077C127.942 17.2599 128.114 17.3809 128.305 17.4629C128.496 17.5449 128.702 17.586 128.91 17.5837C129.176 17.6157 129.446 17.5677 129.685 17.4459C129.924 17.324 130.122 17.1338 130.252 16.8994L131.325 16.8994ZM127.406 15.0804H130.257C130.271 14.8909 130.246 14.7007 130.182 14.5218C130.118 14.3429 130.017 14.1793 129.886 14.0416C129.756 13.9039 129.597 13.7951 129.422 13.7222C129.247 13.6492 129.058 13.6138 128.868 13.6182C128.675 13.6157 128.484 13.6519 128.306 13.7246C128.127 13.7972 127.965 13.9048 127.829 14.0411C127.693 14.1774 127.585 14.3395 127.513 14.518C127.44 14.6965 127.404 14.8877 127.406 15.0804H127.406Z" fill="white" style="fill:white;fill-opacity:1;"/>
+</svg>

Filskillnaden har hållts tillbaka eftersom den är för stor
+ 14 - 0
src/assets/images/home/btn-google-play.svg


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 4 - 0
src/assets/images/home/btn-macos.svg


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 6 - 0
src/assets/images/home/btn-windows.svg


BIN
src/assets/images/home/devices-mockup.png


BIN
src/assets/images/home/feature-detail-ip.png


BIN
src/assets/images/home/feature-detail-privacy.png


BIN
src/assets/images/home/feature-detail-speed.png


BIN
src/assets/images/home/hero-app-store.png


BIN
src/assets/images/home/hero-bg.png


BIN
src/assets/images/home/hero-devices.png


BIN
src/assets/images/home/hero-earth.png


BIN
src/assets/images/home/hero-google-play.png


+ 43 - 91
src/components/Footerbar/index.tsx

@@ -1,110 +1,62 @@
-import { memo } from 'react';
+import { memo } from 'react'
 
-import { Icon } from '@iconify/react';
-import { useTranslation } from 'react-i18next';
-import { useNavigate } from 'react-router-dom';
+import { Icon } from '@iconify/react'
+import { useTranslation } from 'react-i18next'
+import { useNavigate } from 'react-router-dom'
 
-import logoIcon from '@/assets/iconify/multi-color/logo.svg';
-import { useResponsive } from '@/hooks/useSize';
+
+import logoUnion from '@/assets/iconify/multi-color/logo-union.svg'
 
 const Footerbar = memo(() => {
-    const { t } = useTranslation();
-    const { isMobile } = useResponsive();
-    const navigate = useNavigate();
+    const { t } = useTranslation()
+    const navigate = useNavigate()
 
     const handleLinkClick = (path: string) => {
-        navigate(path);
-    };
+        navigate(path)
+    }
+
+    const navLinks = [
+        { path: '/privacy', labelKey: 'components.footerbar.privacyPolicy' },
+        { path: '/terms-of-service', labelKey: 'components.footerbar.termsOfService' },
+        { path: '/contact', labelKey: 'components.footerbar.contact' },
+    ] as const
 
     return (
-        <footer className={`bg-black border-t border-white/10 ${isMobile ? 'min-h-[168px]' : 'min-h-[189px]'}`}>
-            <div className={`px-[30px] sm:px-6 lg:px-8 max-w-[1276px] mx-auto ${isMobile ? 'pt-12 pb-12' : 'pt-[49px] pb-[48px]'}`}>
-                {isMobile ? (
-                    /* Mobile Layout */
-                    <div className="flex flex-col items-start gap-5">
-                        {/* Logo */}
-                        <div className="flex items-center gap-2">
-                            <Icon icon={logoIcon} className="w-8 h-8" />
-                            <h2 className="text-base font-normal text-white leading-6">
+        <footer className="bg-black border-t border-white/10">
+            <div className="max-w-[1440px] mx-auto px-5 py-5 lg:px-20 lg:py-[50px]">
+                {/* Phone & Tablet: stacked centered / Desktop: row space-between */}
+                <div className="flex flex-col items-center gap-8 lg:flex-row lg:items-center lg:justify-between">
+                    {/* Logo + Copyright */}
+                    <div className="flex flex-col items-center gap-4 lg:items-start">
+                        <div className="flex items-center gap-3">
+                            <Icon icon={logoUnion} className="w-8 h-8" />
+                            <h2 className="text-2xl font-bold italic text-white leading-none font-[REM] tracking-wide">
                                 {t('components.footerbar.logo')}
                             </h2>
                         </div>
-                        {/* Copyright */}
-                        <p className="text-sm font-normal leading-[1.43em] text-white/40 text-center">
+                        <p className="text-sm font-normal leading-[1.43] text-white/40 text-center lg:text-left">
                             {t('components.footerbar.copyright')}
                         </p>
                     </div>
-                ) : (
-                    /* Desktop Layout */
-                    <div className="flex flex-col gap-8 h-[92px]">
-                        {/* Top Section */}
-                        <div className="flex items-center justify-between">
-                            {/* Logo */}
-                            <div className="flex items-center gap-2">
-                                <Icon icon={logoIcon} className="w-8 h-8" />
-                                <h2 className="text-base font-normal text-white leading-6">
-                                    {t('components.footerbar.logo')}
-                                </h2>
-                            </div>
 
-                            {/* Navigation Links */}
-                            <nav className="flex items-center gap-8">
-                                <button
-                                    onClick={() => handleLinkClick('/privacy')}
-                                    className="text-sm font-normal leading-[1.43em] text-white/60 hover:text-white transition-colors border-none bg-transparent whitespace-nowrap"
-                                >
-                                    {t('components.footerbar.privacyPolicy')}
-                                </button>
-                                <button
-                                    onClick={() => handleLinkClick('/terms-of-service')}
-                                    className="text-sm font-normal leading-[1.43em] text-white/60 hover:text-white transition-colors border-none bg-transparent whitespace-nowrap"
-                                >
-                                    {t('components.footerbar.termsOfService')}
-                                </button>
-                                <button
-                                    onClick={() => handleLinkClick('/contact')}
-                                    className="text-sm font-normal leading-[1.43em] text-white/60 hover:text-white transition-colors border-none bg-transparent whitespace-nowrap"
-                                >
-                                    {t('components.footerbar.contact')}
-                                </button>
-                            </nav>
-
-                            {/* Social Media Icons */}
-                            <div className="flex items-center gap-4">
-                                <a
-                                    href="https://twitter.com"
-                                    target="_blank"
-                                    rel="noopener noreferrer"
-                                    className="w-10 h-10 flex items-center justify-center bg-white/5 rounded-full hover:bg-white/10 transition-colors"
-                                    aria-label="Twitter"
-                                >
-                                    <Icon icon="ri:twitter-x-fill" className="w-5 h-5 text-white" />
-                                </a>
-                                <a
-                                    href="https://facebook.com"
-                                    target="_blank"
-                                    rel="noopener noreferrer"
-                                    className="w-10 h-10 flex items-center justify-center bg-white/5 rounded-full hover:bg-white/10 transition-colors"
-                                    aria-label="Facebook"
-                                >
-                                    <Icon icon="ri:facebook-fill" className="w-5 h-5 text-white" />
-                                </a>
-                            </div>
-                        </div>
-
-                        {/* Copyright */}
-                        <div className="flex justify-center">
-                            <p className="text-sm font-normal leading-[1.43em] text-white/40 text-center">
-                                {t('components.footerbar.copyright')}
-                            </p>
-                        </div>
-                    </div>
-                )}
+                    {/* Navigation Links */}
+                    <nav className="flex items-center justify-center gap-8">
+                        {navLinks.map(({ path, labelKey }) => (
+                            <button
+                                key={path}
+                                onClick={() => handleLinkClick(path)}
+                                className="text-sm font-normal leading-[1.43] text-white/60 hover:text-white transition-colors border-none bg-transparent whitespace-nowrap"
+                            >
+                                {t(labelKey)}
+                            </button>
+                        ))}
+                    </nav>
+                </div>
             </div>
         </footer>
-    );
-});
+    )
+})
 
-Footerbar.displayName = 'Footerbar';
+Footerbar.displayName = 'Footerbar'
 
-export default Footerbar;
+export default Footerbar

+ 55 - 35
src/components/Topbar/index.tsx

@@ -1,25 +1,30 @@
 import { Fragment, memo, useMemo } from 'react';
 
-import { Dropdown, type MenuProps } from 'antd';
 import { Icon } from '@iconify/react';
+import { Dropdown, type MenuProps } from 'antd';
 import { useTranslation } from 'react-i18next';
 
-import logoIcon from '@/assets/iconify/multi-color/logo.svg';
-import menuIcon from '@/assets/iconify/single-color/menu.svg';
-import closeIcon from '@/assets/iconify/single-color/close.svg';
+import logoUnion from '@/assets/iconify/multi-color/logo-union.svg';
 import chevronDownIcon from '@/assets/iconify/single-color/chevron-down.svg';
-import type { NavMenuItem } from '@/utils/navUtils';
+import closeIcon from '@/assets/iconify/single-color/close.svg';
+import menuIcon from '@/assets/iconify/single-color/menu.svg';
+import { useAuth } from '@/hooks/useAuth';
+import { useLoginDialog } from '@/hooks/useLoginDialog';
 import { useResponsive } from '@/hooks/useSize';
+import type { NavMenuItem } from '@/utils/navUtils';
+
 import { useAction } from './useAction';
 import { useService } from './useService';
 
 const Topbar = memo(() => {
     const { t } = useTranslation();
     const { isMobile } = useResponsive();
-    const isRtl = t('DIR') === 'rtl';
     const { menuItems, isActive, getMenuItemLabel } = useService();
+    const { isLoggedIn } = useAuth();
+    const openLoginDialog = useLoginDialog();
     const {
         menuContainerRef,
+        loginButtonRef,
         isMobileMenuOpen,
         isMobileMenuClosing,
         isOverflowMenuOpen,
@@ -31,7 +36,7 @@ const Topbar = memo(() => {
         handleMenuAnimationEnd,
         setOverflowMenuOpen,
         setMenuItemRef,
-    } = useAction({ menuItems, isMobile });
+    } = useAction({ menuItems, isMobile, isLoggedIn });
 
     const overflowMenuProps: MenuProps = useMemo(
         () => ({
@@ -47,11 +52,11 @@ const Topbar = memo(() => {
     return (
         <Fragment>
             <header className="fixed top-0 start-0 end-0 z-50 bg-black/90 border-b border-white/10 backdrop-blur-sm">
-                <div className="h-[81px] px-[30px] sm:px-6 lg:px-8 flex items-center justify-between max-w-[1276px] mx-auto">
+                <div className="flex items-center justify-between px-5 sm:px-6 lg:px-20 py-5 max-w-[1440px] mx-auto">
                     {/* Logo */}
-                    <div className="flex-shrink-0 flex items-center gap-2">
-                        <Icon icon={logoIcon} className="w-8 h-8" />
-                        <h1 className="text-base font-normal text-white leading-6">
+                    <div className="flex-shrink-0 flex items-center gap-3">
+                        <Icon icon={logoUnion} className="w-8 h-8" />
+                        <h1 className="text-2xl font-bold italic text-white leading-none font-[REM] tracking-wide">
                             {t('components.topbar.logo')}
                         </h1>
                     </div>
@@ -62,21 +67,19 @@ const Topbar = memo(() => {
                             ref={menuContainerRef}
                             className="flex-1 flex items-center justify-end gap-2.5 ms-8 min-w-0"
                         >
-                            <div className="flex items-center gap-2.5 flex-1 justify-end min-w-0">
+                            <div className="flex items-center gap-2.5 min-w-0">
                                 {visibleMenuItems.map((item: NavMenuItem) => {
                                     const active = isActive(item.path);
-                                    const baseClasses =
-                                        'px-5 py-2.5 rounded-[20px] transition-colors font-normal text-base leading-6 border-none whitespace-nowrap';
-                                    const activeClasses = 'bg-[#0FA4E9] text-white';
-                                    const inactiveClasses =
-                                        'bg-transparent text-white/80 hover:text-white';
-
                                     return (
                                         <button
                                             key={item.name}
                                             ref={setMenuItemRef(item.name)}
                                             onClick={() => handleMenuClick(item.path)}
-                                            className={`${baseClasses} ${active ? activeClasses : inactiveClasses}`}
+                                            className={`px-2.5 h-10 flex items-center justify-center transition-colors text-base leading-6 border-none bg-transparent whitespace-nowrap ${
+                                                active
+                                                    ? 'font-bold text-[#0FA4E9] border-b-2 border-[#0FA4E9]'
+                                                    : 'font-normal text-white/80 hover:text-white'
+                                            }`}
                                         >
                                             {getMenuItemLabel(item)}
                                         </button>
@@ -93,7 +96,7 @@ const Topbar = memo(() => {
                                     >
                                         <button
                                             type="button"
-                                            className="px-5 py-2.5 rounded-[20px] transition-colors font-normal text-base leading-6 border-none bg-transparent text-white/80 hover:text-white flex items-center gap-1 whitespace-nowrap"
+                                            className="px-2.5 h-10 flex items-center justify-center transition-colors text-base leading-6 border-none bg-transparent text-white/80 hover:text-white gap-1 whitespace-nowrap"
                                         >
                                             <span>...</span>
                                             <Icon
@@ -106,6 +109,17 @@ const Topbar = memo(() => {
                                     </Dropdown>
                                 )}
                             </div>
+
+                            {!isLoggedIn && (
+                                <button
+                                    ref={loginButtonRef}
+                                    type="button"
+                                    onClick={() => openLoginDialog()}
+                                    className="ms-2 px-4 h-10 rounded-full bg-[#0FA4E9] text-white text-base font-normal border-none hover:bg-[#0d93d1] transition-colors whitespace-nowrap"
+                                >
+                                    {t('components.topbar.login')}
+                                </button>
+                            )}
                         </nav>
                     )}
 
@@ -114,7 +128,7 @@ const Topbar = memo(() => {
                         <button
                             type="button"
                             onClick={toggleMobileMenu}
-                            className="p-2 rounded-lg bg-transparent border-none text-white outline-none focus:outline-none focus:bg-transparent active:bg-transparent hover:text-[#0EA5E9]/80 active:text-[#0EA5E9]/60 [-webkit-tap-highlight-color:transparent] transition-colors"
+                            className="p-2 rounded-xl bg-white/20 border-none text-white outline-none focus:outline-none [-webkit-tap-highlight-color:transparent] transition-colors"
                             aria-label={isMobileMenuOpen ? 'Close menu' : 'Open menu'}
                         >
                             <Icon
@@ -126,44 +140,50 @@ const Topbar = memo(() => {
                 </div>
             </header>
 
-            {/* Mobile Expanded Menu (Sidebar) - 移到 header 外部 */}
+            {/* Mobile Expanded Menu */}
             {isMobile && (isMobileMenuOpen || isMobileMenuClosing) && (
                 <>
-                    {/* 遮罩层(毛玻璃) */}
                     <div
                         className="fixed inset-0 bg-black/40 backdrop-blur-sm z-40 top-[81px]"
                         onClick={closeMobileMenu}
                     />
-                    {/* 侧边栏菜单:LTR 自右向左进入/向右退出,RTL 自左向右进入/向左退出 */}
                     <nav
-                        className={`fixed end-0 top-[81px] bottom-0 w-[250px] bg-black/80 backdrop-blur-[4px] z-50 ${
+                        className={`fixed end-5 top-[81px] z-50 border border-white/[0.35] bg-black/85 rounded-lg origin-top ${
                             isMobileMenuClosing
-                                ? isRtl
-                                    ? 'animate-slide-out-to-start'
-                                    : 'animate-slide-out-to-end'
-                                : isRtl
-                                  ? 'animate-slide-in-from-start'
-                                  : 'animate-slide-in-from-end'
+                                ? 'animate-collapse-up'
+                                : 'animate-expand-down'
                         }`}
                         onAnimationEnd={handleMenuAnimationEnd}
                     >
-                        <div className="h-full px-[30px] pt-[30px] flex flex-col gap-[14px]">
+                        <div className="flex flex-col items-end gap-4 p-4">
                             {menuItems.map((item: NavMenuItem) => {
                                 const active = isActive(item.path);
                                 return (
                                     <button
                                         key={item.name}
                                         onClick={() => handleMenuClick(item.path)}
-                                        className={`w-full text-start text-xl font-medium leading-[1.4em] transition-colors border-none bg-transparent ${
+                                        className={`text-base leading-[1.5] transition-colors border-none bg-transparent whitespace-nowrap ${
                                             active
-                                                ? 'text-[#0EA5E9]'
-                                                : 'text-white hover:text-white/80'
+                                                ? 'font-bold text-white'
+                                                : 'font-normal text-[#999] hover:text-white'
                                         }`}
                                     >
                                         {getMenuItemLabel(item)}
                                     </button>
                                 );
                             })}
+                            {!isLoggedIn && (
+                                <button
+                                    type="button"
+                                    onClick={() => {
+                                        closeMobileMenu();
+                                        openLoginDialog();
+                                    }}
+                                    className="text-base leading-[1.5] font-normal text-[#999] hover:text-white transition-colors border-none bg-transparent whitespace-nowrap"
+                                >
+                                    {t('components.topbar.login')}
+                                </button>
+                            )}
                         </div>
                     </nav>
                 </>

+ 36 - 25
src/components/Topbar/useAction.ts

@@ -10,20 +10,23 @@ const MENU_ITEM_GAP = 10;
 interface UseActionParams {
     menuItems: NavMenuItem[];
     isMobile: boolean;
+    isLoggedIn: boolean;
 }
 
 /**
  * Topbar UI 交互响应逻辑 Hook
  * 处理 UI 状态、事件处理、DOM 交互等响应逻辑
  */
-export function useAction({ menuItems, isMobile }: UseActionParams) {
+export function useAction({ menuItems, isMobile, isLoggedIn }: UseActionParams) {
     const navigate = useNavigate();
     const menuContainerRef = useRef<HTMLDivElement>(null);
     const menuItemsRef = useRef<Map<string, HTMLButtonElement>>(new Map());
+    const itemWidthCache = useRef<Map<string, number>>(new Map());
+    const loginButtonRef = useRef<HTMLButtonElement>(null);
     const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
     const [isMobileMenuClosing, setIsMobileMenuClosing] = useState(false);
     const [isOverflowMenuOpen, setIsOverflowMenuOpen] = useState(false);
-    const [visibleMenuItems, setVisibleMenuItems] = useState<NavMenuItem[]>([]);
+    const [visibleMenuItems, setVisibleMenuItems] = useState<NavMenuItem[]>(() => menuItems);
     const [overflowMenuItems, setOverflowMenuItems] = useState<NavMenuItem[]>([]);
 
     const calculateVisibleItems = useCallback(() => {
@@ -34,7 +37,10 @@ export function useAction({ menuItems, isMobile }: UseActionParams) {
         }
 
         const container = menuContainerRef.current;
-        const containerWidth = container.offsetWidth;
+        const loginBtnWidth = loginButtonRef.current
+            ? loginButtonRef.current.offsetWidth + MENU_ITEM_GAP
+            : 0;
+        const containerWidth = container.offsetWidth - loginBtnWidth;
 
         let totalWidth = 0;
         const visible: NavMenuItem[] = [];
@@ -44,41 +50,45 @@ export function useAction({ menuItems, isMobile }: UseActionParams) {
             const item = menuItems[i];
             const itemElement = menuItemsRef.current.get(item.name);
 
+            let itemWidth: number | undefined;
             if (itemElement) {
-                const itemWidth = itemElement.offsetWidth + MENU_ITEM_GAP;
-                const needsOverflowButton = i < menuItems.length - 1;
-
-                if (
-                    totalWidth + itemWidth + (needsOverflowButton ? OVERFLOW_BUTTON_WIDTH : 0) <=
-                    containerWidth
-                ) {
-                    visible.push(item);
-                    totalWidth += itemWidth;
-                } else {
-                    overflow.push(...menuItems.slice(i));
-                    break;
-                }
+                itemWidth = itemElement.offsetWidth + MENU_ITEM_GAP;
+                itemWidthCache.current.set(item.name, itemWidth);
             } else {
+                itemWidth = itemWidthCache.current.get(item.name);
+            }
+
+            if (itemWidth === undefined) {
                 visible.push(item);
+                continue;
             }
-        }
 
-        console.log('visible', visible);
-        console.log('overflow', overflow);
+            const needsOverflowButton = i < menuItems.length - 1;
+            if (
+                totalWidth + itemWidth + (needsOverflowButton ? OVERFLOW_BUTTON_WIDTH : 0) <=
+                containerWidth
+            ) {
+                visible.push(item);
+                totalWidth += itemWidth;
+            } else {
+                overflow.push(...menuItems.slice(i));
+                break;
+            }
+        }
 
         setVisibleMenuItems(visible);
         setOverflowMenuItems(overflow);
-    }, [isMobile, menuItems]);
+        // eslint-disable-next-line react-hooks/exhaustive-deps
+    }, [isMobile, menuItems, isLoggedIn]);
 
     useEffect(() => {
         if (!isMobile && menuItems.length > 0) {
-            const timer = setTimeout(() => {
+            const rafId = requestAnimationFrame(() => {
                 calculateVisibleItems();
-            }, 0);
-
-            return () => clearTimeout(timer);
+            });
+            return () => cancelAnimationFrame(rafId);
         }
-    }, [isMobile, menuItems.length, calculateVisibleItems]);
+    }, [isMobile, menuItems.length, isLoggedIn, calculateVisibleItems]);
 
     useEffect(() => {
         if (isMobile) {
@@ -138,6 +148,7 @@ export function useAction({ menuItems, isMobile }: UseActionParams) {
 
     return {
         menuContainerRef,
+        loginButtonRef,
         menuItemsRef,
         isMobileMenuOpen,
         isMobileMenuClosing,

+ 32 - 0
src/hooks/useAuth.ts

@@ -0,0 +1,32 @@
+import { useEffect, useMemo, useState } from 'react';
+
+import { userConfigModel } from '@/models/userConfigModel';
+import { isAuthenticated } from '@/utils/authUtils';
+
+/**
+ * 实时判断登录状态的 hook。
+ *
+ * 响应来源:
+ * - userConfigModel 变更(登录/登出时立即响应)
+ * - visibilitychange 事件(切回标签页时重新校验,捕获 token 自然过期)
+ */
+export function useAuth() {
+    const { userConfig } = userConfigModel.useModel();
+    const [tick, setTick] = useState(0);
+
+    useEffect(() => {
+        const onVisible = () => {
+            if (document.visibilityState === 'visible') {
+                setTick((t) => t + 1);
+            }
+        };
+        document.addEventListener('visibilitychange', onVisible);
+        return () => document.removeEventListener('visibilitychange', onVisible);
+    }, []);
+
+    // userConfig 变更驱动重新校验,tick 捕获 visibilitychange
+    // eslint-disable-next-line react-hooks/exhaustive-deps
+    const isLoggedIn = useMemo(() => isAuthenticated(), [userConfig, tick]);
+
+    return { isLoggedIn };
+}

+ 86 - 0
src/hooks/useLoginDialog.tsx

@@ -0,0 +1,86 @@
+import { useCallback } from 'react'
+
+import { Trans, useTranslation } from 'react-i18next'
+
+import LoginForm from '@/components/LoginForm'
+import { useAppUrls } from '@/hooks/useAppUrls'
+import { dialogModel } from '@/models/dialogModel'
+
+export interface LoginDialogOptions {
+    /** 弹窗标题的 i18n key,默认 common.login.dialogTitle */
+    titleKey?: string
+    /** 提示文案的 i18n key(支持 <linkText> / <downloadLink> 插值),默认 common.login.dialogPrompt */
+    promptKey?: string
+}
+
+export function useLoginDialog() {
+    const { t } = useTranslation()
+    const { openDialog, closeDialog } = dialogModel.useModel()
+    const { deeplinkUrl, downloadUrlByPlatform } = useAppUrls()
+
+    return useCallback(
+        (options?: LoginDialogOptions) => {
+            const titleKey = options?.titleKey ?? 'common.login.dialogTitle'
+            const promptKey = options?.promptKey ?? 'common.login.dialogPrompt'
+
+            const id = openDialog({
+                title: t(titleKey),
+                content: (
+                    <div className="flex flex-col gap-2">
+                        <p className="text-white/80 text-sm mb-2">
+                            <Trans
+                                i18nKey={promptKey}
+                                components={{
+                                    linkText: (() => {
+                                        const Wrap = ({
+                                            children,
+                                        }: {
+                                            children?: React.ReactNode
+                                        }) =>
+                                            deeplinkUrl ? (
+                                                <a
+                                                    href={deeplinkUrl}
+                                                    className="text-[#0EA5E9] hover:underline"
+                                                    target="_blank"
+                                                    rel="noopener noreferrer"
+                                                >
+                                                    {children}
+                                                </a>
+                                            ) : (
+                                                <span>{children}</span>
+                                            )
+                                        return <Wrap />
+                                    })(),
+                                    downloadLink: (() => {
+                                        const Wrap = ({
+                                            children,
+                                        }: {
+                                            children?: React.ReactNode
+                                        }) =>
+                                            downloadUrlByPlatform ? (
+                                                <a
+                                                    href={downloadUrlByPlatform}
+                                                    className="text-[#0EA5E9] hover:underline"
+                                                    target="_blank"
+                                                    rel="noopener noreferrer"
+                                                >
+                                                    {children}
+                                                </a>
+                                            ) : (
+                                                <span>{children}</span>
+                                            )
+                                        return <Wrap />
+                                    })(),
+                                }}
+                            />
+                        </p>
+                        <LoginForm onSuccess={() => closeDialog(id)} />
+                    </div>
+                ),
+                maskClosable: false,
+                closeable: true,
+            })
+        },
+        [t, openDialog, closeDialog, deeplinkUrl, downloadUrlByPlatform]
+    )
+}

+ 8 - 3
src/hooks/useSize.ts

@@ -60,12 +60,17 @@ export const useRefSize = <T extends HTMLElement = HTMLDivElement>(
 };
 
 const MOBILE_BREAKPOINT = 768;
+const PHONE_BREAKPOINT = 640;
 
 /**
- * 响应式检测:基于 useScreenSize 的 body 宽度判断是否为移动端(宽度 < 768px)。
- * @returns { isMobile } 当前是否视为移动端
+ * 响应式检测:基于 useScreenSize 的 body 宽度判断设备类型。
+ * - isMobile: 宽度 ≤ 768px(手机 + 平板)
+ * - isPhone: 宽度 < 640px(仅手机,对应 Tailwind sm 断点以下)
  */
 export function useResponsive() {
     const { width } = useScreenSize();
-    return { isMobile: width <= MOBILE_BREAKPOINT };
+    return {
+        isMobile: width <= MOBILE_BREAKPOINT,
+        isPhone: width > 0 && width < PHONE_BREAKPOINT,
+    };
 }

+ 7 - 1
src/locales/en-US/common.ts

@@ -1 +1,7 @@
-export default {};
+export default {
+    login: {
+        dialogTitle: 'Log In',
+        dialogPrompt:
+            'No account? <linkText>Go to app</linkText> to register, or <downloadLink>download app</downloadLink>',
+    },
+};

+ 3 - 2
src/locales/en-US/components.ts

@@ -1,12 +1,13 @@
 export default {
     topbar: {
-        logo: 'NOMO',
+        logo: 'NOMO VPN',
+        login: 'Login',
     },
     footerbar: {
         logo: 'NOMO VPN',
         privacyPolicy: 'Privacy Policy',
         termsOfService: 'Terms of Service',
         contact: 'Contact',
-        copyright: '© 2026 NOMO VPN Inc., All Rights Reserved',
+        copyright: '© 2026 NOMO VPN Inc., All rights reserved',
     },
 };

+ 38 - 26
src/locales/en-US/pages.ts

@@ -19,28 +19,51 @@ export default {
 
     home: {
         hero: {
-            title: 'Browse Without\nBoundaries',
-            subtitle:
-                'Secure, fast, and private VPN service with military-grade encryption. Browse the internet freely with global coverage.',
+            connection: {
+                title: '2-Second\nConnection',
+                description: 'Connect to our VPN servers instantly with just one tap',
+            },
+            trial: {
+                title: '3-Day\nFree Trial',
+                description: 'Try all premium features free for 3 days without a commitment',
+            },
+            speed: {
+                title: 'High-Speed & Low Latency',
+                description: 'Experience blazing fast speeds with minimal latency',
+            },
+            private: {
+                title: 'Secure\n& Private',
+                description: "Your data is encrypted and we don't keep any logs",
+            },
         },
-        features: {
-            title: 'Why Choose NOMO VPN?',
-            subtitle:
-                'Experience the next generation of VPN technology with our cutting-edge features',
+        slogan: {
+            title: 'VPN for Everyday Use',
+            description:
+                'Connect in 2 seconds, enjoy 3 days free trial, and experience high-speed, low-latency internet access. NomoVPN helps you access global content with premium quality and best-in-class privacy protection.',
+        },
+        choosePlatform: {
+            title: 'Choose your NOMO VPN',
+            subtitle: 'Choose the platform that suits you best',
+        },
+        appAccess: {
+            title: 'Seamless access to popular global applications',
+            subtitle: 'Enjoy Smooth Access To Your favorite services',
+        },
+        featureDetails: {
             speed: {
-                title: 'Lightning Fast Speed',
+                title: 'High-quality low-latency dedicated line',
                 description:
-                    'Experience blazing-fast connection speeds with our optimized server network. Stream, game, and browse without buffering.',
+                    'NomoVPN uses high-quality proprietary lines, resulting in shorter routing paths, faster and more stable internet speeds, with no congestion or packet loss during peak hours.',
             },
-            coverage: {
-                title: 'Global Coverage',
+            privacy: {
+                title: 'No Record VPN',
                 description:
-                    'Access content from anywhere with our 5000+ servers in 60+ countries. Connect to the location that works best for you.',
+                    'NomoVPN never collects your browsing history, downloads, or any other sensitive data',
             },
-            encryption: {
-                title: 'Military-Grade Encryption',
+            fixedIp: {
+                title: 'Provide fixed IP functionality',
                 description:
-                    'Your data is protected with AES-256 encryption. We maintain a strict no-logs policy to ensure your privacy is always protected.',
+                    'When connecting to the same node, we prioritize assigning servers with the same historical IP for your connection, significantly enhancing the stability of your accounts across platforms.',
             },
         },
         pricing: {
@@ -49,17 +72,6 @@ export default {
                 'Select the perfect plan for your needs. All plans include our core security features.',
             selectPlan: 'Get Started',
         },
-        download: {
-            title: 'Get Started Today',
-            subtitle:
-                'Available for iOS, Android, and Windows soon. Download NOMO VPN and experience true online freedom.',
-            downloadsCount: '1M+',
-            downloadsLabel: 'Downloads',
-            appStoreRating: '4.8★',
-            appStoreLabel: 'App Store',
-            googlePlayRating: '4.7★',
-            googlePlayLabel: 'Google Play',
-        },
     },
 
     pricing: {

+ 7 - 1
src/locales/fa-IR/common.ts

@@ -1 +1,7 @@
-export default {};
+export default {
+    login: {
+        dialogTitle: 'ورود به حساب',
+        dialogPrompt:
+            'حساب ندارید؟ <linkText>به اپ بروید</linkText> برای ثبت‌نام، یا <downloadLink>دانلود اپ</downloadLink>',
+    },
+};

+ 2 - 1
src/locales/fa-IR/components.ts

@@ -1,6 +1,7 @@
 export default {
     topbar: {
-        logo: 'NOMO',
+        logo: 'NOMO VPN',
+        login: 'ورود',
     },
     footerbar: {
         logo: 'NOMO VPN',

+ 38 - 25
src/locales/fa-IR/pages.ts

@@ -19,27 +19,51 @@ export default {
 
     home: {
         hero: {
-            title: 'مرور بدون\nمرز',
-            subtitle:
-                'سرویس VPN امن، سریع و خصوصی با رمزنگاری سطح نظامی. با پوشش جهانی آزادانه در اینترنت گشت‌زنی کنید.',
+            connection: {
+                title: 'اتصال\n۲ ثانیه‌ای',
+                description: 'با یک ضربه فوراً به سرورهای VPN ما متصل شوید',
+            },
+            trial: {
+                title: '۳ روز\nآزمایش رایگان',
+                description: 'بدون تعهد، ۳ روز تمام امکانات ویژه را رایگان امتحان کنید',
+            },
+            speed: {
+                title: 'سرعت بالا و تأخیر کم',
+                description: 'سرعت خیره‌کننده با کمترین تأخیر را تجربه کنید',
+            },
+            private: {
+                title: 'امن\nو خصوصی',
+                description: 'داده‌های شما رمزنگاری شده و ما هیچ لاگی نگه نمی‌داریم',
+            },
         },
-        features: {
-            title: 'چرا NOMO VPN؟',
-            subtitle: 'نسل بعدی فناوری VPN را با امکانات پیشرفته تجربه کنید',
+        slogan: {
+            title: 'VPN برای استفاده روزانه',
+            description:
+                'در ۲ ثانیه متصل شوید، ۳ روز آزمایش رایگان و دسترسی اینترنتی پرسرعت و کم‌تأخیر را تجربه کنید. NomoVPN به شما کمک می‌کند با کیفیت برتر و بهترین حفاظت حریم خصوصی به محتوای جهانی دسترسی پیدا کنید.',
+        },
+        choosePlatform: {
+            title: 'NOMO VPN خود را انتخاب کنید',
+            subtitle: 'پلتفرمی که برای شما مناسب‌تر است را انتخاب کنید',
+        },
+        appAccess: {
+            title: 'دسترسی بی‌وقفه به اپلیکیشن‌های محبوب جهانی',
+            subtitle: 'از دسترسی روان به سرویس‌های مورد علاقه خود لذت ببرید',
+        },
+        featureDetails: {
             speed: {
-                title: 'سرعت برق‌آسا',
+                title: 'خط اختصاصی با کیفیت بالا و تأخیر کم',
                 description:
-                    'با شبکه سرورهای بهینه‌شده از اتصال فوق‌سریع لذت ببرید. استریم، بازی و مرور بدون بافرینگ.',
+                    'NomoVPN از خطوط اختصاصی با کیفیت بالا استفاده می‌کند که مسیر کوتاه‌تر، سرعت اینترنت سریع‌تر و پایدارتر و بدون ازدحام یا از دست رفتن بسته در ساعات اوج مصرف را فراهم می‌کند.',
             },
-            coverage: {
-                title: 'پوشش جهانی',
+            privacy: {
+                title: 'VPN بدون ثبت سوابق',
                 description:
-                    'با بیش از ۵۰۰۰ سرور در بیش از ۶۰ کشور از هرجا به محتوا دسترسی داشته باشید. به بهترین موقعیت برای خود متصل شوید.',
+                    'NomoVPN هرگز تاریخچه مرور، دانلودها یا هر داده حساس دیگری را جمع‌آوری نمی‌کند',
             },
-            encryption: {
-                title: 'رمزنگاری سطح نظامی',
+            fixedIp: {
+                title: 'قابلیت IP ثابت',
                 description:
-                    'داده‌های شما با رمزنگاری AES-256 محافظت می‌شوند. ما سیاست بدون ذخیره لاگ را رعایت می‌کنیم تا حریم خصوصی شما همیشه در امان باشد.',
+                    'هنگام اتصال به همان نود، ما اولویت را به اختصاص سرورهای با IP تاریخی یکسان برای اتصال شما می‌دهیم و پایداری حساب‌های شما در پلتفرم‌های مختلف را به‌طور قابل‌توجهی افزایش می‌دهیم.',
             },
         },
         pricing: {
@@ -48,17 +72,6 @@ export default {
                 'مناسب‌ترین پلن را برای نیاز خود انتخاب کنید. همه پلن‌ها شامل امکانات امنیتی اصلی هستند.',
             selectPlan: 'خرید',
         },
-        download: {
-            title: 'همین امروز شروع کنید',
-            subtitle:
-                'به‌زودی برای iOS، اندروید و ویندوز. NOMO VPN را دانلود کنید و آزادی واقعی آنلاین را تجربه کنید.',
-            downloadsCount: '۱ میلیون+',
-            downloadsLabel: 'دانلود',
-            appStoreRating: '۴.۸★',
-            appStoreLabel: 'اپ استور',
-            googlePlayRating: '۴.۷★',
-            googlePlayLabel: 'گوگل پلی',
-        },
     },
 
     pricing: {

+ 7 - 1
src/locales/zh-CN/common.ts

@@ -1 +1,7 @@
-export default {};
+export default {
+    login: {
+        dialogTitle: '账号登录',
+        dialogPrompt:
+            '没有账号?<linkText>前往 APP</linkText> 注册,或 <downloadLink>下载 APP</downloadLink>',
+    },
+};

+ 2 - 1
src/locales/zh-CN/components.ts

@@ -1,6 +1,7 @@
 export default {
     topbar: {
-        logo: 'NOMO',
+        logo: 'NOMO VPN',
+        login: '登录',
     },
     footerbar: {
         logo: 'NOMO VPN',

+ 38 - 26
src/locales/zh-CN/pages.ts

@@ -19,27 +19,50 @@ export default {
 
     home: {
         hero: {
-            title: '畅游无界',
-            subtitle:
-                '安全、快速、私密的 VPN 服务,采用军用级加密。全球覆盖,自由畅享互联网。',
+            connection: {
+                title: '2秒极速连接',
+                description: '一键即连,瞬间接入全球 VPN 服务器',
+            },
+            trial: {
+                title: '3天免费试用',
+                description: '无需承诺,免费体验3天全部高级功能',
+            },
+            speed: {
+                title: '高速低延迟',
+                description: '极致速度,最低延迟',
+            },
+            private: {
+                title: '安全且私密',
+                description: '数据全程加密,我们不保留任何日志',
+            },
         },
-        features: {
-            title: '为什么选择 NOMO VPN?',
-            subtitle: '体验新一代 VPN 技术,畅享前沿功能',
+        slogan: {
+            title: '日常必备 VPN',
+            description:
+                '2秒极速连接,享受3天免费试用,体验高速低延迟的网络访问。NomoVPN 帮助您以优质品质和一流隐私保护访问全球内容。',
+        },
+        choosePlatform: {
+            title: '选择你的 NOMO VPN',
+            subtitle: '选择最适合你的平台',
+        },
+        appAccess: {
+            title: '无缝访问全球热门应用',
+            subtitle: '畅享流畅访问你喜爱的服务',
+        },
+        featureDetails: {
             speed: {
-                title: '极速连接',
+                title: '高品质低延迟专线',
                 description:
-                    '依托优化服务器网络,畅享极速连接。追剧、游戏、浏览,告别卡顿。',
+                    'NomoVPN 使用高品质自有专线,路由路径更短,网速更快更稳定,高峰时段无拥堵、无丢包。',
             },
-            coverage: {
-                title: '全球覆盖',
-                description:
-                    '60+ 国家/地区、5000+ 服务器,随时随地访问内容,连接最适合你的节点。',
+            privacy: {
+                title: '零记录 VPN',
+                description: 'NomoVPN 绝不收集您的浏览历史、下载记录或任何其他敏感数据',
             },
-            encryption: {
-                title: '军用级加密',
+            fixedIp: {
+                title: '提供固定 IP 功能',
                 description:
-                    'AES-256 加密保护你的数据。我们坚持零日志政策,始终守护你的隐私。',
+                    '连接同一节点时,我们优先分配历史相同 IP 的服务器,显著提升您在各平台账号的稳定性。',
             },
         },
         pricing: {
@@ -47,17 +70,6 @@ export default {
             subtitle: '根据需求选择最合适的方案,所有套餐均包含核心安全功能。',
             selectPlan: '去购买',
         },
-        download: {
-            title: '立即开始使用',
-            subtitle:
-                '即将支持 iOS、Android 与 Windows。下载 NOMO VPN,畅享真正的上网自由。',
-            downloadsCount: '100 万+',
-            downloadsLabel: '次下载',
-            appStoreRating: '4.8★',
-            appStoreLabel: 'App Store',
-            googlePlayRating: '4.7★',
-            googlePlayLabel: 'Google Play',
-        },
     },
 
     pricing: {

+ 46 - 0
src/pages/home/components/AppAccess.tsx

@@ -0,0 +1,46 @@
+import { useTranslation } from 'react-i18next';
+
+import appAccessSection from '@/assets/images/home/app-access-section.png';
+import appLogosStrip from '@/assets/images/home/app-logos-strip.png';
+
+import Wrapper from './Wrapper';
+
+export function AppAccess() {
+    const { t } = useTranslation();
+
+    return (
+        <section className="w-full py-10 lg:py-16">
+            <Wrapper className="flex flex-col items-center gap-6">
+                {/* Title + Subtitle */}
+                <div className="flex flex-col items-center gap-4 text-center">
+                    <h2 className="text-2xl lg:text-[32px] font-semibold italic text-white leading-[1.667] lg:leading-[0.94] tracking-[-0.019em] lg:tracking-[-0.014em] font-[REM]">
+                        {t('pages.home.appAccess.title')}
+                    </h2>
+                    <p className="text-sm lg:text-base font-normal text-white/60 leading-[1.5]">
+                        {t('pages.home.appAccess.subtitle')}
+                    </p>
+                </div>
+
+                {/* App wall background with NOMO branding */}
+                <div className="relative w-full max-w-[1280px] rounded-lg overflow-hidden">
+                    <img
+                        src={appAccessSection}
+                        alt=""
+                        className="w-full h-auto object-cover"
+                    />
+                </div>
+
+                {/* Logo strip with fade edges */}
+                <div className="relative w-full max-w-[1280px] overflow-hidden">
+                    <div className="absolute inset-y-0 start-0 w-20 lg:w-40 bg-gradient-to-r from-[#010203] to-transparent z-10 rtl:bg-gradient-to-l" />
+                    <div className="absolute inset-y-0 end-0 w-20 lg:w-40 bg-gradient-to-l from-[#010203] to-transparent z-10 rtl:bg-gradient-to-r" />
+                    <img
+                        src={appLogosStrip}
+                        alt="Popular apps"
+                        className="w-full h-auto object-contain"
+                    />
+                </div>
+            </Wrapper>
+        </section>
+    );
+}

+ 71 - 0
src/pages/home/components/ChoosePlatform.tsx

@@ -0,0 +1,71 @@
+import { useTranslation } from 'react-i18next'
+
+import btnAndroid from '@/assets/images/home/btn-android.svg'
+import btnAppStore from '@/assets/images/home/btn-app-store.svg'
+import btnGooglePlay from '@/assets/images/home/btn-google-play.svg'
+import btnMacos from '@/assets/images/home/btn-macos.svg'
+import btnWindows from '@/assets/images/home/btn-windows.svg'
+import devicesMockup from '@/assets/images/home/devices-mockup.png'
+import { useAppUrls } from '@/hooks/useAppUrls'
+
+import Wrapper from './Wrapper'
+
+interface PlatformButton {
+    image: string
+    alt: string
+    urlKey: 'appleStoreUrl' | 'googleStoreUrl' | 'downloadUrlByPlatform'
+}
+
+const STORE_BUTTONS: PlatformButton[] = [
+    { image: btnAppStore, alt: 'App Store', urlKey: 'appleStoreUrl' },
+    { image: btnGooglePlay, alt: 'Google Play', urlKey: 'googleStoreUrl' },
+]
+
+const PLATFORM_ICONS = [
+    { image: btnAndroid, alt: 'Android' },
+    { image: btnWindows, alt: 'Windows' },
+    { image: btnMacos, alt: 'Mac OS' },
+] as const
+
+export function ChoosePlatform() {
+    const { t } = useTranslation()
+    const urls = useAppUrls()
+
+    return (
+        <section className="w-full py-8 sm:py-10 lg:py-16">
+            <Wrapper className="flex flex-col items-center gap-9 sm:gap-10 lg:gap-[72px]">
+                <div className="flex flex-col items-center gap-6 w-full">
+                    <div className="flex flex-col items-center gap-4 max-w-[672px] text-center">
+                        <h2 className="text-2xl lg:text-[32px] font-semibold italic text-white leading-[1.667] lg:leading-[0.94] tracking-[-0.019em] lg:tracking-[-0.014em] font-[REM]">
+                            {t('pages.home.choosePlatform.title')}
+                        </h2>
+                        <p className="text-sm lg:text-base font-normal text-white/60 leading-[1.5]">
+                            {t('pages.home.choosePlatform.subtitle')}
+                        </p>
+                    </div>
+
+                    <div className="grid grid-cols-2 gap-6 px-4 w-full sm:flex sm:flex-wrap sm:items-center sm:justify-center">
+                        {STORE_BUTTONS.map(({ image, alt, urlKey }) => {
+                            const url = urls[urlKey]
+                            if (!url) return null
+                            return (
+                                <a key={alt} href={url} target="_blank" rel="noopener noreferrer" className="h-[50px]">
+                                    <img src={image} alt={alt} className="h-full w-full object-contain sm:w-auto" />
+                                </a>
+                            )
+                        })}
+                        {PLATFORM_ICONS.map(({ image, alt }) => (
+                            <img key={alt} src={image} alt={alt} className="h-[50px] w-full object-contain sm:w-auto" />
+                        ))}
+                    </div>
+                </div>
+
+                <img
+                    src={devicesMockup}
+                    alt="NOMO VPN on multiple devices"
+                    className="w-full max-w-[350px] sm:max-w-[600px] lg:max-w-[786px] h-auto object-contain"
+                />
+            </Wrapper>
+        </section>
+    )
+}

+ 0 - 62
src/pages/home/components/Download.tsx

@@ -1,62 +0,0 @@
-import { useTranslation } from 'react-i18next';
-
-import { useAppUrls } from '@/hooks/useAppUrls';
-
-import heroAppStore from '@/assets/images/home/hero-app-store.png';
-import heroGooglePlay from '@/assets/images/home/hero-google-play.png';
-
-import { DownloadButton } from './DownloadButton';
-import Wrapper from './Wrapper';
-
-export function Download() {
-    const { t } = useTranslation();
-    const { appleStoreUrl, downloadUrlByPlatform } = useAppUrls();
-
-    const items = [
-        {
-            titleKey: 'pages.home.download.downloadsCount',
-            descKey: 'pages.home.download.downloadsLabel',
-        },
-        {
-            titleKey: 'pages.home.download.appStoreRating',
-            descKey: 'pages.home.download.appStoreLabel',
-        },
-        {
-            titleKey: 'pages.home.download.googlePlayRating',
-            descKey: 'pages.home.download.googlePlayLabel',
-        },
-    ] as const;
-
-    return (
-        <section className="w-full py-10 sm:py-20 mt-20 sm:mt-5 lg:mt-20">
-            <Wrapper className="text-center">
-                <h2 className="text-2xl sm:text-[32px] font-medium text-white leading-[1.25] sm:leading-[0.9375]">
-                    {t('pages.home.download.title')}
-                </h2>
-                <p className="text-base mt-4 sm:mt-[18px] text-white/60 sm:text-white/90 max-w-[632px] mx-auto leading-[1.5]">
-                    {t('pages.home.download.subtitle')}
-                </p>
-                <div className="mt-10 sm:mt-12 flex flex-col sm:flex-row items-center justify-center gap-[27px] sm:gap-4">
-                    <DownloadButton
-                        image={heroAppStore}
-                        url={appleStoreUrl ?? ''}
-                        className="h-[67px]"
-                    />
-                    <DownloadButton
-                        image={heroGooglePlay}
-                        url={downloadUrlByPlatform ?? ''}
-                        className="h-[67px]"
-                    />
-                </div>
-                <div className="mt-10 sm:mt-12 flex flex-wrap justify-center items-center gap-6 sm:gap-12 text-base">
-                    {items.map((item, index) => (
-                        <div className="flex flex-col items-center gap-1 text-base" key={index}>
-                            <span className="font-semibold text-white/80">{t(item.titleKey)}</span>
-                            <span className="text-white/60">{t(item.descKey)}</span>
-                        </div>
-                    ))}
-                </div>
-            </Wrapper>
-        </section>
-    );
-}

+ 0 - 23
src/pages/home/components/DownloadButton.tsx

@@ -1,23 +0,0 @@
-interface DownloadButtonProps {
-    image: string;
-    url: string;
-    alt?: string;
-    /** 图片高度样式,如 h-[50px],不传则默认 h-[34px] */
-    className?: string;
-}
-
-const DEFAULT_IMG_CLASS = 'h-[34px]';
-
-export function DownloadButton({ image, url, alt = '', className }: DownloadButtonProps) {
-    if (!url) return null;
-
-    return (
-        <a href={url} target="_blank" rel="noopener noreferrer">
-            <img
-                src={image}
-                alt={alt}
-                className={`${className ?? DEFAULT_IMG_CLASS} sm:h-[50px] w-auto object-contain block`}
-            />
-        </a>
-    );
-}

+ 72 - 0
src/pages/home/components/FeatureDetails.tsx

@@ -0,0 +1,72 @@
+import { useTranslation } from 'react-i18next'
+
+import featureDetailIp from '@/assets/images/home/feature-detail-ip.png'
+import featureDetailPrivacy from '@/assets/images/home/feature-detail-privacy.png'
+import featureDetailSpeed from '@/assets/images/home/feature-detail-speed.png'
+
+import Wrapper from './Wrapper'
+
+interface FeatureItem {
+    titleKey: string
+    descKey: string
+    image: string
+    imageAlt: string
+    reverse: boolean
+}
+
+const FEATURES: FeatureItem[] = [
+    {
+        titleKey: 'pages.home.featureDetails.speed.title',
+        descKey: 'pages.home.featureDetails.speed.description',
+        image: featureDetailSpeed,
+        imageAlt: 'High-quality dedicated line',
+        reverse: false,
+    },
+    {
+        titleKey: 'pages.home.featureDetails.privacy.title',
+        descKey: 'pages.home.featureDetails.privacy.description',
+        image: featureDetailPrivacy,
+        imageAlt: 'No Record VPN',
+        reverse: true,
+    },
+    {
+        titleKey: 'pages.home.featureDetails.fixedIp.title',
+        descKey: 'pages.home.featureDetails.fixedIp.description',
+        image: featureDetailIp,
+        imageAlt: 'Fixed IP functionality',
+        reverse: false,
+    },
+]
+
+export function FeatureDetails() {
+    const { t } = useTranslation()
+
+    return (
+        <section className="w-full py-10 lg:py-16">
+            <Wrapper className="flex flex-col gap-10 sm:gap-14 lg:gap-20">
+                {FEATURES.map(({ titleKey, descKey, image, imageAlt, reverse }, index) => (
+                    <div
+                        key={index}
+                        className={`flex flex-col ${reverse ? 'lg:flex-row-reverse' : 'lg:flex-row'} items-center gap-4 sm:gap-6 lg:gap-8`}
+                    >
+                        <div className="w-full lg:flex-1 flex justify-center">
+                            <img
+                                src={image}
+                                alt={imageAlt}
+                                className="w-full max-w-[320px] sm:max-w-[450px] lg:max-w-none h-auto rounded-lg object-contain"
+                            />
+                        </div>
+                        <div className="w-full lg:flex-1 flex flex-col gap-4 max-w-[700px] text-center">
+                            <h3 className="text-2xl lg:text-[32px] font-semibold italic text-white leading-[1.667] lg:leading-[0.94] tracking-[-0.019em] lg:tracking-[-0.014em] font-[REM]">
+                                {t(titleKey)}
+                            </h3>
+                            <p className="text-sm lg:text-base font-normal text-white/60 leading-[1.5]">
+                                {t(descKey)}
+                            </p>
+                        </div>
+                    </div>
+                ))}
+            </Wrapper>
+        </section>
+    )
+}

+ 0 - 69
src/pages/home/components/Features.tsx

@@ -1,69 +0,0 @@
-import { useTranslation } from 'react-i18next';
-
-import { Icon } from '@iconify/react';
-
-import featureCoverage from '@/assets/iconify/multi-color/feature-coverage.svg';
-import featureEncryption from '@/assets/iconify/multi-color/feature-encryption.svg';
-import featureSpeed from '@/assets/iconify/multi-color/feature-speed.svg';
-import { useScrollToCenter } from '../useScrollToCenter';
-import Wrapper from './Wrapper';
-
-export function Features() {
-    const { t } = useTranslation();
-    const scrollRef = useScrollToCenter();
-
-    const items = [
-        {
-            titleKey: 'pages.home.features.speed.title',
-            descKey: 'pages.home.features.speed.description',
-            icon: featureSpeed,
-        },
-        {
-            titleKey: 'pages.home.features.coverage.title',
-            descKey: 'pages.home.features.coverage.description',
-            icon: featureCoverage,
-        },
-        {
-            titleKey: 'pages.home.features.encryption.title',
-            descKey: 'pages.home.features.encryption.description',
-            icon: featureEncryption,
-        },
-    ] as const;
-
-    return (
-        <section className="w-full bg-gradient-to-b from-black to-[#030712] pt-12 sm:pt-24 lg:pt-4 pb-20">
-            <Wrapper>
-                <div className="text-center max-w-[672px] mx-auto mb-10 sm:mb-16">
-                    <h2 className="text-2xl sm:text-[32px] font-medium text-white leading-[0.94]">
-                        {t('pages.home.features.title')}
-                    </h2>
-                    <p className="mt-4 text-white/60 text-base text-center leading-[1.5]">
-                        {t('pages.home.features.subtitle')}
-                    </p>
-                </div>
-            </Wrapper>
-            <div
-                ref={scrollRef}
-                className="w-full sm:max-w-full sm:overflow-x-auto sm:overflow-y-hidden sm:no-scrollbar"
-            >
-                <Wrapper className="w-fit min-w-full flex flex-wrap sm:flex-nowrap items-stretch gap-8">
-                    {items.map(({ titleKey, descKey, icon }, index) => (
-                        <div
-                            key={index}
-                            className="transition-all rounded-2xl border border-white/10 bg-white/5 sm:min-h-[246px] w-full sm:w-[406px] p-5 sm:p-8 shrink-0 flex flex-col"
-                        >
-                            <Icon
-                                icon={icon}
-                                className="w-12 h-12 shrink-0 rounded-[14px] mb-5 sm:mb-6"
-                            />
-                            <h3 className="text-base font-semibold text-white">{t(titleKey)}</h3>
-                            <p className="mt-[10px] sm:mt-3 text-white/60 text-base leading-[1.5] flex-1">
-                                {t(descKey)}
-                            </p>
-                        </div>
-                    ))}
-                </Wrapper>
-            </div>
-        </section>
-    );
-}

+ 81 - 37
src/pages/home/components/Hero.tsx

@@ -1,51 +1,95 @@
+import { Icon } from '@iconify/react';
 import { useTranslation } from 'react-i18next';
 
-import { useAppUrls } from '@/hooks/useAppUrls';
+import featureConnection from '@/assets/iconify/multi-color/feature-connection.svg';
+import featurePrivate from '@/assets/iconify/multi-color/feature-private.svg';
+import featureSpeedNew from '@/assets/iconify/multi-color/feature-speed-new.svg';
+import featureTrial from '@/assets/iconify/multi-color/feature-trial.svg';
+import heroEarth from '@/assets/images/home/hero-earth.png';
 
-import heroAppStore from '@/assets/images/home/hero-app-store.png';
-import heroBg from '@/assets/images/home/hero-bg.png';
-import heroDevices from '@/assets/images/home/hero-devices.png';
-import heroGooglePlay from '@/assets/images/home/hero-google-play.png';
-
-import { DownloadButton } from './DownloadButton';
-import Wrapper from './Wrapper';
+const HERO_FEATURES = [
+    {
+        titleKey: 'pages.home.hero.connection.title',
+        descKey: 'pages.home.hero.connection.description',
+        icon: featureConnection,
+    },
+    {
+        titleKey: 'pages.home.hero.trial.title',
+        descKey: 'pages.home.hero.trial.description',
+        icon: featureTrial,
+    },
+    {
+        titleKey: 'pages.home.hero.speed.title',
+        descKey: 'pages.home.hero.speed.description',
+        icon: featureSpeedNew,
+    },
+    {
+        titleKey: 'pages.home.hero.private.title',
+        descKey: 'pages.home.hero.private.description',
+        icon: featurePrivate,
+    },
+] as const;
 
 export function Hero() {
     const { t } = useTranslation();
-    const { appleStoreUrl, downloadUrlByPlatform } = useAppUrls();
 
     return (
-        <section className="relative overflow-hidden w-full">
-            <img
-                src={heroBg}
-                className="abs-tc !max-w-none w-[calc(100%+140px)] !top-[140px] sm:!top-[260px] lg:!top-[155px] lg:!left-[calc(50%+96px)] lg:!w-[78%] object-cover object-center"
-            />
-            <Wrapper className="z-10">
-                <div className="relative w-full pt-[30px] sm:pt-[152px] lg:pb-20 flex flex-col lg:flex-row gap-[60px] sm:gap-20 lg:justify-between items-center">
-                    <div className="w-full lg:w-[59%]">
-                        <div className="w-full max-w-[503px] flex flex-col gap-[22px] md:gap-8">
-                            <h1 className="text-[43px] sm:text-[64px] leading-tight font-normal text-white whitespace-pre-line">
-                                {t('pages.home.hero.title')}
-                            </h1>
-                            <p className="text-[11px] sm:text-base text-white/60 max-w-[503px] leading-[1.5]">
-                                {t('pages.home.hero.subtitle')}
-                            </p>
-                            <div className="flex flex-row flex-wrap gap-[11px] sm:gap-4">
-                                <DownloadButton image={heroAppStore} url={appleStoreUrl ?? ''} />
-                                <DownloadButton
-                                    image={heroGooglePlay}
-                                    url={downloadUrlByPlatform ?? ''}
-                                />
+        <section className="relative w-full overflow-hidden bg-[#010203] -mt-[81px] lg:mt-0">
+            <div className="absolute top-0 left-1/2 -translate-x-1/2 w-[1280px] h-[720px] pointer-events-none">
+                <img
+                    src={heroEarth}
+                    alt=""
+                    className="absolute inset-0 w-full h-full object-cover"
+                />
+                <div className="absolute inset-x-0 top-0 h-[452px] bg-[linear-gradient(180deg,#00040A_70%,#00040A00_100%)]" />
+                <div className="absolute inset-x-0 top-[650px] h-[70px] bg-[linear-gradient(0deg,#00040A_8.57%,#00040A00_100%)]" />
+            </div>
+            <div className="relative z-10 flex flex-col items-center max-w-[1440px] mx-auto">
+                <div className="flex flex-col items-center gap-6 px-5 sm:px-12 lg:px-20 pt-[140px] sm:pt-[200px] lg:pt-[265px] w-full">
+                    <h1 className="text-4xl lg:text-5xl font-black italic text-white text-center uppercase font-[REM] leading-[1.5] tracking-[0.007em]">
+                        {t('pages.home.slogan.title')}
+                    </h1>
+                    <p className="text-base lg:text-xl font-normal text-[#DBEAFE] leading-[1.5] lg:leading-[1.625] max-w-[800px] text-center font-[REM] tracking-[-0.028em] lg:tracking-[-0.022em]">
+                        {t('pages.home.slogan.description')}
+                    </p>
+                </div>
+                <div className="flex flex-col lg:flex-row items-stretch pt-[230px] sm:pt-[140px] lg:pt-[170px] px-5 sm:px-12 lg:px-20 pb-6 w-full">
+                    {HERO_FEATURES.map(({ titleKey, descKey, icon }, index) => (
+                        <div key={index} className="flex flex-col lg:flex-row flex-1 min-w-0">
+                            {index > 0 && (
+                                <div className="hidden lg:block w-px shrink-0 self-stretch bg-[rgba(190,219,255,0.1)]" />
+                            )}
+                            {index > 0 && (
+                                <div className="lg:hidden h-px w-full bg-[rgba(190,219,255,0.1)]" />
+                            )}
+
+                            <div className="lg:hidden flex flex-col gap-[30px] p-10">
+                                <div className="flex flex-row items-center gap-4">
+                                    <Icon icon={icon} className="w-[72px] h-[72px] shrink-0" />
+                                    <h3 className="text-2xl font-bold italic text-white leading-[1.5] whitespace-pre-line font-[REM]">
+                                        {t(titleKey)}
+                                    </h3>
+                                </div>
+                                <p className="text-base font-normal text-[#E6E6E6] leading-[1.5] font-[Barlow]">
+                                    {t(descKey)}
+                                </p>
+                            </div>
+
+                            <div className="hidden lg:flex flex-1 flex-col gap-[30px] p-10">
+                                <Icon icon={icon} className="w-[72px] h-[72px] shrink-0" />
+                                <div className="flex flex-col gap-[14px]">
+                                    <h3 className="text-2xl font-bold italic text-white leading-[1.5] whitespace-pre-line font-[REM]">
+                                        {t(titleKey)}
+                                    </h3>
+                                    <p className="text-base font-normal text-[#E6E6E6] leading-[1.5] font-[Barlow]">
+                                        {t(descKey)}
+                                    </p>
+                                </div>
                             </div>
                         </div>
-                    </div>
-                    <img
-                        src={heroDevices}
-                        alt="hero widgets"
-                        className="w-[280px] sm:w-[80%] lg:w-[41%] max-w-[520px] h-auto object-contain object-center"
-                    />
+                    ))}
                 </div>
-            </Wrapper>
+            </div>
         </section>
     );
 }

+ 0 - 76
src/pages/home/components/Pricing/PlanCard.tsx

@@ -1,76 +0,0 @@
-import { memo } from 'react';
-
-import { useTranslation } from 'react-i18next';
-
-import { PlanTagType } from '@/defines';
-import { Button } from 'antd';
-
-const PLAN_TAG_TYPE_I18N_KEY: Record<PlanTagType, string> = {
-    [PlanTagType.NONE]: '',
-    [PlanTagType.MOST_POPULAR]: 'mostPopular',
-    [PlanTagType.LIMITED_TIME]: 'limitedTime',
-};
-
-export interface PlanCardProps {
-    id: string;
-    title: string;
-    subTitle: string;
-    introduce: string;
-    tag?: string;
-    tagType?: PlanTagType;
-    isSelected?: boolean;
-    onSelected?: () => void;
-    onGetStarted?: () => void;
-}
-
-const PlanCard = memo(
-    ({
-        title,
-        subTitle,
-        introduce,
-        tag,
-        tagType,
-        isSelected = false,
-        onSelected,
-        onGetStarted,
-    }: PlanCardProps) => {
-        const { t } = useTranslation();
-        return (
-            <div
-                className={`relative box-border cursor-pointer transition-all bg-white/10 border-2 flex-col-c p-5 sm:p-8 w-full sm:w-[356px] lg:w-[406px] min-w-[180px] rounded-2xl gap-2 sm:gap-3 shrink-0 ${
-                    isSelected ? 'border-[#0FA4E9]' : 'border-white/10'
-                }`}
-                onClick={onSelected}
-            >
-                {tagType != null && tagType !== PlanTagType.NONE && (
-                    <div className="absolute top-[-13.5px] bg-[#0FA4E9] px-4 start-[21.13px] rounded-full py-1">
-                        <span className="text-black text-[12px] leading-normal sm:text-[14px] font-normal uppercase">
-                            {tag?.trim()
-                                ? tag
-                                : t(`pages.pricing.planTag.${PLAN_TAG_TYPE_I18N_KEY[tagType]}`)}
-                        </span>
-                    </div>
-                )}
-                <h3 className="text-white font-normal leading-[1.5] text-center text-[20px] sm:text-[24px]">
-                    {title}
-                </h3>
-                <span className="text-white font-normal leading-[1.11] text-center text-[24px] sm:text-[28px]">
-                    {subTitle}
-                </span>
-                <span className="text-white/80 font-normal leading-[1.5] text-center text-[16px] sm:text-[20px]">
-                    {introduce}
-                </span>
-                <Button
-                    className={`bg-[#0EA5E9] text-black hover:!bg-[#0081FF] hover:!text-white active:!bg-[#0081FF]/80 active:!text-white/80 text-sm sm:text-base font-normal uppercase rounded-[25px] py-[12px] w-full h-auto border-none`}
-                    onClick={onGetStarted}
-                >
-                    {t('pages.home.pricing.selectPlan')}
-                </Button>
-            </div>
-        );
-    }
-);
-
-PlanCard.displayName = 'PlanCard';
-
-export default PlanCard;

+ 0 - 54
src/pages/home/components/Pricing/index.tsx

@@ -1,54 +0,0 @@
-import { useTranslation } from 'react-i18next';
-import { useNavigate } from 'react-router-dom';
-
-import { useScrollToCenter } from '../../useScrollToCenter';
-import PlanCard from './PlanCard';
-import { useService } from './useService';
-import Wrapper from '../Wrapper';
-import { encryptUrlParams } from '@/utils/requestCrypto';
-
-export function Pricing() {
-    const { t } = useTranslation();
-    const navigate = useNavigate();
-    const { plans, selectedPlanId, setSelectedPlanId } = useService();
-    const scrollRef = useScrollToCenter();
-
-    return (
-        <section className="w-full pt-7 sm:pt-16 lg:pt-[176px]">
-            <Wrapper>
-                <div className="text-center max-w-[672px] mx-auto mb-8 sm:mb-12">
-                    <h2 className="text-2xl sm:text-[32px] font-medium text-white">
-                        {t('pages.home.pricing.title')}
-                    </h2>
-                    <p className="mt-4 text-white/60 text-base text-center">
-                        {t('pages.home.pricing.subtitle')}
-                    </p>
-                </div>
-            </Wrapper>
-            <div
-                ref={scrollRef}
-                className="w-full sm:max-w-full sm:overflow-x-auto sm:overflow-y-hidden sm:no-scrollbar py-4"
-            >
-                <Wrapper className="w-fit min-w-full flex flex-wrap sm:flex-nowrap items-stretch gap-8">
-                    {plans.map((plan) => (
-                        <PlanCard
-                            key={plan.id}
-                            id={plan.id}
-                            title={plan.title}
-                            subTitle={plan.subTitle}
-                            introduce={plan.introduce}
-                            tag={plan.tag}
-                            tagType={plan.tagType}
-                            isSelected={selectedPlanId === plan.id}
-                            onSelected={() => setSelectedPlanId(plan.id)}
-                            onGetStarted={async () => {
-                                const d = await encryptUrlParams({ planId: plan.id });
-                                navigate(`/pricing?d=${d}`);
-                            }}
-                        />
-                    ))}
-                </Wrapper>
-            </div>
-        </section>
-    );
-}

+ 0 - 63
src/pages/home/components/Pricing/useService.tsx

@@ -1,63 +0,0 @@
-import { useEffect, useState } from 'react';
-
-import { PlanTagType } from '@/defines';
-import { fetchPlanList } from '@/services/config';
-import { getUniqueSign } from '@/utils/stringUtils';
-
-export interface Plan {
-    id: string;
-    title: string;
-    subTitle: string;
-    introduce: string;
-    tag?: string;
-    tagType?: PlanTagType;
-    price: number;
-    isDefault: boolean;
-}
-
-export interface UseServiceReturn {
-    plans: Plan[];
-    selectedPlanId: string | null;
-    setSelectedPlanId: (planId: string) => void;
-}
-
-export function useService(): UseServiceReturn {
-    const [plans, setPlans] = useState<Plan[]>([]);
-    const [selectedPlanId, setSelectedPlanId] = useState<string | null>(null);
-
-    useEffect(() => {
-        fetchPlanList({})
-            .then((res) => {
-                const list = res?.data?.list ?? [];
-                const mapped: Plan[] = list.map((item) => ({
-                    id: item.channelItemId ?? getUniqueSign(),
-                    title: item.title ?? '',
-                    subTitle: item.subTitle ?? '',
-                    introduce: item.introduce ?? '',
-                    tag: item.tag,
-                    tagType: item.tagType ?? PlanTagType.NONE,
-                    price: item.price ?? 0,
-                    isDefault: item.isDefault ?? false,
-                }));
-                setPlans(mapped);
-            })
-            .catch(() => {});
-    }, []);
-
-    useEffect(() => {
-        if (plans.length > 0) {
-            for (const plan of plans) {
-                if (plan.isDefault) {
-                    setSelectedPlanId(plan.id);
-                    break;
-                }
-            }
-        }
-    }, [plans]);
-
-    return {
-        plans,
-        selectedPlanId,
-        setSelectedPlanId,
-    };
-}

+ 17 - 0
src/pages/home/components/SectionDivider.tsx

@@ -0,0 +1,17 @@
+export function SectionDivider() {
+    return (
+        <div className="w-full opacity-40">
+            <svg width="100%" height="1" xmlns="http://www.w3.org/2000/svg">
+                <line
+                    x1="0"
+                    y1="0.5"
+                    x2="100%"
+                    y2="0.5"
+                    stroke="white"
+                    strokeWidth="1"
+                    strokeDasharray="12 12"
+                />
+            </svg>
+        </div>
+    )
+}

+ 1 - 1
src/pages/home/components/Wrapper.tsx

@@ -5,7 +5,7 @@ type WrapperProps = {
 };
 
 const Wrapper: React.FC<React.PropsWithChildren<WrapperProps>> = ({ children, className }) => {
-    return <div className={`w-full px-5 sm:px-12 lg:px-20 ${className ?? ''}`}>{children}</div>;
+    return <div className={`w-full px-5 sm:px-8 lg:px-20 ${className ?? ''}`}>{children}</div>;
 };
 
 export default Wrapper;

+ 16 - 12
src/pages/home/index.tsx

@@ -1,18 +1,22 @@
-import { Download } from './components/Download';
-import { Features } from './components/Features';
-import { Hero } from './components/Hero';
-import { Pricing } from './components/Pricing';
+import { AppAccess } from './components/AppAccess'
+import { ChoosePlatform } from './components/ChoosePlatform'
+import { FeatureDetails } from './components/FeatureDetails'
+import { Hero } from './components/Hero'
+import { SectionDivider } from './components/SectionDivider'
 
-/** 设计稿基准宽度 1440px,大于时内容垂直居中;内容区最大 1280px(1440 - 80*2) */
 const Home: React.FC = () => {
     return (
-        <div className="w-full max-w-[1440px] mx-auto flex flex-col justify-center max-[768px]:no-scrollbar">
+        <div className="w-full max-w-[1440px] mx-auto flex flex-col">
             <Hero />
-            <Features />
-            <Pricing />
-            <Download />
+            <SectionDivider />
+            <ChoosePlatform />
+            <SectionDivider />
+            <AppAccess />
+            <SectionDivider />
+            <FeatureDetails />
+            <SectionDivider />
         </div>
-    );
-};
+    )
+}
 
-export default Home;
+export default Home

+ 0 - 26
src/pages/home/useScrollToCenter.ts

@@ -1,26 +0,0 @@
-import { useEffect, useRef } from 'react';
-
-/**
- * 横向滚动容器超出视口时,默认滚动到中间;尺寸变化时重新居中。
- * 返回 ref,挂到可横向滚动的容器上。
- */
-export function useScrollToCenter() {
-    const scrollRef = useRef<HTMLDivElement>(null);
-
-    useEffect(() => {
-        const el = scrollRef.current;
-        if (!el) return;
-        const scrollToCenter = () => {
-            const { scrollWidth, clientWidth } = el;
-            if (scrollWidth > clientWidth) {
-                el.scrollLeft = (scrollWidth - clientWidth) / 2;
-            }
-        };
-        scrollToCenter();
-        const observer = new ResizeObserver(scrollToCenter);
-        observer.observe(el);
-        return () => observer.disconnect();
-    }, []);
-
-    return scrollRef;
-}

+ 12 - 72
src/pages/pricing/useAction.tsx

@@ -1,21 +1,20 @@
-import { useCallback, useEffect } from 'react';
+import { useCallback, useEffect, useMemo } from 'react';
 
 import { FormInstance } from 'antd';
-import { Trans, useTranslation } from 'react-i18next';
+import { useTranslation } from 'react-i18next';
 import { useSearchParams } from 'react-router-dom';
 
-import LoginForm from '@/components/LoginForm';
 import { PayUrlShowType } from '@/defines';
-import { useAppUrls } from '@/hooks/useAppUrls';
+import { useLoginDialog } from '@/hooks/useLoginDialog';
 import { dialogModel } from '@/models/dialogModel';
 import { fetchGetUserConfig, fetchPayOrderCreate } from '@/services/config';
-import { getToken } from '@/utils/authUtils';
-import { decryptUrlParams } from '@/utils/requestCrypto';
-import { currentUnixTimestamp } from '@/utils/timeUtils';
 import { message } from '@/utils/antdAppInstance';
+import { isAuthenticated } from '@/utils/authUtils';
+import { decryptUrlParams } from '@/utils/requestCrypto';
 
 import { PayQrContent } from './components/OrderSummary/PayQrContent';
 import { PayWaitingContent } from './components/OrderSummary/PayWaitingContent';
+
 import type { Plan } from './useService';
 
 const ORDER_TYPE_PLAN = 1;
@@ -26,64 +25,6 @@ export interface UseActionReturn {
     handlePayNow: (plan: Plan, payMethod: string) => void;
 }
 
-function useLoginDialog() {
-    const { t } = useTranslation();
-    const { openDialog, closeDialog } = dialogModel.useModel();
-    const { deeplinkUrl, downloadUrlByPlatform } = useAppUrls();
-
-    return useCallback(() => {
-        const id = openDialog({
-            title: t('pages.pricing.payFlow.loginTitle'),
-            content: (
-                <div className="flex flex-col gap-2">
-                    <p className="text-white/80 text-sm mb-2">
-                        <Trans
-                            i18nKey="pages.pricing.payFlow.loginPrompt"
-                            components={{
-                                linkText: (() => {
-                                    const Wrap = ({ children }: { children?: React.ReactNode }) =>
-                                        deeplinkUrl ? (
-                                            <a
-                                                href={deeplinkUrl}
-                                                className="text-[#0EA5E9] hover:underline"
-                                                target="_blank"
-                                                rel="noopener noreferrer"
-                                            >
-                                                {children}
-                                            </a>
-                                        ) : (
-                                            <span>{children}</span>
-                                        );
-                                    return <Wrap />;
-                                })(),
-                                downloadLink: (() => {
-                                    const Wrap = ({ children }: { children?: React.ReactNode }) =>
-                                        downloadUrlByPlatform ? (
-                                            <a
-                                                href={downloadUrlByPlatform}
-                                                className="text-[#0EA5E9] hover:underline"
-                                                target="_blank"
-                                                rel="noopener noreferrer"
-                                            >
-                                                {children}
-                                            </a>
-                                        ) : (
-                                            <span>{children}</span>
-                                        );
-                                    return <Wrap />;
-                                })(),
-                            }}
-                        />
-                    </p>
-                    <LoginForm onSuccess={() => closeDialog(id)} />
-                </div>
-            ),
-            maskClosable: false,
-            closeable: true,
-        });
-    }, [t, openDialog, closeDialog, deeplinkUrl, downloadUrlByPlatform]);
-}
-
 function useQrPayDialog() {
     const { t } = useTranslation();
     const { openDialog, closeDialog } = dialogModel.useModel();
@@ -190,7 +131,7 @@ export interface UseActionParams {
 
 export function useAction(params?: UseActionParams): UseActionReturn {
     const form = params?.form;
-    const plans = params?.plans ?? [];
+    const plans = useMemo(() => params?.plans ?? [], [params?.plans]);
     const [searchParams] = useSearchParams();
     const openLoginDialog = useLoginDialog();
     const openQrPayDialog = useQrPayDialog();
@@ -219,12 +160,11 @@ export function useAction(params?: UseActionParams): UseActionReturn {
 
     const handlePayNow = useCallback(
         (selectedPlan: Plan, selectedPayMethod: string) => {
-            const token = getToken();
-            const expired =
-                !token?.accessExpires || (token.accessExpires ?? 0) - currentUnixTimestamp() <= 0;
-            if (!token?.accessToken || expired) {
-                // 如果用户未登录,则打开登录对话框
-                openLoginDialog();
+            if (!isAuthenticated()) {
+                openLoginDialog({
+                    titleKey: 'pages.pricing.payFlow.loginTitle',
+                    promptKey: 'pages.pricing.payFlow.loginPrompt',
+                });
                 return;
             }
 

+ 3 - 3
src/router/routes.tsx

@@ -1,19 +1,19 @@
 import { Navigate } from 'react-router-dom';
 
-import type { AppRouteObject } from './types';
-
 import Layout from '@/layouts/BasicLayout';
 import Forbidden from '@/pages/error/403';
 import NotFound from '@/pages/error/404';
 import ServerError from '@/pages/error/500';
-import Redirect from '@/pages/redirect';
 import Home from '@/pages/home';
 // import RouteDemo from '@/pages/routeDemo';
 // import FeatureDemo from '@/pages/featureDemo';
 import Pricing from '@/pages/pricing';
 import PrivacyPolicy from '@/pages/privacyPolicy';
+import Redirect from '@/pages/redirect';
 import TermsOfService from '@/pages/termsOfService';
 
+import type { AppRouteObject } from './types';
+
 const routes: AppRouteObject[] = [
     {
         path: '/',

Vissa filer visades inte eftersom för många filer har ändrats