新增 邀请赠积分

This commit is contained in:
moux1024 2025-09-19 20:33:15 +08:00
parent 876da82139
commit 331257e8f4
10 changed files with 358 additions and 146 deletions

View File

@ -1,39 +0,0 @@
import { get, post } from './request'
import { ApiResponse } from './common'
/**
*
*/
export interface CheckinData {
hasCheckedInToday: boolean
points: number
lastCheckinDate: string | null
pointsHistory: Array<{ date: string; points: number; expiryDate: string }>
}
/**
*
*/
export interface CheckinResponse {
success: boolean
points: number
message: string
}
/**
*
* @returns Promise<CheckinData>
*/
export const getCheckinStatus = async (): Promise<CheckinData> => {
const response = await get<ApiResponse<CheckinData>>('/api/user/checkin/status')
return response.data
}
/**
*
* @returns Promise<CheckinResponse>
*/
export const performCheckin = async (): Promise<CheckinResponse> => {
const response = await post<ApiResponse<CheckinResponse>>('/api/user/checkin', {})
return response.data
}

36
api/signin.ts Normal file
View File

@ -0,0 +1,36 @@
import { get, post } from './request'
import { ApiResponse } from './common'
/**
*
*/
export interface SigninData {
has_signin: boolean
credits: number
// signin_record: Array<{ date: string; points: number; expiryDate: string }>
}
/**
*
*/
export interface SigninResponse {
credits: number
}
/**
*
* @returns Promise<SigninData>
*/
export const getSigninStatus = async (): Promise<ApiResponse> => {
const response = await get<ApiResponse<SigninData>>('/api/user_fission/check_today_signin')
return response
}
/**
*
* @returns Promise<SigninResponse>
*/
export const performSignin = async (): Promise<ApiResponse> => {
const response = await post<ApiResponse<SigninResponse>>('/api/user_fission/signin', {})
return response
}

View File

