forked from 77media/video-flow
386 lines
14 KiB
TypeScript
386 lines
14 KiB
TypeScript
"use client";
|
|
|
|
import "../pages/style/top-bar.css";
|
|
import { Button } from "@/components/ui/button";
|
|
import { GradientText } from "@/components/ui/gradient-text";
|
|
import { useTheme } from "next-themes";
|
|
import {
|
|
Sun,
|
|
Moon,
|
|
User,
|
|
Sparkles,
|
|
LogOut,
|
|
PanelsLeftBottom,
|
|
Bell,
|
|
} from 'lucide-react';
|
|
import { motion } from 'framer-motion';
|
|
import ReactDOM from 'react-dom';
|
|
import { useRouter, usePathname } from 'next/navigation';
|
|
import React, { useRef, useEffect, useLayoutEffect, useState } from 'react';
|
|
import { logoutUser } from '@/lib/auth';
|
|
import { createPortalSession, redirectToPortal, getUserSubscriptionInfo } from '@/lib/stripe';
|
|
|
|
|
|
interface User {
|
|
id: string;
|
|
name: string;
|
|
email: string;
|
|
avatar: string;
|
|
username: string;
|
|
}
|
|
|
|
export function TopBar({
|
|
collapsed,
|
|
onToggleSidebar,
|
|
}: {
|
|
collapsed: boolean;
|
|
onToggleSidebar: () => void;
|
|
}) {
|
|
const router = useRouter();
|
|
const [isOpen, setIsOpen] = React.useState(false);
|
|
const menuRef = useRef<HTMLDivElement>(null);
|
|
const buttonRef = useRef<HTMLButtonElement>(null);
|
|
const currentUser: User = JSON.parse(
|
|
localStorage.getItem("currentUser") || "{}"
|
|
);
|
|
const pathname = usePathname()
|
|
const [mounted, setMounted] = React.useState(false);
|
|
const [isLogin, setIsLogin] = useState(false);
|
|
const [isManagingSubscription, setIsManagingSubscription] = useState(false);
|
|
const [subscriptionStatus, setSubscriptionStatus] = useState<string>('');
|
|
const [credits, setCredits] = useState<number>(100);
|
|
const [isLoadingSubscription, setIsLoadingSubscription] = useState(false);
|
|
|
|
// 获取用户订阅信息
|
|
const fetchSubscriptionInfo = async () => {
|
|
if (!currentUser?.id) return;
|
|
|
|
setIsLoadingSubscription(true);
|
|
try {
|
|
const response = await getUserSubscriptionInfo(String(currentUser.id));
|
|
if (response.successful && response.data) {
|
|
setSubscriptionStatus(response.data.subscription_status);
|
|
setCredits(response.data.credits);
|
|
}
|
|
} catch (error) {
|
|
console.error('获取订阅信息失败:', error);
|
|
} finally {
|
|
setIsLoadingSubscription(false);
|
|
}
|
|
};
|
|
|
|
useEffect(() => {
|
|
const token = localStorage.getItem("token");
|
|
if (token) {
|
|
setIsLogin(true);
|
|
} else {
|
|
setIsLogin(false);
|
|
}
|
|
});
|
|
useLayoutEffect(() => {
|
|
console.log("Setting mounted state");
|
|
setMounted(true);
|
|
return () => console.log("Cleanup mounted effect");
|
|
}, []);
|
|
|
|
// 处理订阅管理
|
|
const handleManageSubscription = async () => {
|
|
if (!currentUser?.id) {
|
|
console.error('用户未登录');
|
|
return;
|
|
}
|
|
|
|
setIsManagingSubscription(true);
|
|
try {
|
|
const response = await createPortalSession({
|
|
user_id: String(currentUser.id),
|
|
return_url: window.location.origin + '/dashboard'
|
|
});
|
|
|
|
if (response.successful && response.data?.portal_url) {
|
|
redirectToPortal(response.data.portal_url);
|
|
} else {
|
|
console.log("cannot open the manage subscription");
|
|
return;
|
|
}
|
|
} catch (error) {
|
|
console.log("cannot open the manage subscription");
|
|
return;
|
|
} finally {
|
|
setIsManagingSubscription(false);
|
|
}
|
|
};
|
|
|
|
// 处理点击事件
|
|
useEffect(() => {
|
|
if (!isOpen) return;
|
|
|
|
let isClickStartedInside = false;
|
|
|
|
const handleMouseDown = (event: MouseEvent) => {
|
|
const target = event.target as Node;
|
|
isClickStartedInside = !!(
|
|
menuRef.current?.contains(target) || buttonRef.current?.contains(target)
|
|
);
|
|
};
|
|
|
|
const handleMouseUp = (event: MouseEvent) => {
|
|
const target = event.target as Node;
|
|
const isClickEndedInside = !!(
|
|
menuRef.current?.contains(target) || buttonRef.current?.contains(target)
|
|
);
|
|
|
|
// 只有当点击开始和结束都在外部时才关闭
|
|
if (!isClickStartedInside && !isClickEndedInside) {
|
|
setIsOpen(false);
|
|
}
|
|
isClickStartedInside = false;
|
|
};
|
|
|
|
// 在冒泡阶段处理事件
|
|
document.addEventListener("mousedown", handleMouseDown);
|
|
document.addEventListener("mouseup", handleMouseUp);
|
|
|
|
return () => {
|
|
document.removeEventListener("mousedown", handleMouseDown);
|
|
document.removeEventListener("mouseup", handleMouseUp);
|
|
};
|
|
}, [isOpen]);
|
|
|
|
const handleAnimationEnd = (event: React.AnimationEvent<HTMLDivElement>) => {
|
|
const element = event.currentTarget;
|
|
element.classList.remove("on");
|
|
};
|
|
|
|
const handleMouseEnter = (event: React.MouseEvent<HTMLDivElement>) => {
|
|
const element = event.currentTarget;
|
|
element.classList.add("on");
|
|
};
|
|
|
|
return (
|
|
<div
|
|
className="fixed right-0 top-0 left-0 h-16 header z-[999]"
|
|
style={{ isolation: "isolate" }}
|
|
>
|
|
<div className="h-full flex items-center justify-between pr-6 pl-6">
|
|
<div className="flex items-center space-x-4">
|
|
<div
|
|
className={`flex items-center cursor-pointer space-x-1 link-logo roll event-on`}
|
|
onClick={() => router.push("/")}
|
|
onMouseEnter={handleMouseEnter}
|
|
onAnimationEnd={handleAnimationEnd}
|
|
>
|
|
<span className="translate">
|
|
<span>
|
|
<h1 className="logo text-2xl font-bold">
|
|
<GradientText
|
|
text="MovieFlow"
|
|
startPercentage={30}
|
|
endPercentage={70}
|
|
/>
|
|
</h1>
|
|
</span>
|
|
<span>
|
|
<h1 className="logo text-2xl font-bold">
|
|
<GradientText
|
|
text="MovieFlow"
|
|
startPercentage={30}
|
|
endPercentage={70}
|
|
/>
|
|
</h1>
|
|
</span>
|
|
</span>
|
|
{/* beta标签 */}
|
|
<div className="relative transform translate-y-[-1px]">
|
|
<span className="inline-flex items-center px-1.5 py-0.5 text-[10px] font-semibold tracking-wider text-[rgb(212 202 202)] border border-[rgba(106,244,249,0.2)] rounded-full shadow-[0_0_10px_rgba(106,244,249,0.1)]">
|
|
Beta
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{
|
|
isLogin ?(<div className="flex items-center space-x-4">
|
|
{/* Pricing Link */}
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={() => {
|
|
localStorage.setItem("callBackUrl", pathname);
|
|
window.open("/pricing", "_blank");
|
|
}}
|
|
className="text-gray-300 hover:text-white"
|
|
>
|
|
Pricing
|
|
</Button>
|
|
|
|
{/* Notifications */}
|
|
{/* <Button variant="ghost" size="sm" onClick={() => showQueueNotification(3, 10)}>
|
|
<Bell className="h-4 w-4" />
|
|
</Button> */}
|
|
|
|
{/* Theme Toggle */}
|
|
{/* <Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}
|
|
>
|
|
<Sun className="h-4 w-4 rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
|
|
<Moon className="absolute h-4 w-4 rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
|
|
</Button> */}
|
|
|
|
{/* User Menu */}
|
|
<div className="relative" style={{ isolation: "isolate" }}>
|
|
<Button
|
|
ref={buttonRef}
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={() => {
|
|
console.log("Button clicked, current isOpen:", isOpen);
|
|
if (!isOpen) {
|
|
// 每次打开菜单时重新获取订阅信息
|
|
fetchSubscriptionInfo();
|
|
}
|
|
setIsOpen(!isOpen);
|
|
}}
|
|
data-alt="user-menu-trigger"
|
|
>
|
|
<User className="h-4 w-4" />
|
|
</Button>
|
|
|
|
{mounted && isOpen
|
|
? ReactDOM.createPortal(
|
|
<motion.div
|
|
ref={menuRef}
|
|
initial={{ opacity: 0, scale: 0.95, y: -20 }}
|
|
animate={{ opacity: 1, scale: 1, y: 0 }}
|
|
exit={{ opacity: 0, scale: 0.95, y: -20 }}
|
|
transition={{ duration: 0.2 }}
|
|
style={{
|
|
position: "fixed",
|
|
top: "4rem",
|
|
right: "1rem",
|
|
width: "18rem",
|
|
zIndex: 9999,
|
|
}}
|
|
className="bg-[#1E1E1E] rounded-lg shadow-lg overflow-hidden"
|
|
data-alt="user-menu-dropdown"
|
|
onClick={(e) => e.stopPropagation()}
|
|
>
|
|
{/* User Info */}
|
|
<div className="p-4">
|
|
<div className="flex items-center space-x-3">
|
|
<div className="h-10 w-10 rounded-full bg-[#C73BFF] flex items-center justify-center text-white font-semibold">
|
|
MF
|
|
</div>
|
|
<div className="flex-1">
|
|
<p className="text-sm font-medium">
|
|
{currentUser.name || currentUser.username}
|
|
</p>
|
|
<p className="text-xs text-gray-500">
|
|
{currentUser.email}
|
|
</p>
|
|
</div>
|
|
<div
|
|
className="cursor-pointer hover:text-red-400 transition-colors duration-200"
|
|
onClick={() => {
|
|
logoutUser();
|
|
}}
|
|
title="退出登录"
|
|
>
|
|
<LogOut className="h-4 w-4" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* AI Points */}
|
|
<div className="px-4 py-3 flex items-center justify-between">
|
|
<div className="flex items-center space-x-2">
|
|
<Sparkles className="h-4 w-4" />
|
|
<span className="text-white underline text-sm">
|
|
{isLoadingSubscription ? '...' : `${credits} credits`}
|
|
</span>
|
|
</div>
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
className="text-white border-white hover:bg-white/10 rounded-full px-3 py-1 text-[10px]"
|
|
onClick={() => {
|
|
window.open("/pricing", "_blank");
|
|
}}
|
|
>
|
|
Upgrade
|
|
</Button>
|
|
{subscriptionStatus === 'ACTIVE' && (
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
className="text-white border-white hover:bg-white/10 rounded-full px-3 py-1 text-[10px]"
|
|
onClick={handleManageSubscription}
|
|
disabled={isManagingSubscription}
|
|
>
|
|
Manage
|
|
</Button>
|
|
)}
|
|
</div>
|
|
|
|
{/* Menu Items */}
|
|
<div className="p-2">
|
|
{/* <motion.button
|
|
whileHover={{ backgroundColor: 'rgba(255,255,255,0.1)' }}
|
|
className="w-full flex items-center space-x-2 px-3 py-2 rounded-md text-sm text-white"
|
|
onClick={() => router.push('/my-library')}
|
|
data-alt="my-library-button"
|
|
>
|
|
<Library className="h-4 w-4" />
|
|
<span>My Library</span>
|
|
</motion.button>
|
|
|
|
<motion.button
|
|
whileHover={{ backgroundColor: 'rgba(255,255,255,0.1)' }}
|
|
className="w-full flex items-center space-x-2 px-3 py-2 rounded-md text-sm text-white"
|
|
onClick={() => {
|
|
// 处理退出登录
|
|
setIsOpen(false);
|
|
}}
|
|
data-alt="logout-button"
|
|
>
|
|
<LogOut className="h-4 w-4" />
|
|
<span>Logout</span>
|
|
</motion.button> */}
|
|
|
|
{/* Footer */}
|
|
<div className="mt-4 px-3 py-2 text-xs text-gray-400 text-center">
|
|
<div>Privacy Policy · Terms of Service</div>
|
|
<div>250819215404 | 2025/8/20 06:00:50</div>
|
|
</div>
|
|
</div>
|
|
</motion.div>,
|
|
document.body
|
|
)
|
|
: null}
|
|
</div>
|
|
</div>):(
|
|
<div className="flex items-center space-x-4">
|
|
<button
|
|
data-alt="login-button"
|
|
className="w-[8.5rem] h-[3rem] text-base text-gray-300 hover:text-white transition-colors"
|
|
onClick={() => router.push("/login")}
|
|
>
|
|
Login
|
|
</button>
|
|
<button
|
|
data-alt="signup-button"
|
|
className="w-[8.5rem] h-[3rem] text-base bg-gray-200 text-gray-800 rounded-full hover:bg-white transition-colors"
|
|
onClick={() => router.push("/signup")}
|
|
>
|
|
Sign Up
|
|
</button>
|
|
</div>
|
|
)
|
|
}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|