forked from 77media/video-flow
216 lines
12 KiB
TypeScript
216 lines
12 KiB
TypeScript
"use client";
|
|
|
|
import React from 'react';
|
|
import { DashboardLayout } from "@/components/layout/dashboard-layout";
|
|
|
|
/**
|
|
* Share (Invite) Page - Static UI with mocked data.
|
|
* 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)
|
|
};
|
|
|
|
const PAGE_SIZE = 10;
|
|
|
|
/**
|
|
* Format epoch ms using browser preferred language.
|
|
* @param {number} epochMs - timestamp in milliseconds
|
|
* @returns {string} - localized date time string
|
|
*/
|
|
function formatLocalTime(epochMs: number): string {
|
|
try {
|
|
const locale = typeof navigator !== 'undefined' ? navigator.language : 'en-US';
|
|
const dtf = new Intl.DateTimeFormat(locale, {
|
|
dateStyle: 'medium',
|
|
timeStyle: 'medium',
|
|
});
|
|
return dtf.format(new Date(epochMs));
|
|
} catch {
|
|
return new Date(epochMs).toLocaleString();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Generate mocked invite records for demo purpose only.
|
|
* @param {number} count - number of items
|
|
* @returns {InviteRecord[]} - mocked records
|
|
*/
|
|
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' : '—',
|
|
}));
|
|
}
|
|
|
|
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 [copyState, setCopyState] = React.useState<'idle' | 'copied' | 'error'>('idle');
|
|
|
|
const [records] = React.useState<InviteRecord[]>(() => generateMockRecords(47));
|
|
const [pageIndex, setPageIndex] = React.useState<number>(0);
|
|
|
|
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]);
|
|
|
|
const handleCopy = React.useCallback(async () => {
|
|
try {
|
|
await navigator.clipboard.writeText(inviteCode);
|
|
setCopyState('copied');
|
|
window.setTimeout(() => setCopyState('idle'), 1600);
|
|
} catch {
|
|
setCopyState('error');
|
|
window.setTimeout(() => setCopyState('idle'), 1600);
|
|
}
|
|
}, [inviteCode]);
|
|
|
|
const canPrev = pageIndex > 0;
|
|
const canNext = pageIndex < totalPages - 1;
|
|
|
|
return (
|
|
<DashboardLayout>
|
|
<div data-alt="share-page" className="w-full h-full overflow-y-auto bg-black text-white">
|
|
<div
|
|
data-alt="container"
|
|
className="w-full max-w-[95%] mx-auto px-4 py-10 sm:px-6 md:px-8 lg:px-12 xl:px-16 2xl:px-20"
|
|
>
|
|
<header data-alt="page-header" className="mb-8 flex items-end justify-between">
|
|
<div data-alt="title-box">
|
|
<h1 data-alt="title" className="text-2xl font-semibold text-white">Invite Friends</h1>
|
|
<p data-alt="subtitle" className="mt-1 text-sm text-white/60">Invite friends to join and earn rewards.</p>
|
|
</div>
|
|
</header>
|
|
|
|
{/* Section 1: Invite Flow */}
|
|
<section data-alt="invite-flow" className="mb-8 rounded-lg border border-white/20 bg-black p-6 shadow-sm">
|
|
<h2 data-alt="section-title" className="text-lg font-medium text-white">Invitation Flow</h2>
|
|
<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>
|
|
</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>
|
|
</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>
|
|
</div>
|
|
<p data-alt="step-desc" className="mt-2 text-sm text-white/70">You both receive rewards after successful registration.</p>
|
|
</li>
|
|
</ol>
|
|
</section>
|
|
|
|
{/* 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-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">
|
|
<div data-alt="code" className="rounded-md border border-white/20 bg-white/10 px-4 py-2 text-lg font-semibold tracking-wider text-white">
|
|
{inviteCode}
|
|
</div>
|
|
<button
|
|
data-alt="copy-button"
|
|
className="inline-flex h-9 items-center justify-center rounded-full bg-gradient-to-r from-custom-blue to-custom-purple px-3 text-sm font-medium text-black/90 hover:opacity-90 active:translate-y-px"
|
|
onClick={handleCopy}
|
|
type="button"
|
|
aria-label="Copy invitation code"
|
|
>
|
|
{copyState === 'copied' ? 'Copied' : copyState === 'error' ? 'Failed' : 'Copy'}
|
|
</button>
|
|
</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="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-2 text-xs text-white/60">Points detail will be available soon.</span>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
{/* Section 3: Invite Records */}
|
|
<section data-alt="invite-records" className="rounded-lg border border-white/20 bg-black p-6 shadow-sm">
|
|
<div data-alt="section-header" className="mb-4 flex items-center justify-between">
|
|
<h2 data-alt="section-title" className="text-lg font-medium text-white">Invite Records</h2>
|
|
<div data-alt="pagination" className="flex items-center gap-2">
|
|
<button
|
|
data-alt="prev-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.max(0, p - 1))}
|
|
disabled={!canPrev}
|
|
aria-label="Previous page"
|
|
>
|
|
Prev
|
|
</button>
|
|
<span data-alt="page-info" className="text-sm text-white/70">
|
|
{pageIndex + 1} / {totalPages}
|
|
</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}
|
|
aria-label="Next page"
|
|
>
|
|
Next
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div data-alt="table-wrapper" className="overflow-x-auto">
|
|
<table data-alt="records-table" className="min-w-full divide-y divide-white/10">
|
|
<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>
|
|
</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>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</section>
|
|
</div>
|
|
</div>
|
|
</DashboardLayout>
|
|
);
|
|
}
|
|
|
|
|