@ -1,6 +1,7 @@
"use client";
import React from 'react';
import { get } from '@/api/request';
import { DashboardLayout } from "@/components/layout/dashboard-layout";
/**
@ -8,12 +9,43 @@ import { DashboardLayout } from "@/components/layout/dashboard-layout";
* Sections: Invite Flow, My Invitation Code, Invite Records (with pagination)
*/
/**
*
*/
type InviteRecord = {
id: string;
invitedUsername: string;
registeredAt: number; // epoch ms
rewardA: string; // reward item 1 (content TBD)
rewardB: string; // reward item 2 (content TBD)
user_email: string;
user_id: string;
user_name: string;
reg_time: number;
purchase_status: number;
reward_status: number;
invite_reward: number;
pay_reward: number;
};
/**
*
*/
type PaginationInfo = {
page: number;
page_size: number;
total: number;
total_pages: number;
has_next: boolean;
has_prev: boolean;
};
/**
* API响应
*/
type InviteRecordsResponse = {
successful: boolean;
code: number;
message: string;
data: {
invited_list: InviteRecord[];
pagination: PaginationInfo;
};
};
const PAGE_SIZE = 10;
@ -37,39 +69,79 @@ function formatLocalTime(epochMs: number): string {
}
/**
* Generate mocked invite records for demo purpose only.
* @param {number} count - number of items
* @returns {InviteRecord[]} - mocked records
*
* @returns {Promise<InviteRecordsResponse>}
*/
function generateMockRecords(count: number): InviteRecord[] {
const now = Date.now();
return Array.from({ length: count }).map((_, index) => ({
id: String(index + 1),
invitedUsername: `user_${index + 1}`,
registeredAt: now - index * 36_000, // different times
rewardA: index % 3 === 0 ? '+100 points' : '—',
rewardB: index % 5 === 0 ? '3-day membership' : '—',
}));
async function fetchInviteRecords(): Promise<InviteRecordsResponse> {
const res = await get<InviteRecordsResponse>('/api/user_fission/query_invite_record');
return res;
}
export default function SharePage(): JSX.Element {
// Mocked data (to be replaced by real API integration later)
const [inviteCode] = React.useState<string>('VF-ABCD-1234');
const [invitedCount] = React.useState<number>(37);
const [inviteCode, setInviteCode] = React.useState<string>('');
const [invitedCount, setInvitedCount] = React.useState<number>(0);
const [totalInviteCredits, setTotalInviteCredits] = React.useState<number>(0);
const [copyState, setCopyState] = React.useState<'idle' | 'copied' | 'error'>('idle');
const [records] = React.useState<InviteRecord[]>(() => generateMockRecords(47));
const [records, setRecords] = React.useState<InviteRecord[]>([]);
const [pagination, setPagination] = React.useState<PaginationInfo>({
page: 1,
page_size: 20,
total: 0,
total_pages: 1,
has_next: false,
has_prev: false
});
const [pageIndex, setPageIndex] = React.useState<number>(0);
const [expandedRowIds, setExpandedRowIds] = React.useState<Set<string>>(() => new Set());
const totalPages = Math.max(1, Math.ceil(records.length / PAGE_SIZE));
const pagedRecords = React.useMemo(() => {
const start = pageIndex * PAGE_SIZE;
return records.slice(start, start + PAGE_SIZE);
}, [records, pageIndex]);
React.useEffect(() => {
let mounted = true;
(async () => {
try {
const response = await fetchInviteRecords();
if (mounted && response.successful) {
setRecords(response.data.invited_list);
setPagination(response.data.pagination);
}
} catch {
// 保持静默失败,页面仍可用
}
// 获取邀请统计信息
try {
const res = await get<any>('/api/user_fission/my_invite_stats');
const stats = res?.data ?? {};
if (mounted) {
if (typeof stats.total_invited === 'number') {
setInvitedCount(stats.total_invited);
}
if (typeof stats.total_invite_credits === 'number') {
setTotalInviteCredits(stats.total_invite_credits);
}
}
} catch {
// 保持静默失败
}
try {
const res = await get<any>('/api/user_fission/my_invite_code');
const code = res?.data?.invite_code ?? res?.data?.inviteCode ?? '';
if (mounted && typeof code === 'string') setInviteCode(code);
} catch {
// 保持静默失败
}
})();
return () => {
mounted = false;
};
}, []);
const totalPages = pagination.total_pages;
const pagedRecords = records; // 后端已经分页直接使用records
const handleCopy = React.useCallback(async () => {
try {
await navigator.clipboard.writeText(inviteCode);
await navigator.clipboard.writeText(location.origin + '/signup?inviteCode=' + inviteCode);
setCopyState('copied');
window.setTimeout(() => setCopyState('idle'), 1600);
} catch {
@ -78,8 +150,18 @@ export default function SharePage(): JSX.Element {
}
}, [inviteCode]);
const canPrev = pageIndex > 0;
const canNext = pageIndex < totalPages - 1;
const toggleRow = React.useCallback((id: string) => {
setExpandedRowIds((prev) => {
const next = new Set(prev);
if (next.has(id)) {
next.delete(id);
} else {
next.add(id);
}
return next;
});
}, []);
return (
<DashboardLayout>
@ -101,22 +183,22 @@ export default function SharePage(): JSX.Element {
<ol data-alt="steps" className="mt-4 grid gap-4 sm:grid-cols-3">
<li data-alt="step" className="rounded-md border border-white/20 p-4">
<div data-alt="step-header" className="flex items-center justify-between">
<span className="text-sm font-medium text-white/80">Step 1</span>
<span className="rounded bg-white/10 px-2 py-0.5 text-xs text-white">Share</span>
<span className="text-sm font-medium text-custom-blue/50">Step 1</span>
<span className="rounded bg-white/10 px-2 py-0.5 text-xs text-white bg-custom-purple/50">Share</span>
</div>
<p data-alt="step-desc" className="mt-2 text-sm text-white/70">Copy your invitation code and share it with friends.</p>
</li>
<li data-alt="step" className="rounded-md border border-white/20 p-4">
<div data-alt="step-header" className="flex items-center justify-between">
<span className="text-sm font-medium text-white/80">Step 2</span>
<span className="rounded bg-white/10 px-2 py-0.5 text-xs text-white">Register</span>
<span className="text-sm font-medium text-custom-blue/50">Step 2</span>
<span className="rounded bg-white/10 px-2 py-0.5 text-xs text-white bg-custom-purple/50">Register</span>
</div>
<p data-alt="step-desc" className="mt-2 text-sm text-white/70">Friends register and enter your invitation code.</p>
</li>
<li data-alt="step" className="rounded-md border border-white/20 p-4">
<div data-alt="step-header" className="flex items-center justify-between">
<span className="text-sm font-medium text-white/80">Step 3</span>
<span className="rounded bg-white/10 px-2 py-0.5 text-xs text-white">Reward</span>
<span className="text-sm font-medium text-custom-blue/50">Step 3</span>
<span className="rounded bg-white/10 px-2 py-0.5 text-xs text-white bg-custom-purple/50">Reward</span>
</div>
<p data-alt="step-desc" className="mt-2 text-sm text-white/70">You both receive rewards after successful registration.</p>
</li>
@ -125,7 +207,7 @@ export default function SharePage(): JSX.Element {
{/* Section 2: My Invitation Code */}
<section data-alt="my-invite-code" className="mb-8 rounded-lg border border-white/20 bg-black p-6 shadow-sm">
<div data-alt="code-panel" className="mt-4 grid gap-6 sm:grid-cols-3">
<div data-alt="code-panel" className="mt-4 grid gap-6 sm:grid-cols-4">
<div data-alt="code-box" className="sm:col-span-2">
<h2 data-alt="section-title" className="text-lg font-medium text-white">My Invitation Code</h2>
<div className="flex items-center gap-3">
@ -144,9 +226,14 @@ export default function SharePage(): JSX.Element {
</div>
<p data-alt="hint" className="mt-2 text-xs text-white/60">Share this code. Your friends can enter it during registration.</p>
</div>
<div data-alt="total-credits" className="flex flex-col items-start justify-center rounded-md border border-white/20 bg-white/5 p-4">
<span className="text-sm text-white/70">Total Credits</span>
<span className="mt-1 text-2xl font-semibold text-transparent bg-clip-text bg-gradient-to-r from-custom-blue to-custom-purple">{totalInviteCredits}</span>
<span className="mt-2 text-xs text-white/60">All credits earned from invitations.</span>
</div>
<div data-alt="invited-count" className="flex flex-col items-start justify-center rounded-md border border-white/20 bg-white/5 p-4">
<span className="text-sm text-white/70">Invited Friends</span>
<span className="mt-1 text-2xl font-semibold text-white">{invitedCount}</span>
<span className="mt-1 text-2xl font-semibold text-transparent bg-clip-text bg-gradient-to-r from-custom-blue to-custom-purple">{invitedCount}</span>
<span className="mt-2 text-xs text-white/60">Points detail will be available soon.</span>
</div>
</div>
@ -162,20 +249,20 @@ export default function SharePage(): JSX.Element {
className="inline-flex h-8 items-center justify-center rounded border border-gray-300 bg-white px-2 text-sm text-gray-700 hover:bg-gray-50 disabled:cursor-not-allowed disabled:opacity-50"
type="button"
onClick={() => setPageIndex((p) => Math.max(0, p - 1))}
disabled={!canPrev}
disabled={!pagination.has_prev}
aria-label="Previous page"
>
Prev
</button>
<span data-alt="page-info" className="text-sm text-white/70">
{pageIndex + 1} / {totalPages}
{pagination.page} / {pagination.total_pages}
</span>
<button
data-alt="next-page"
className="inline-flex h-8 items-center justify-center rounded border border-gray-300 bg-white px-2 text-sm text-gray-700 hover:bg-gray-50 disabled:cursor-not-allowed disabled:opacity-50"
type="button"
onClick={() => setPageIndex((p) => Math.min(totalPages - 1, p + 1))}
disabled={!canNext}
disabled={!pagination.has_next}
aria-label="Next page"
>
Next
@ -184,24 +271,65 @@ export default function SharePage(): JSX.Element {
</div>
<div data-alt="table-wrapper" className="overflow-x-auto">
<table data-alt="records-table" className="min-w-full divide-y divide-white/10">
<table data-alt="records-table" className="min-w-full divide-y divide-white/10 table-fixed">
<thead data-alt="table-head" className="bg-black">
<tr data-alt="table-head-row">
<th className="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-white/60">Invited Username</th>
<th className="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-white/60">Registered At</th>
<th className="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-white/60">Registration Reward</th>
<th className="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-white/60">First Payment Reward</th>
<th className="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-white/60 w-48">Invited Username</th>
<th className="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-white/60 w-56">Registered At</th>
<th className="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-white/60">Reward</th>
</tr>
</thead>
<tbody data-alt="table-body" className="divide-y divide-white/10 bg-black">
{pagedRecords.map((r) => (
<tr key={r.id} data-alt="table-row" className="hover:bg-white/5">
<td className="px-4 py-3 text-sm text-white">{r.invitedUsername}</td>
<td className="px-4 py-3 text-sm text-white/80">{formatLocalTime(r.registeredAt)}</td>
<td className="px-4 py-3 text-sm text-white/90">{r.rewardA}</td>
<td className="px-4 py-3 text-sm text-white/90">{r.rewardB}</td>
</tr>
))}
{pagedRecords.map((r) => {
const isExpanded = expandedRowIds.has(r.user_id);
return (
<React.Fragment key={r.user_id}>
<tr data-alt="table-row" className="hover:bg-white/5">
<td className="px-4 py-3 text-sm text-white w-48">{r.user_name}</td>
<td className="px-4 py-3 text-sm text-white/80 w-56 whitespace-nowrap">{formatLocalTime(r.reg_time * 1000)}</td>
<td className="px-4 py-3 text-sm text-white/90">
<div data-alt="reward-cell" className="flex items-center justify-between gap-3">
<div data-alt="reward-summary" className="flex-1 truncate text-[#FFCC6D]">
{r.invite_reward + r.pay_reward}
</div>
<button
type="button"
data-alt="expand-button"
className="inline-flex items-center rounded border border-white/20 px-2 py-1 text-xs text-white/90 hover:bg-white/10"
aria-expanded={isExpanded}
aria-label={isExpanded ? 'Collapse reward details' : 'Expand reward details'}
onClick={() => toggleRow(r.user_id)}
>
{isExpanded ? 'Hide' : 'Details'}
</button>
</div>
</td>
</tr>
{isExpanded && (
<tr data-alt="row-details">
<td className="px-4 py-0 w-48" />
<td className="px-4 py-0 w-56" />
<td className="px-4 py-3 bg-white/5">
<div data-alt="details-wrapper" className="overflow-x-auto">
<table data-alt="reward-subtable" className="min-w-[320px] text-sm">
<tbody data-alt="subtable-body" className="text-white/90">
<tr>
<td className="px-2 py-2">{r.invite_reward}</td>
<td className="px-2 py-2">Register</td>
</tr>
<tr>
<td className="px-2 py-2">{r.pay_reward}</td>
<td className="px-2 py-2">First Payment</td>
</tr>
</tbody>
</table>
</div>
</td>
</tr>
)}
</React.Fragment>
);
})}
</tbody>
</table>
</div>

View File

@ -3,7 +3,7 @@
import React, { useState } from "react";
import { useRouter } from "next/navigation";
import Link from "next/link";
import { signInWithGoogle, registerUser, sendVerificationLink } from "@/lib/auth";
import { signInWithGoogle, sendVerificationLink, registerUserWithInvite } from "@/lib/auth";
import { GradientText } from "@/components/ui/gradient-text";
import { GoogleLoginButton } from "@/components/ui/google-login-button";
import { Eye, EyeOff, Mail } from "lucide-react";
@ -31,6 +31,16 @@ export default function SignupPage() {
// Handle scroll indicator for small screens
React.useEffect(() => {
try {
const url = new URL(window.location.href);
const codeFromUrl = url.searchParams.get("inviteCode");
const codeFromSession = sessionStorage.getItem("inviteCode");
const code = codeFromUrl || codeFromSession || "";
if (code) {
setInviteCode(code);
sessionStorage.setItem("inviteCode", code);
}
} catch (err) {}
const handleScroll = () => {
const scrollableElement = document.querySelector('.signup-form > div:nth-child(2)');
const formElement = document.querySelector('.signup-form');
@ -208,19 +218,23 @@ export default function SignupPage() {
try {
// Use new registration API
await registerUser({
userName: name,
const response = await registerUserWithInvite({
name: name,
email,
password,
inviteCode: inviteCode || undefined,
invite_code: inviteCode || undefined,
});
// Clear inviteCode after successful registration
try {
sessionStorage.removeItem("inviteCode");
} catch {}
// Show activation modal instead of redirecting to login
setShowActivationModal(true);
setResendCooldown(60);
} catch (error: any) {
console.error("Signup error:", error);
setFormError(error.message||error.msg || "Registration failed, please try again");
setFormError(error.message || "Registration failed, please try again");
} finally {
setIsSubmitting(false);
}

View File

@ -20,7 +20,8 @@ import {
PanelsLeftBottom,
ArrowLeftToLine,
BookHeart,
PanelRightClose
PanelRightClose,
Gift
} from 'lucide-react';
interface SidebarProps {
@ -33,6 +34,7 @@ const navigationItems = [
title: 'Main',
items: [
{ name: 'My Portfolio', href: '/movies', icon: BookHeart },
{ name: 'Share', href: '/share', icon: Gift },
],
}
];

View File

@ -4,15 +4,13 @@ import { useState, useEffect } from "react"
import { Button } from "@/components/ui/button"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { Coins, Trophy, HelpCircle } from "lucide-react"
import { getCheckinStatus, performCheckin, CheckinData } from "@/api/checkin"
import { getSigninStatus, performSignin, SigninData } from "@/api/signin"
export default function CheckinPage() {
const [checkinData, setCheckinData] = useState<CheckinData>({
hasCheckedInToday: false,
points: 0,
lastCheckinDate: null,
pointsHistory: [],
export default function SigninPage() {
const [signinData, setSigninData] = useState<SigninData>({
has_signin: false,
credits: 0
})
const [isLoading, setIsLoading] = useState(false)
const [showTip, setShowTip] = useState(false)
@ -20,15 +18,15 @@ export default function CheckinPage() {
/**
* Fetch checkin status
* Fetch signin status
*/
const fetchCheckinStatus = async () => {
const fetchSigninStatus = async () => {
try {
setIsInitialLoading(true)
const data = await getCheckinStatus()
setCheckinData(data)
const data = await getSigninStatus()
setSigninData(data.data as SigninData)
} catch (error) {
console.error('Failed to fetch checkin status:', error)
console.error('Failed to fetch signin status:', error)
// Keep default state
} finally {
setIsInitialLoading(false)
@ -36,25 +34,25 @@ export default function CheckinPage() {
}
useEffect(() => {
fetchCheckinStatus()
fetchSigninStatus()
}, [])
/**
* Perform checkin operation
* Perform signin operation
*/
const handleCheckin = async () => {
if (checkinData.hasCheckedInToday) return
const handleSignin = async () => {
if (signinData.has_signin) return
try {
setIsLoading(true)
const response = await performCheckin()
const response = await performSignin()
if (response.success) {
// Refresh status after successful checkin
await fetchCheckinStatus()
if (response.successful) {
// Refresh status after successful signin
await fetchSigninStatus()
}
} catch (error) {
console.error('Checkin failed:', error)
console.error('Signin failed:', error)
} finally {
setIsLoading(false)
}
@ -79,14 +77,14 @@ export default function CheckinPage() {
return (
<div className="mx-auto max-w-md space-y-6">
{/* Checkin status card */}
{/* Signin status card */}
<Card className="bg-transparent border-0 shadow-none">
<CardHeader className="text-center pb-4 pt-0">
<h1 className="text-3xl font-bold text-balance bg-gradient-to-r from-custom-blue to-custom-purple bg-clip-text text-transparent">
Daily Check-in
Daily Sign-in
</h1>
<div className="flex items-center justify-center gap-2">
<p className="text-muted-foreground">Check in to earn credits, credits valid for 7 days</p>
<p className="text-muted-foreground">Sign in to earn credits. Credits are valid for 7 days</p>
<div className="relative">
<button
onMouseEnter={() => setShowTip(true)}
@ -98,8 +96,8 @@ export default function CheckinPage() {
{showTip && (
<div className="absolute bottom-full left-1/2 transform -translate-x-1/2 mb-2 w-80 p-3 bg-popover border rounded-lg shadow-lg z-10">
<div className="text-sm space-y-1 text-left">
<p className="font-medium text-foreground">Check-in Rules</p>
<p className="text-muted-foreground"> Daily check-in earns 100 credits</p>
<p className="font-medium text-foreground">Sign-in Rules</p>
<p className="text-muted-foreground"> Daily sign-in earns 100 credits</p>
<p className="text-muted-foreground"> Credits are valid for 7 days</p>
<p className="text-muted-foreground"> Expired credits will be automatically cleared</p>
</div>
@ -118,33 +116,33 @@ export default function CheckinPage() {
<span className="text-sm text-muted-foreground">Current Credits</span>
</div>
<div className="text-3xl font-bold bg-gradient-to-r from-custom-blue to-custom-purple bg-clip-text text-transparent">
{checkinData.points}
{signinData.credits || 0}
</div>
</div>
</div>
{/* Check-in button */}
{/* Sign-in button */}
<Button
onClick={handleCheckin}
disabled={checkinData.hasCheckedInToday || isLoading}
onClick={handleSignin}
disabled={signinData.has_signin || isLoading}
className="w-full h-12 text-lg font-semibold bg-gradient-to-r from-custom-blue to-custom-purple hover:from-custom-blue/90 hover:to-custom-purple/90 text-white"
size="lg"
>
{isLoading ? (
<div className="flex items-center gap-2 text-white">
<div className="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin" />
Checking in...
</div>
) : checkinData.hasCheckedInToday ? (
Signing in...
</div>
) : signinData.has_signin ? (
<div className="flex items-center gap-2 text-white">
<Trophy className="w-4 h-4" />
Checked in Today
Signed in Today
</div>
) : (
<div className="flex items-center gap-2 text-white">
<Coins className="w-4 h-4" />
Check In Now +100 Credits
Sign In Now +100 Credits
</div>
)}
</Button>

View File

@ -33,7 +33,7 @@ import {
} from "@/lib/stripe";
import UserCard from "@/components/common/userCard";
import { showInsufficientPointsNotification } from "@/utils/notifications";
import CheckinBox from "./checkin-box";
import SigninBox from "./signin-box";
interface User {
id: string;
@ -61,7 +61,7 @@ export function TopBar({ collapsed, isDesktop=true }: { collapsed: boolean, isDe
const [isLoadingSubscription, setIsLoadingSubscription] = useState(false);
const [isBuyingTokens, setIsBuyingTokens] = useState(false);
const [customAmount, setCustomAmount] = useState<string>("");
const [isCheckinModalOpen, setIsCheckinModalOpen] = useState(false);
const [isSigninModalOpen, setIsSigninModalOpen] = useState(false);
// 获取用户订阅信息
const fetchSubscriptionInfo = async () => {
@ -249,8 +249,8 @@ export function TopBar({ collapsed, isDesktop=true }: { collapsed: boolean, isDe
/**
* modal
*/
const handleCheckin = () => {
setIsCheckinModalOpen(true);
const handleSignin = () => {
setIsSigninModalOpen(true);
};
return (
@ -408,16 +408,16 @@ export function TopBar({ collapsed, isDesktop=true }: { collapsed: boolean, isDe
{currentUser.email}
</p>
</div>
{/* Check-in entry */}
{/* <div>
{/* Sign-in entry */}
<div>
<button
className="px-2 py-1 text-xs bg-gray-600 text-white rounded hover:bg-gray-700 transition-colors disabled:opacity-50 flex items-center justify-center"
onClick={() => handleCheckin()}
title="Daily Check-in"
onClick={() => handleSignin()}
title="Daily Sign-in"
>
<CalendarDays className="h-3 w-3" />
</button>
</div> */}
</div>
</div>
{/* AI 积分 */}
@ -552,15 +552,15 @@ export function TopBar({ collapsed, isDesktop=true }: { collapsed: boolean, isDe
)}
</div>
{/* Check-in Modal */}
<Dialog open={isCheckinModalOpen} onOpenChange={setIsCheckinModalOpen}>
{/* Sign-in Modal */}
<Dialog open={isSigninModalOpen} onOpenChange={setIsSigninModalOpen}>
<DialogContent
className="max-w-md mx-auto bg-white/95 dark:bg-gray-900/95 backdrop-blur-xl border-0 shadow-2xl"
data-alt="checkin-modal"
data-alt="signin-modal"
>
<DialogTitle></DialogTitle>
<div className="p-4">
<CheckinBox />
<SigninBox />
</div>
</DialogContent>
</Dialog>

View File

@ -8,6 +8,26 @@ const JAVA_BASE_URL = process.env.NEXT_PUBLIC_JAVA_URL || '';
const TOKEN_KEY = 'token';
const USER_KEY = 'currentUser';
/**
*
*/
type RegisterUserData = {
user_id: string;
email: string;
name: string;
invite_code: string;
};
/**
* API响应
*/
type RegisterUserResponse = {
code: number;
message: string;
data: RegisterUserData;
successful: boolean;
};
/**
*
*/
@ -370,7 +390,48 @@ export const registerUser = async ({
}
};
export const registerUserWithInvite = async ({
name,
password,
email,
invite_code,
}: {
name: string;
password: string;
email: string;
invite_code?: string;
}): Promise<RegisterUserResponse> => {
try {
const response = await fetch(`${BASE_URL}/api/user_fission/register_with_invite`, {
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify({
name,
password,
email,
invite_code,
}),
});
const data = await response.json();
if(!data.successful){
throw new Error(data.message || '注册失败');
}
return data as RegisterUserResponse;
} catch (error) {
console.error('Register with invite failed:', error);
throw error;
}
};
/**
*
* @param {string} email -
* @returns {Promise<any>}
*/
export const sendVerificationLink = async (email: string) => {
try {
const response = await fetch(`${JAVA_BASE_URL}/api/user/sendVerificationLink?email=${email}`);

View File

@ -30,6 +30,16 @@ const nextConfig = {
},
};
}
// 优化缓存配置,解决缓存错误问题
if (dev) {
config.cache = {
type: 'memory',
// 设置内存缓存大小限制512MB
maxGenerations: 1,
};
}
return config;
},

View File

@ -8,7 +8,9 @@
"start": "next start",
"lint": "next lint",
"test": "jest",
"test:watch": "jest --watch"
"test:watch": "jest --watch",
"clear-cache": "bash scripts/clear-cache.sh",
"dev:clean": "npm run clear-cache && npm run dev"
},
"dependencies": {
"@dnd-kit/core": "^6.3.1",