🎉 feat: initialization

This commit is contained in:
web
2025-11-26 19:56:16 -08:00
commit a801849fb2
553 changed files with 213088 additions and 0 deletions
@@ -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>
);
}
+59
View File
@@ -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>
);
}
+27
View File
@@ -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} />;
}
+82
View File
@@ -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>
);
}