🎉 feat: initialization
This commit is contained in:
@@ -0,0 +1,53 @@
|
||||
import { DotLottieReact } from "@lottiefiles/dotlottie-react";
|
||||
import { motion } from "framer-motion";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export function GlobalMap() {
|
||||
const { t } = useTranslation("main");
|
||||
return (
|
||||
<motion.section
|
||||
initial={{ opacity: 0 }}
|
||||
transition={{ duration: 0.5 }}
|
||||
viewport={{ once: true }}
|
||||
whileInView={{ opacity: 1 }}
|
||||
>
|
||||
<motion.h2
|
||||
className="mb-2 text-center font-bold text-3xl"
|
||||
initial={{ opacity: 0, y: -20 }}
|
||||
transition={{ duration: 0.5 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
>
|
||||
{t("global_map_itle", "Global Connection, Easy and Worry-free")}
|
||||
</motion.h2>
|
||||
<motion.p
|
||||
className="mb-8 text-center text-lg text-muted-foreground"
|
||||
initial={{ opacity: 0, y: -20 }}
|
||||
transition={{ duration: 0.5, delay: 0.1 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
>
|
||||
{t(
|
||||
"global_map_description",
|
||||
"Explore seamless global connectivity. Choose network services that suit your needs and stay connected anytime, anywhere."
|
||||
)}
|
||||
</motion.p>
|
||||
<motion.div
|
||||
animate={{ scale: 1, opacity: 1 }}
|
||||
className="aspect-video w-full overflow-hidden"
|
||||
initial={{ scale: 0.9, opacity: 0 }}
|
||||
transition={{
|
||||
type: "spring",
|
||||
stiffness: 100,
|
||||
damping: 15,
|
||||
delay: 0.4,
|
||||
}}
|
||||
>
|
||||
<DotLottieReact
|
||||
autoplay
|
||||
className="w-full scale-150"
|
||||
loop
|
||||
src="/lotties/global-map.json"
|
||||
/>
|
||||
</motion.div>
|
||||
</motion.section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import { DotLottieReact } from "@lottiefiles/dotlottie-react";
|
||||
import { Link } from "@tanstack/react-router";
|
||||
import { HoverBorderGradient } from "@workspace/ui/components/hover-border-gradient";
|
||||
import { TextGenerateEffect } from "@workspace/ui/components/text-generate-effect";
|
||||
import { motion } from "framer-motion";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useGlobalStore } from "@/stores/global";
|
||||
|
||||
export function Hero() {
|
||||
const { t } = useTranslation("main");
|
||||
const { common, user } = useGlobalStore();
|
||||
const { site } = common;
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
className="grid gap-8 pt-16 sm:grid-cols-2"
|
||||
initial={{ opacity: 0, y: -50 }}
|
||||
transition={{ type: "spring", stiffness: 100, damping: 20 }}
|
||||
viewport={{ once: true, amount: 0.2 }}
|
||||
>
|
||||
<motion.div
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
className="flex flex-col items-start justify-center"
|
||||
initial={{ opacity: 0, y: 50 }}
|
||||
transition={{ type: "spring", stiffness: 80, damping: 15, delay: 0.3 }}
|
||||
viewport={{ once: true, amount: 0.3 }}
|
||||
>
|
||||
<h1 className="my-6 font-bold text-4xl lg:text-6xl">
|
||||
{t("welcome", "Welcome to")} {site.site_name}
|
||||
</h1>
|
||||
{site.site_desc && (
|
||||
<TextGenerateEffect
|
||||
className="mb-8 max-w-xl *:text-muted-foreground"
|
||||
words={site.site_desc}
|
||||
/>
|
||||
)}
|
||||
<Link to={user ? "/dashboard" : "/auth"}>
|
||||
<HoverBorderGradient
|
||||
as="button"
|
||||
className="m-0.5 flex items-center space-x-2 text-white"
|
||||
containerClassName="rounded-full"
|
||||
>
|
||||
{t("started", "Get Started")}
|
||||
</HoverBorderGradient>
|
||||
</Link>
|
||||
</motion.div>
|
||||
<motion.div
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
className="flex w-full"
|
||||
initial={{ opacity: 0, y: 50 }}
|
||||
transition={{ type: "spring", stiffness: 80, damping: 15, delay: 0.5 }}
|
||||
viewport={{ once: true, amount: 0.3 }}
|
||||
>
|
||||
<DotLottieReact autoplay loop src="/lotties/network-security.json" />
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import { useNavigate } from "@tanstack/react-router";
|
||||
import { useEffect } from "react";
|
||||
import { useGlobalStore } from "@/stores/global";
|
||||
import { GlobalMap } from "./global-map";
|
||||
import { Hero } from "./hero";
|
||||
import { ProductShowcase } from "./product-showcase";
|
||||
import { Stats } from "./stats";
|
||||
|
||||
export default function Main() {
|
||||
const { user } = useGlobalStore();
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
if (user) {
|
||||
navigate({ to: "/dashboard" });
|
||||
}
|
||||
}, [user, navigate]);
|
||||
|
||||
return (
|
||||
<main className="container space-y-16">
|
||||
<Hero />
|
||||
<Stats />
|
||||
<ProductShowcase />
|
||||
<GlobalMap />
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
import { Link } from "@tanstack/react-router";
|
||||
import { Button } from "@workspace/ui/components/button";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
} from "@workspace/ui/components/card";
|
||||
import { Separator } from "@workspace/ui/components/separator";
|
||||
import { Icon } from "@workspace/ui/composed/icon";
|
||||
import { cn } from "@workspace/ui/lib/utils";
|
||||
import { motion } from "framer-motion";
|
||||
import type { Key, ReactNode } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Display } from "@/components/display";
|
||||
import { SubscribeDetail } from "@/sections/subscribe/detail";
|
||||
import { useGlobalStore } from "@/stores/global";
|
||||
|
||||
interface ProductShowcaseProps {
|
||||
subscriptionData: API.Subscribe[];
|
||||
}
|
||||
|
||||
export function Content({ subscriptionData }: ProductShowcaseProps) {
|
||||
const { t } = useTranslation("main");
|
||||
const { user } = useGlobalStore();
|
||||
|
||||
const unitTimeMap: Record<string, string> = {
|
||||
Day: t("Day", "Day"),
|
||||
Hour: t("Hour", "Hour"),
|
||||
Minute: t("Minute", "Minute"),
|
||||
Month: t("Month", "Month"),
|
||||
NoLimit: t("NoLimit", "No Limit"),
|
||||
Year: t("Year", "Year"),
|
||||
};
|
||||
|
||||
return (
|
||||
<motion.section
|
||||
initial={{ opacity: 0 }}
|
||||
transition={{ duration: 0.5 }}
|
||||
viewport={{ once: true }}
|
||||
whileInView={{ opacity: 1 }}
|
||||
>
|
||||
<motion.h2
|
||||
className="mb-2 text-center font-bold text-3xl"
|
||||
initial={{ opacity: 0, y: -20 }}
|
||||
transition={{ duration: 0.5 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
>
|
||||
{t("product_showcase_title", "Choose Your Package")}
|
||||
</motion.h2>
|
||||
<motion.p
|
||||
className="mb-8 text-center text-lg text-muted-foreground"
|
||||
initial={{ opacity: 0, y: -20 }}
|
||||
transition={{ duration: 0.5, delay: 0.1 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
>
|
||||
{t(
|
||||
"product_showcase_description",
|
||||
"Let us help you select the package that best suits you and enjoy exploring it."
|
||||
)}
|
||||
</motion.p>
|
||||
<div className="mx-auto flex flex-wrap justify-center gap-8 overflow-x-auto overflow-y-hidden *:max-w-80 *:flex-auto">
|
||||
{subscriptionData?.map((item, index) => (
|
||||
<motion.div
|
||||
className="w-1/2 lg:w-1/4"
|
||||
initial={{ opacity: 0, y: 50 }}
|
||||
key={item.id}
|
||||
transition={{ duration: 0.5, delay: index * 0.1 }}
|
||||
viewport={{ once: true, amount: 0.5 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
>
|
||||
<Card className="flex flex-col gap-0 overflow-hidden rounded-lg py-0 shadow-lg transition-shadow duration-300 hover:shadow-2xl">
|
||||
<CardHeader className="bg-muted/50 p-4 font-medium text-xl">
|
||||
{item.name}
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-grow flex-col gap-4 p-6 text-sm">
|
||||
<ul className="flex flex-grow flex-col gap-3">
|
||||
{(() => {
|
||||
let parsedDescription: {
|
||||
description: string;
|
||||
features: Array<{
|
||||
icon: string;
|
||||
label: ReactNode;
|
||||
type: "default" | "success" | "destructive";
|
||||
}>;
|
||||
};
|
||||
try {
|
||||
parsedDescription = JSON.parse(item.description);
|
||||
} catch {
|
||||
parsedDescription = { description: "", features: [] };
|
||||
}
|
||||
|
||||
const { description, features } = parsedDescription;
|
||||
return (
|
||||
<>
|
||||
{description && (
|
||||
<li className="text-muted-foreground">
|
||||
{description}
|
||||
</li>
|
||||
)}
|
||||
{features?.map(
|
||||
(
|
||||
feature: {
|
||||
type: string;
|
||||
icon: string;
|
||||
label: ReactNode;
|
||||
},
|
||||
index: Key
|
||||
) => (
|
||||
<li
|
||||
className={cn("flex items-center gap-2", {
|
||||
"text-muted-foreground line-through":
|
||||
feature.type === "destructive",
|
||||
})}
|
||||
key={index}
|
||||
>
|
||||
{feature.icon && (
|
||||
<Icon
|
||||
className={cn("size-5 text-primary", {
|
||||
"text-green-500":
|
||||
feature.type === "success",
|
||||
"text-destructive":
|
||||
feature.type === "destructive",
|
||||
})}
|
||||
icon={feature.icon}
|
||||
/>
|
||||
)}
|
||||
{feature.label}
|
||||
</li>
|
||||
)
|
||||
)}
|
||||
</>
|
||||
);
|
||||
})()}
|
||||
</ul>
|
||||
<SubscribeDetail
|
||||
subscribe={{
|
||||
...item,
|
||||
name: undefined,
|
||||
}}
|
||||
/>
|
||||
</CardContent>
|
||||
<Separator />
|
||||
<CardFooter className="relative flex flex-col gap-4 p-4">
|
||||
<motion.h2
|
||||
animate={{ opacity: 1 }}
|
||||
className="pb-4 font-semibold text-2xl sm:text-3xl"
|
||||
initial={{ opacity: 0 }}
|
||||
transition={{ duration: 0.5, delay: 0.2 }}
|
||||
>
|
||||
<Display type="currency" value={item.unit_price} />
|
||||
<span className="font-medium text-base">
|
||||
/
|
||||
{unitTimeMap[item.unit_time!] ||
|
||||
t(item.unit_time || "Month", item.unit_time || "Month")}
|
||||
</span>
|
||||
</motion.h2>
|
||||
<motion.div>
|
||||
<Button
|
||||
asChild
|
||||
className="absolute bottom-0 left-0 w-full rounded-t-none rounded-b-xl"
|
||||
>
|
||||
<Link
|
||||
search={user ? undefined : { id: item.id }}
|
||||
to={user ? "/subscribe" : "/purchasing"}
|
||||
>
|
||||
{t("subscribe", "Subscribe")}
|
||||
</Link>
|
||||
</Button>
|
||||
</motion.div>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</motion.section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import { getSubscription } from "@workspace/ui/services/user/portal";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Content } from "./content";
|
||||
|
||||
export function ProductShowcase() {
|
||||
const { i18n } = useTranslation();
|
||||
const [subscriptionList, setSubscriptionList] = useState<API.Subscribe[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchSubscriptions = async () => {
|
||||
try {
|
||||
const { data } = await getSubscription(
|
||||
{
|
||||
language: i18n.language,
|
||||
},
|
||||
{
|
||||
skipErrorHandler: true,
|
||||
}
|
||||
);
|
||||
setSubscriptionList(data.data?.list || []);
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch subscriptions:", error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchSubscriptions();
|
||||
}, [i18n.language]);
|
||||
|
||||
if (isLoading || subscriptionList.length === 0) return null;
|
||||
|
||||
return <Content subscriptionData={subscriptionList} />;
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
import { DotLottieReact } from "@lottiefiles/dotlottie-react";
|
||||
import { motion } from "framer-motion";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export function Stats() {
|
||||
const { t } = useTranslation("main");
|
||||
|
||||
const list = [
|
||||
{
|
||||
name: t("users", "Users"),
|
||||
description: t("users_description", "Trusted by users worldwide"),
|
||||
icon: (
|
||||
<DotLottieReact
|
||||
autoplay
|
||||
className="size-24"
|
||||
loop
|
||||
src="/lotties/users.json"
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
name: t("servers", "Servers"),
|
||||
description: t(
|
||||
"servers_description",
|
||||
"High-performance servers globally"
|
||||
),
|
||||
icon: (
|
||||
<DotLottieReact
|
||||
autoplay
|
||||
className="size-24"
|
||||
loop
|
||||
src="/lotties/servers.json"
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
name: t("locations", "Locations"),
|
||||
description: t("locations_description", "Available in multiple regions"),
|
||||
icon: (
|
||||
<DotLottieReact
|
||||
autoplay
|
||||
className="size-24"
|
||||
loop
|
||||
src="/lotties/locations.json"
|
||||
/>
|
||||
),
|
||||
},
|
||||
];
|
||||
return (
|
||||
<motion.section
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
className="z-10 grid w-full grid-cols-1 divide-y-2 divide-muted rounded-lg sm:grid-cols-3 sm:divide-x-2 sm:divide-y-0"
|
||||
initial={{ opacity: 0, y: 50 }}
|
||||
transition={{ duration: 1, ease: "easeOut" }}
|
||||
viewport={{ once: true, amount: 0.8 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
>
|
||||
{list.map((item, index) => (
|
||||
<motion.div
|
||||
className="mx-auto flex w-10/12 items-center justify-start px-4 py-4 sm:w-full sm:justify-center sm:py-6"
|
||||
initial={{ opacity: 0, scale: 0.8 }}
|
||||
key={item.name}
|
||||
transition={{ duration: 0.8, delay: index * 0.3, ease: "easeOut" }}
|
||||
viewport={{ once: true, amount: 0.8 }}
|
||||
whileInView={{ opacity: 1, scale: 1 }}
|
||||
>
|
||||
<div className="flex w-full items-center sm:w-auto">
|
||||
<div className="mr-4 flex h-20 w-20 items-center justify-center rounded-full">
|
||||
{item.icon}
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<p className="font-semibold text-lg">{item.name}</p>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
{item.description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</motion.section>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user