forked from 77media/video-flow
新增 邀请赠积分
This commit is contained in:
parent
876da82139
commit
331257e8f4
@ -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
36
api/signin.ts
Normal 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
|
||||
}
|
||||
@ -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>
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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 },
|
||||
],
|
||||
}
|
||||
];
|
||||
|
||||
@ -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>
|
||||
@ -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>
|
||||
|
||||
61
lib/auth.ts
61
lib/auth.ts
@ -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}`);
|
||||
|
||||
@ -30,6 +30,16 @@ const nextConfig = {
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// 优化缓存配置,解决缓存错误问题
|
||||
if (dev) {
|
||||
config.cache = {
|
||||
type: 'memory',
|
||||
// 设置内存缓存大小限制(512MB)
|
||||
maxGenerations: 1,
|
||||
};
|
||||
}
|
||||
|
||||
return config;
|
||||
},
|
||||
|
||||
|
||||
@ -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",
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user