luowuyin's picture
25:05:05 10:41:39 v0.3.7
a572854
"use client";
import { useEffect, useState } from "react";
import Link from "next/link";
import { FiDatabase, FiUsers, FiBarChart2, FiGithub } from "react-icons/fi";
import { CloseOutlined } from "@ant-design/icons";
import { APP_VERSION } from "@/lib/version";
import { message } from "antd";
import { useTranslation } from "react-i18next";
import { motion } from "framer-motion";
import { AnimatedGridPattern } from "@/components/ui/animated-grid-pattern";
import { cn } from "@/lib/utils";
export default function HomePage() {
const { t } = useTranslation("common");
const [isUpdateVisible, setIsUpdateVisible] = useState(false);
const [latestVersion, setLatestVersion] = useState("");
useEffect(() => {
const checkUpdate = async () => {
try {
const response = await fetch(
"https://api.github.com/repos/variantconst/openwebui-monitor/releases/latest"
);
const data = await response.json();
const latestVer = data.tag_name;
if (!latestVer) {
return;
}
const currentVer = APP_VERSION.replace(/^v/, "");
const newVer = latestVer.replace(/^v/, "");
const ignoredVersion = localStorage.getItem("ignoredVersion");
if (currentVer !== newVer && ignoredVersion !== latestVer) {
setLatestVersion(latestVer);
setIsUpdateVisible(true);
}
} catch (error) {
console.error(t("header.messages.updateCheckFailed"), error);
}
};
checkUpdate();
}, [t]);
const handleUpdate = () => {
window.open(
"https://github.com/VariantConst/OpenWebUI-Monitor/releases/latest",
"_blank"
);
setIsUpdateVisible(false);
};
const handleIgnore = () => {
localStorage.setItem("ignoredVersion", latestVersion);
setIsUpdateVisible(false);
message.success(t("update.ignore"));
};
return (
<main className="relative min-h-screen w-full overflow-hidden bg-gradient-to-br from-rose-50 via-slate-50 to-teal-50 pt-16">
<AnimatedGridPattern
numSquares={30}
maxOpacity={0.03}
duration={3}
repeatDelay={1}
className={cn(
"[mask-image:radial-gradient(1200px_circle_at_center,white,transparent)]",
"absolute inset-x-0 inset-y-[-30%] h-[160%] w-full skew-y-12 z-0"
)}
/>
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[1000px] h-[1000px] bg-gradient-to-br from-rose-100/20 via-slate-100/20 to-teal-100/20 rounded-full blur-3xl opacity-40" />
<div className="absolute top-1/4 left-1/4 w-[500px] h-[500px] bg-gradient-to-br from-pink-100/10 to-indigo-100/10 rounded-full blur-3xl opacity-30" />
<div className="absolute bottom-1/4 right-1/4 w-[700px] h-[700px] bg-gradient-to-br from-teal-100/10 to-slate-100/10 rounded-full blur-3xl opacity-30" />
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
className="min-h-[calc(100vh-4rem)] flex flex-col relative z-10"
>
<motion.div className="flex-1 flex flex-col items-center justify-center">
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }}
className="w-full space-y-6 sm:space-y-8 mb-12 sm:mb-16 px-4"
>
<div className="text-center space-y-2">
<motion.h1
initial={{ scale: 0.9, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
className="text-3xl sm:text-4xl md:text-5xl font-bold bg-gradient-to-r from-slate-800 via-slate-700 to-slate-800 bg-clip-text text-transparent mb-2 sm:mb-3 tracking-tight"
>
{t("common.appName")}
</motion.h1>
<motion.p
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.2 }}
className="text-sm sm:text-base md:text-lg text-slate-600/90 max-w-2xl mx-auto font-light"
>
{t("common.description")}
</motion.p>
</div>
</motion.div>
<div className="w-full max-w-2xl px-4 sm:px-6">
<div className="relative">
<div className="absolute inset-0 bg-gradient-to-r from-rose-100/20 via-slate-100/20 to-teal-100/20 blur-3xl -z-10" />
<div className="space-y-4">
{[
{
path: "/models",
icon: <FiDatabase className="w-6 h-6" />,
title: t("home.features.models.title"),
desc: t("home.features.models.description"),
gradient: "from-blue-500/80 to-indigo-500/80",
lightColor: "bg-blue-50/50",
borderColor: "border-blue-200/20",
iconColor: "text-blue-500/70",
},
{
path: "/users",
icon: <FiUsers className="w-6 h-6" />,
title: t("home.features.users.title"),
desc: t("home.features.users.description"),
gradient: "from-rose-500/80 to-pink-500/80",
lightColor: "bg-rose-50/50",
borderColor: "border-rose-200/20",
iconColor: "text-rose-500/70",
},
{
path: "/panel",
icon: <FiBarChart2 className="w-6 h-6" />,
title: t("home.features.stats.title"),
desc: t("home.features.stats.description"),
gradient: "from-emerald-500/80 to-teal-500/80",
lightColor: "bg-emerald-50/50",
borderColor: "border-emerald-200/20",
iconColor: "text-emerald-500/70",
},
].map((item, index) => (
<Link
key={item.path}
href={item.path}
className="group block"
>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: index * 0.1 }}
className="relative bg-white rounded-2xl overflow-hidden transition-all duration-500
shadow-[0_4px_20px_-4px_rgba(0,0,0,0.05)]
hover:shadow-[0_8px_30px_-4px_rgba(0,0,0,0.12)]"
>
<div
className={`absolute inset-0 bg-gradient-to-r ${item.gradient} opacity-0 group-hover:opacity-100 transition-opacity duration-500`}
/>
<div className="relative p-6">
<div className="flex items-center gap-4">
<div
className={cn(
"p-3 rounded-xl transition-all duration-500",
item.lightColor,
item.iconColor,
"shadow-[0_2px_10px_-2px_rgba(0,0,0,0.05)]",
"group-hover:bg-white/10 group-hover:text-white group-hover:shadow-none"
)}
>
{item.icon}
</div>
<div className="flex-1 min-w-0">
<h3 className="text-lg font-medium text-slate-800 group-hover:text-white transition-colors mb-1">
{item.title}
</h3>
<p className="text-sm text-slate-600 group-hover:text-white/90 transition-colors">
{item.desc}
</p>
</div>
<div
className={cn(
"transform transition-all duration-300",
item.iconColor,
"group-hover:text-white group-hover:translate-x-1"
)}
>
<svg
className="w-5 h-5"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={1.5}
d="M9 5l7 7-7 7"
/>
</svg>
</div>
</div>
</div>
</motion.div>
</Link>
))}
</div>
</div>
</div>
</motion.div>
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.6 }}
className="py-8 text-center"
>
<a
href="https://github.com/VariantConst/OpenWebUI-Monitor"
target="_blank"
rel="noopener noreferrer"
className="inline-flex p-2 text-slate-400 hover:text-slate-600 transition-colors"
>
<FiGithub className="w-6 h-6" />
</a>
</motion.div>
</motion.div>
{isUpdateVisible && (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="fixed bottom-4 right-4 z-50 w-auto max-w-[calc(100%-2rem)]"
>
<div className="bg-white rounded-lg shadow-2xl p-3 border border-gray-200 w-fit">
<div className="flex items-center gap-2 text-gray-600 text-sm">
<span className="whitespace-nowrap">
{t("update.newVersion")} {latestVersion}
</span>
<div className="flex gap-2">
<button
onClick={handleIgnore}
className="text-xs hover:text-gray-900 transition-colors"
>
{t("update.ignore")}
</button>
<span className="text-gray-300">|</span>
<button
onClick={handleUpdate}
className="text-xs text-blue-500 hover:text-blue-600 transition-colors"
>
{t("update.update")}
</button>
</div>
<button
onClick={() => setIsUpdateVisible(false)}
className="text-gray-400 hover:text-gray-500 ml-1"
>
<CloseOutlined className="text-[10px]" />
</button>
</div>
</div>
</motion.div>
)}
</main>
);
}