diff --git a/apps/user/app/(main)/page.tsx b/apps/user/app/(main)/page.tsx index fe43f4b..a62603b 100644 --- a/apps/user/app/(main)/page.tsx +++ b/apps/user/app/(main)/page.tsx @@ -1,246 +1,15 @@ -'use client'; +import { GlobalMap } from '@/components/main/global-map'; +import { Hero } from '@/components/main/hero'; +import { ProductShowcase } from '@/components/main/product-showcase'; +import { Stats } from '@/components/main/stats'; -import useGlobalStore from '@/config/use-global'; -import { - GiftIcon, - GlobalMapIcon, - LocationsIcon, - NetworkSecurityIcon, - ServersIcon, - UsersIcon, -} from '@repo/ui/lotties'; -import { Button, buttonVariants } from '@shadcn/ui/button'; -import { AnimationProps, motion, MotionProps } from 'framer-motion'; -import { useTranslations } from 'next-intl'; -import Link from 'next/link'; -import { useMemo } from 'react'; - -function ScrollAnimationWrapper({ - children, - className, - ...props -}: AnimationProps & - MotionProps & { - className?: string; - }) { - return ( - - {children} - - ); -} -function getScrollAnimation() { - return { - offscreen: { - y: 150, - opacity: 0, - }, - onscreen: ({ duration = 2 } = {}) => ({ - y: 0, - opacity: 1, - transition: { - type: 'spring', - duration, - }, - }), - }; -} - -const listUser = [ - { - name: 'Users', - number: '390', - icon: , - }, - { - name: 'Locations', - number: '20', - icon: , - }, - { - name: 'Server', - number: '50', - icon: , - }, -]; - -export default function Page() { - const scrollAnimation = useMemo(() => getScrollAnimation(), []); - const { common, user } = useGlobalStore(); - const { site } = common; - const t = useTranslations('index'); +export default function Home() { return (
- - -
-

- {t('welcome')} {site.site_name} -

-

{site.site_desc}

-
- - {t('started')} - -
-
-
- - - -
-
-
- -
- {listUser.map((item, index) => ( - -
-
- {item.icon} -
-
-

{item.number}+

-

{item.name}

-
-
-
- ))} -
-
-
-
- - - {t('choose_plan')} - - - {t('choose_plan_desc')} - - -
- - - -

Free Plan

-
    -
  • Unlimited Bandwitch
  • -
  • Encrypted Connection
  • -
  • No Traffic Logs
  • -
  • Works on All Devices
  • -
-
-

Free

- -
-
-
- - - -

Standard Plan

-
    -
  • Unlimited Bandwitch
  • -
  • Encrypted Connection
  • -
  • No Traffic Logs
  • -
  • Works on All Devices
  • -
  • Connect Anyware
  • -
-
-

- $9 / mo -

- -
-
-
- - - -

Premium Plan

-
    -
  • Unlimited Bandwitch
  • -
  • Encrypted Connection
  • -
  • No Traffic Logs
  • -
  • Works on All Devices
  • -
  • Connect Anyware
  • -
  • Get New Features
  • -
-
-

- $12 / mo -

- - -
-
-
-
-
-
- - - {t('huge_network')} {site.site_name} - - - {site.site_name} {t('global_network_desc')} - - - - - - - + + + +
); } diff --git a/apps/user/components/main/global-map.tsx b/apps/user/components/main/global-map.tsx new file mode 100644 index 0000000..8a464a5 --- /dev/null +++ b/apps/user/components/main/global-map.tsx @@ -0,0 +1,47 @@ +'use client'; + +import { GlobalMapIcon } from '@repo/ui/lotties'; +import { motion } from 'framer-motion'; +import { useTranslations } from 'next-intl'; + +export function GlobalMap() { + const t = useTranslations('index'); + return ( + + + {t('global_map_itle')} + + + {t('global_map_description')} + + + + + + ); +} diff --git a/apps/user/components/main/hero.tsx b/apps/user/components/main/hero.tsx new file mode 100644 index 0000000..3fec194 --- /dev/null +++ b/apps/user/components/main/hero.tsx @@ -0,0 +1,61 @@ +'use client'; + +import useGlobalStore from '@/config/use-global'; +import { NetworkSecurityIcon } from '@repo/ui/lotties'; +import { HoverBorderGradient } from '@shadcn/ui/hover-border-gradient'; +import { TextGenerateEffect } from '@shadcn/ui/text-generate-effect'; +import { motion } from 'framer-motion'; +import { useTranslations } from 'next-intl'; +import Link from 'next/link'; + +export function Hero() { + const t = useTranslations('index'); + const { common, user } = useGlobalStore(); + const { site } = common; + + return ( + + +

+ {t('welcome')} {site.site_name} +

+ {site.site_desc && ( + + )} + + + {t('started')} + + +
+ + + +
+ ); +} diff --git a/apps/user/components/main/product-showcase.tsx b/apps/user/components/main/product-showcase.tsx new file mode 100644 index 0000000..c7955e2 --- /dev/null +++ b/apps/user/components/main/product-showcase.tsx @@ -0,0 +1,133 @@ +'use client'; + +import { SubscribeDetail } from '@/app/(main)/(user)/subscribe/detail'; +import { Display } from '@/components/display'; +import { getSubscription } from '@/services/common/common'; +import { Icon } from '@iconify/react'; +import { Button } from '@shadcn/ui/button'; +import { Card, CardContent, CardFooter, CardHeader } from '@shadcn/ui/card'; +import { cn } from '@shadcn/ui/lib/utils'; +import { Separator } from '@shadcn/ui/separator'; +import { useQuery } from '@tanstack/react-query'; +import { motion } from 'framer-motion'; +import { useTranslations } from 'next-intl'; + +export function ProductShowcase() { + const t = useTranslations('index'); + + const { data } = useQuery({ + queryKey: ['getSubscription'], + queryFn: async () => { + const { data } = await getSubscription({ + skipErrorHandler: true, + }); + return data.data?.list || []; + }, + }); + if (data?.length === 0) return null; + return ( + + + {t('product_showcase_title')} + + + {t('product_showcase_description')} + +
+ {data?.map((item, index) => ( + + + {item.name} + +
    + {(() => { + let parsedDescription; + try { + parsedDescription = JSON.parse(item.description); + } catch { + parsedDescription = { description: '', features: [] }; + } + + const { description, features } = parsedDescription; + return ( + <> + {description &&
  • {description}
  • } + {features.map((feature, index) => ( +
  • + {feature.icon && ( + + )} + {feature.label} +
  • + ))} + + ); + })()} +
+ +
+ + + + + /{t('per_month')} + + + + + +
+
+ ))} +
+
+ ); +} diff --git a/apps/user/components/main/stats.tsx b/apps/user/components/main/stats.tsx new file mode 100644 index 0000000..ba4a506 --- /dev/null +++ b/apps/user/components/main/stats.tsx @@ -0,0 +1,104 @@ +'use client'; + +import { getStat } from '@/services/common/common'; +import { LocationsIcon, ServersIcon, UsersIcon } from '@repo/ui/lotties'; +import { useQuery } from '@tanstack/react-query'; +import { motion } from 'framer-motion'; +import { useTranslations } from 'next-intl'; +import { useEffect, useState } from 'react'; + +export function Stats() { + const t = useTranslations('index'); + + const { data } = useQuery({ + queryKey: ['getStat'], + queryFn: async () => { + const { data } = await getStat({ + skipErrorHandler: true, + }); + return data.data; + }, + refetchOnWindowFocus: false, + }); + + const list = [ + { + name: t('users'), + number: data?.user || 999, + icon: , + }, + { + name: t('servers'), + number: data?.server || 30, + icon: , + }, + { + name: t('locations'), + number: data?.country || 10, + icon: , + }, + ]; + return ( + + {list.map((item, index) => ( + +
+
+ {item.icon} +
+
+

+ + +

+

{item.name}

+
+
+
+ ))} +
+ ); +} + +function CountUp({ end, duration = 2000 }: { end: number; duration?: number }) { + const [count, setCount] = useState(0); + + useEffect(() => { + let startTime; + let animationFrame; + + const easeOutQuad = (t) => t * (2 - t); + + const updateCount = (timestamp) => { + if (!startTime) startTime = timestamp; + const progress = timestamp - startTime; + const easedProgress = easeOutQuad(Math.min(progress / duration, 1)); + const nextCount = Math.round(easedProgress * end); + + setCount(nextCount); + + if (progress < duration) { + animationFrame = requestAnimationFrame(updateCount); + } + }; + + animationFrame = requestAnimationFrame(updateCount); + + return () => cancelAnimationFrame(animationFrame); + }, [end, duration]); + + return <>{count.toLocaleString()}; +} diff --git a/apps/user/locales/en-US/index.json b/apps/user/locales/en-US/index.json index bab81cd..d52d41e 100644 --- a/apps/user/locales/en-US/index.json +++ b/apps/user/locales/en-US/index.json @@ -1,9 +1,14 @@ { - "choose_plan": "Choose Your Plan", - "choose_plan_desc": "Let's select the plan that best suits you and explore it happily.", - "global_network_desc": "Easily access our services while on the move.", - "huge_network": "Vast and Fast Global Network", + "global_map_description": "Explore seamless global connectivity. Choose network services that suit your needs and stay connected anytime, anywhere.", + "global_map_title": "Global Connectivity, Effortless Peace of Mind", + "locations": "Locations", + "per_month": "Per Month", + "product_showcase_description": "Let us help you select the package that best suits you and enjoy exploring it.", + "product_showcase_title": "Choose Your Package", + "servers": "Servers", "started": "Get Started", + "subscribe": "Subscribe", "tos": "Terms of Service", + "users": "Users", "welcome": "Welcome to" } diff --git a/apps/user/locales/zh-CN/index.json b/apps/user/locales/zh-CN/index.json index 3ecde90..36f9326 100644 --- a/apps/user/locales/zh-CN/index.json +++ b/apps/user/locales/zh-CN/index.json @@ -1,9 +1,14 @@ { - "choose_plan": "选择您的套餐", - "choose_plan_desc": "让我们选择最适合您的套餐,快乐地探索它。", - "global_network_desc": "让您在移动地点时更轻松地看到我们的服务。", - "huge_network": "庞大的快速全球网络", + "global_map_description": "探索无缝的全球连接。选择符合您需求的网络服务,随时随地保持连接。", + "global_map_itle": "全球互联,轻松无忧", + "locations": "地区", + "per_month": "每月", + "product_showcase_description": "让我们选择最适合您的套餐,快乐地探索它。", + "product_showcase_title": "选择您的套餐", + "servers": "服务器", "started": "开始使用", + "subscribe": "订阅", "tos": "服务协议", + "users": "用户", "welcome": "欢迎来到" } diff --git a/apps/user/utils/request.ts b/apps/user/utils/request.ts index 4bb3d6d..7f7fc37 100644 --- a/apps/user/utils/request.ts +++ b/apps/user/utils/request.ts @@ -28,6 +28,7 @@ requset.interceptors.request.use( async ( config: InternalAxiosRequestConfig & { Authorization?: string; + skipErrorHandler?: boolean; }, ) => { const Authorization = getAuthorization(config.Authorization);