Separate distributor banner and notifications
This commit is contained in:
1 parent
7a142e4845
commit
b850a5d709
5 files changed
+89
-36
No files matched your search
@@ -1,6 +1,7 @@
|
||||
import React, { lazy, Suspense } from 'react';
|
||||
import { Routes, Route, Navigate } from 'react-router-dom';
|
||||
import AuthGuard from './components/AuthGuard';
|
||||
import NotificationBell from './components/NotificationBell';
|
||||
import { ThemeProvider, useTheme } from './context/ThemeContext';
|
||||
|
||||
const Login = lazy(() => import('./pages/Login'));
|
||||
@@ -46,6 +47,7 @@ function ThemedRoutes() {
|
||||
|
||||
<Route path="*" element={<Navigate to="/" replace />} />
|
||||
</Routes>
|
||||
<NotificationBell />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import { Bell, X } from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useSite } from '../context/SiteContext';
|
||||
|
||||
export default function NotificationBell() {
|
||||
const { t } = useTranslation();
|
||||
const { site } = useSite();
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const notifications = useMemo(() => {
|
||||
if (!Array.isArray(site?.notifications)) return [];
|
||||
return site.notifications
|
||||
.filter((item) => item?.content)
|
||||
.slice(0, 20);
|
||||
}, [site?.notifications]);
|
||||
|
||||
if (notifications.length === 0) return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setOpen(true)}
|
||||
className="fixed bottom-6 right-6 z-[80] flex h-14 w-14 items-center justify-center rounded-full border border-brand-500/30 bg-brand-500 text-white shadow-2xl shadow-brand-500/30 transition hover:scale-105 hover:bg-brand-600 focus:outline-none focus:ring-2 focus:ring-brand-400"
|
||||
aria-label={t('notifications.open')}
|
||||
>
|
||||
<Bell size={22} />
|
||||
<span className="absolute -right-1 -top-1 flex h-5 min-w-5 items-center justify-center rounded-full bg-red-500 px-1 text-[11px] font-bold text-white ring-2 ring-[var(--page-bg)]">
|
||||
{notifications.length > 9 ? '9+' : notifications.length}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
{open && (
|
||||
<div className="fixed inset-0 z-[90] flex items-end justify-center bg-black/55 px-4 py-6 backdrop-blur-sm sm:items-center" onClick={() => setOpen(false)}>
|
||||
<div
|
||||
className="w-full max-w-lg overflow-hidden rounded-3xl border border-page-card-border bg-[var(--page-bg)] shadow-2xl"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div className="flex items-center justify-between border-b border-page-card-border bg-page-surface px-5 py-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex h-10 w-10 items-center justify-center rounded-2xl bg-brand-500/15 text-page-link">
|
||||
<Bell size={20} />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-base font-semibold text-page">{t('notifications.title')}</h2>
|
||||
<p className="text-xs text-page-muted">{t('notifications.subtitle')}</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setOpen(false)}
|
||||
className="rounded-full p-2 text-page-muted transition hover:bg-page-surface-hover hover:text-page"
|
||||
aria-label={t('common.close')}
|
||||
>
|
||||
<X size={18} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="max-h-[65vh] space-y-3 overflow-y-auto p-5">
|
||||
{notifications.map((item, index) => (
|
||||
<div key={`${item.publish_date || 'notice'}-${index}`} className="rounded-2xl border border-page-card-border bg-page-surface p-4">
|
||||
<div className="mb-2 inline-flex rounded-full bg-brand-500/10 px-3 py-1 text-xs font-medium text-page-link">
|
||||
{item.publish_date || t('notifications.notice')}
|
||||
</div>
|
||||
<p className="whitespace-pre-wrap text-sm leading-6 text-page-secondary">
|
||||
{item.content}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -9,6 +9,7 @@
|
||||
"nav.signUp": "Sign Up",
|
||||
"nav.logout": "Logout",
|
||||
"nav.contact": "Contact",
|
||||
"common.close": "Close",
|
||||
|
||||
"home.heroSubtitle": "Access the world's most powerful AI models through a single API endpoint. Simple, affordable, reliable.",
|
||||
"home.getStarted": "Get Started Free",
|
||||
@@ -84,9 +85,10 @@
|
||||
"dashboard.redeem": "Redeem",
|
||||
"dashboard.redeemSuccess": "Code redeemed successfully!",
|
||||
"dashboard.quickLinks": "Quick Links",
|
||||
"dashboard.announcements": "Announcements",
|
||||
"dashboard.announcementsDesc": "Latest updates from the site owner",
|
||||
"dashboard.notice": "Notice",
|
||||
"notifications.open": "Open announcements",
|
||||
"notifications.title": "Announcements",
|
||||
"notifications.subtitle": "Latest updates from the site owner",
|
||||
"notifications.notice": "Notice",
|
||||
"dashboard.apiKeys": "API Keys",
|
||||
"dashboard.manageKeys": "Manage your keys",
|
||||
"dashboard.packages": "Packages",
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
"nav.signUp": "注册",
|
||||
"nav.logout": "退出",
|
||||
"nav.contact": "联系我们",
|
||||
"common.close": "关闭",
|
||||
|
||||
"home.heroSubtitle": "通过单一 API 端点访问全球最强大的 AI 模型。简单、实惠、可靠。",
|
||||
"home.getStarted": "免费开始",
|
||||
@@ -84,9 +85,10 @@
|
||||
"dashboard.redeem": "兑换",
|
||||
"dashboard.redeemSuccess": "兑换码兑换成功!",
|
||||
"dashboard.quickLinks": "快捷入口",
|
||||
"dashboard.announcements": "公告通知",
|
||||
"dashboard.announcementsDesc": "站点管理员发布的最新消息",
|
||||
"dashboard.notice": "通知",
|
||||
"notifications.open": "打开公告通知",
|
||||
"notifications.title": "公告通知",
|
||||
"notifications.subtitle": "站点管理员发布的最新消息",
|
||||
"notifications.notice": "通知",
|
||||
"dashboard.apiKeys": "API 密钥",
|
||||
"dashboard.manageKeys": "管理您的密钥",
|
||||
"dashboard.packages": "套餐",
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Megaphone } from 'lucide-react';
|
||||
import { useAuth } from '../context/AuthContext';
|
||||
import {
|
||||
getUserUsage,
|
||||
@@ -175,9 +174,6 @@ export default function Dashboard() {
|
||||
);
|
||||
const hasCustomCommissionRate =
|
||||
currentCommissionRate > defaultCommissionRate + 1e-8;
|
||||
const announcements = Array.isArray(site?.announcements)
|
||||
? site.announcements.filter((item) => item?.content)
|
||||
: (site?.announcement ? [{ content: site.announcement, publish_date: '' }] : []);
|
||||
|
||||
const handleWithdraw = async () => {
|
||||
const amount = Number.parseFloat(withdrawAmount);
|
||||
@@ -315,32 +311,6 @@ export default function Dashboard() {
|
||||
<p className="text-sm text-page-secondary">{t('dashboard.manageDesc')}</p>
|
||||
</div>
|
||||
|
||||
{announcements.length > 0 && (
|
||||
<div className="glass rounded-2xl p-5 mb-8 border border-brand-500/20">
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-brand-500/15 text-page-link">
|
||||
<Megaphone size={17} />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-base font-semibold text-page">{t('dashboard.announcements')}</h2>
|
||||
<p className="text-xs text-page-muted">{t('dashboard.announcementsDesc')}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
{announcements.slice(0, 10).map((item, index) => (
|
||||
<div key={`${item.publish_date || 'notice'}-${index}`} className="flex gap-3 rounded-xl bg-page-surface/60 p-3">
|
||||
<div className="shrink-0 rounded-full border border-brand-500/20 bg-brand-500/10 px-3 py-1 text-xs font-medium text-page-link">
|
||||
{item.publish_date || t('dashboard.notice')}
|
||||
</div>
|
||||
<p className="min-w-0 whitespace-pre-wrap text-sm leading-6 text-page-secondary">
|
||||
{item.content}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-10">
|
||||
<div className="glass rounded-2xl p-6">
|
||||
<p className="text-sm text-page-secondary mb-2">{t('dashboard.balance')}</p>
|
||||
|
||||
Reference in new issue
Block a user