feat: tighten workspace auth and onboarding flow
This commit is contained in:
856
docs/superpowers/plans/2026-03-27-workspace-improvements.md
Normal file
856
docs/superpowers/plans/2026-03-27-workspace-improvements.md
Normal file
@@ -0,0 +1,856 @@
|
||||
# Workspace Improvements Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Polish the workspace by removing logo/login button clutter, adding a history toggle, enforcing mandatory login after 3 guest uploads, and adding email verification code to the registration flow.
|
||||
|
||||
**Architecture:** Five independent changes across the workspace layer. History toggle state lives in `WorkspacePage` and is passed into `LeftSidebar`. Email verification adds a two-step flow inside `AuthModal` only — no new files needed. All i18n strings go through `translations.ts`.
|
||||
|
||||
**Tech Stack:** React 18, TypeScript, Tailwind CSS (utility classes), Lucide React icons, existing `http` client in `lib/api.ts`
|
||||
|
||||
---
|
||||
|
||||
## File Map
|
||||
|
||||
| File | Change |
|
||||
|------|--------|
|
||||
| `src/components/layout/AppNavbar.tsx` | Remove logo icon + text + divider |
|
||||
| `src/components/LeftSidebar.tsx` | Remove login button; add history toggle prop |
|
||||
| `src/pages/WorkspacePage.tsx` | Add `historyEnabled` state; pass to sidebar; force mandatory modal after 3rd upload |
|
||||
| `src/components/AuthModal.tsx` | Add send-code button, countdown, verification code field to signup tab |
|
||||
| `src/lib/authService.ts` | Add `sendEmailCode()` method; update `register()` to send `code` |
|
||||
| `src/types/api.ts` | Add `SendEmailCodeRequest`; add `code` to `RegisterRequest` |
|
||||
| `src/lib/translations.ts` | Add new i18n keys for history toggle, verification code flow |
|
||||
|
||||
---
|
||||
|
||||
## Task 1: Add i18n strings for new features
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/lib/translations.ts`
|
||||
|
||||
- [ ] **Step 1: Add new keys to both `en` and `zh` auth sections and sidebar section**
|
||||
|
||||
In `src/lib/translations.ts`, find the `en.auth` block and add after `oauthFailed`:
|
||||
```ts
|
||||
sendCode: 'Send Code',
|
||||
resendCode: 'Resend',
|
||||
codeSent: 'Code sent',
|
||||
verificationCode: 'Verification Code',
|
||||
verificationCodePlaceholder: 'Enter 6-digit code',
|
||||
verificationCodeRequired: 'Please enter the verification code.',
|
||||
verificationCodeHint: 'Check your inbox for the 6-digit code.',
|
||||
sendCodeFailed: 'Failed to send verification code, please retry.',
|
||||
```
|
||||
|
||||
In `en.sidebar`, add after `historyHeader`:
|
||||
```ts
|
||||
historyToggle: 'Show History',
|
||||
historyLoginRequired: 'Login to enable history',
|
||||
```
|
||||
|
||||
In `zh.auth`, add after `oauthFailed`:
|
||||
```ts
|
||||
sendCode: '发送验证码',
|
||||
resendCode: '重新发送',
|
||||
codeSent: '验证码已发送',
|
||||
verificationCode: '验证码',
|
||||
verificationCodePlaceholder: '请输入 6 位验证码',
|
||||
verificationCodeRequired: '请输入验证码。',
|
||||
verificationCodeHint: '请查收邮箱中的 6 位验证码。',
|
||||
sendCodeFailed: '发送验证码失败,请重试。',
|
||||
```
|
||||
|
||||
In `zh.sidebar`, add after `historyHeader`:
|
||||
```ts
|
||||
historyToggle: '显示历史',
|
||||
historyLoginRequired: '登录后开启历史记录',
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Commit**
|
||||
```bash
|
||||
git add src/lib/translations.ts
|
||||
git commit -m "feat: add i18n keys for history toggle and email verification"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 2: Remove logo from AppNavbar
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/components/layout/AppNavbar.tsx:38-50`
|
||||
|
||||
- [ ] **Step 1: Remove logo image, text, and divider — keep only the Home icon link**
|
||||
|
||||
Replace the entire `{/* Left: Logo + Home link */}` div (lines 38–51) with:
|
||||
```tsx
|
||||
{/* Left: Home link */}
|
||||
<div className="flex items-center gap-2">
|
||||
<Link
|
||||
to="/"
|
||||
className="flex items-center gap-1.5 px-2 py-1 text-ink-muted hover:text-ink-secondary text-xs font-medium transition-colors rounded-md hover:bg-cream-200/60"
|
||||
>
|
||||
<Home size={13} />
|
||||
<span className="hidden sm:inline">{t.marketing.nav.home}</span>
|
||||
</Link>
|
||||
</div>
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Verify dev server renders correctly**
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
Navigate to `/app`. Confirm the navbar no longer shows the TexPixel icon or text, only the Home link on the left.
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
```bash
|
||||
git add src/components/layout/AppNavbar.tsx
|
||||
git commit -m "feat: remove logo from workspace navbar"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 3: Remove login button from LeftSidebar and add history toggle prop
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/components/LeftSidebar.tsx`
|
||||
|
||||
- [ ] **Step 1: Add `historyEnabled` and `onToggleHistory` to the props interface**
|
||||
|
||||
Replace the existing `LeftSidebarProps` interface with:
|
||||
```ts
|
||||
interface LeftSidebarProps {
|
||||
files: FileRecord[];
|
||||
selectedFileId: string | null;
|
||||
onFileSelect: (fileId: string) => void;
|
||||
onUploadClick: () => void;
|
||||
canUploadAnonymously: boolean;
|
||||
onRequireAuth: () => void;
|
||||
isCollapsed: boolean;
|
||||
onToggleCollapse: () => void;
|
||||
onUploadFiles: (files: File[]) => void;
|
||||
hasMore: boolean;
|
||||
loadingMore: boolean;
|
||||
onLoadMore: () => void;
|
||||
historyEnabled: boolean;
|
||||
onToggleHistory: () => void;
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Destructure the two new props in the function signature**
|
||||
|
||||
Replace the destructuring block (the function params) to add `historyEnabled` and `onToggleHistory`:
|
||||
```ts
|
||||
export default function LeftSidebar({
|
||||
files,
|
||||
selectedFileId,
|
||||
onFileSelect,
|
||||
onUploadClick,
|
||||
canUploadAnonymously,
|
||||
onRequireAuth,
|
||||
isCollapsed,
|
||||
onToggleCollapse,
|
||||
onUploadFiles,
|
||||
hasMore,
|
||||
loadingMore,
|
||||
onLoadMore,
|
||||
historyEnabled,
|
||||
onToggleHistory,
|
||||
}: LeftSidebarProps) {
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Add `Toggle` icon to lucide imports**
|
||||
|
||||
Change the import line at the top from:
|
||||
```ts
|
||||
import { Upload, LogIn, LogOut, FileText, Clock, ChevronLeft, ChevronRight, History, MousePointerClick, FileUp, ClipboardPaste, Loader2 } from 'lucide-react';
|
||||
```
|
||||
to:
|
||||
```ts
|
||||
import { Upload, LogOut, FileText, Clock, ChevronLeft, ChevronRight, MousePointerClick, FileUp, ClipboardPaste, Loader2 } from 'lucide-react';
|
||||
```
|
||||
(Remove `LogIn`, `History` — no longer used in expanded view)
|
||||
|
||||
- [ ] **Step 4: Remove the `showAuthModal` state and the `AuthModal` import/usage from LeftSidebar**
|
||||
|
||||
Remove these lines near the top of the function body:
|
||||
```ts
|
||||
const [showAuthModal, setShowAuthModal] = useState(false);
|
||||
```
|
||||
and the `useEffect` that sets `showAuthModal` to false on auth:
|
||||
```ts
|
||||
useEffect(() => {
|
||||
if (isAuthenticated) {
|
||||
setShowAuthModal(false);
|
||||
}
|
||||
}, [isAuthenticated]);
|
||||
```
|
||||
Also remove `import AuthModal from './AuthModal';` at the top of the file, and at the bottom remove the `{showAuthModal && <AuthModal onClose={() => setShowAuthModal(false)} />}` JSX.
|
||||
|
||||
- [ ] **Step 5: Replace the history header section with a toggle switch**
|
||||
|
||||
Find the `{/* Middle Area: History */}` block. Replace the header div (the one with `Clock` icon and `historyHeader` text) with:
|
||||
```tsx
|
||||
<div className="flex items-center justify-between text-xs font-semibold text-gray-500 uppercase tracking-wider mb-3 px-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<Clock size={14} />
|
||||
<span>{t.sidebar.historyHeader}</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={onToggleHistory}
|
||||
className={`relative w-8 h-4 rounded-full transition-colors duration-200 focus:outline-none ${
|
||||
historyEnabled ? 'bg-blue-500' : 'bg-gray-300'
|
||||
}`}
|
||||
title={t.sidebar.historyToggle}
|
||||
aria-pressed={historyEnabled}
|
||||
>
|
||||
<span
|
||||
className={`absolute top-0.5 left-0.5 w-3 h-3 bg-white rounded-full shadow transition-transform duration-200 ${
|
||||
historyEnabled ? 'translate-x-4' : 'translate-x-0'
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
```
|
||||
|
||||
- [ ] **Step 6: Gate the history list behind `historyEnabled`**
|
||||
|
||||
Replace the entire `<div ref={listRef} ...>` scrollable list with:
|
||||
```tsx
|
||||
<div
|
||||
ref={listRef}
|
||||
onScroll={handleScroll}
|
||||
className="flex-1 overflow-y-auto space-y-1 pr-2 -mr-2 custom-scrollbar"
|
||||
>
|
||||
{!historyEnabled ? (
|
||||
<div className="text-center py-12 text-gray-400 text-sm">
|
||||
<div className="mb-2 opacity-50"><FileText size={40} className="mx-auto" /></div>
|
||||
{t.sidebar.historyToggle}
|
||||
</div>
|
||||
) : !user ? (
|
||||
<div className="text-center py-12 text-gray-400 text-sm">
|
||||
<div className="mb-2 opacity-50"><FileText size={40} className="mx-auto" /></div>
|
||||
{t.sidebar.historyLoginRequired}
|
||||
</div>
|
||||
) : files.length === 0 ? (
|
||||
<div className="text-center py-12 text-gray-400 text-sm">
|
||||
<div className="mb-2 opacity-50"><FileText size={40} className="mx-auto" /></div>
|
||||
{t.sidebar.noHistory}
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{files.map((file) => (
|
||||
<button
|
||||
key={file.id}
|
||||
onClick={() => onFileSelect(file.id)}
|
||||
className={`w-full p-3 rounded-lg text-left transition-all border group relative ${selectedFileId === file.id
|
||||
? 'bg-blue-50 border-blue-200 shadow-sm'
|
||||
: 'bg-white border-transparent hover:bg-gray-50 hover:border-gray-100'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-start gap-3">
|
||||
<div className={`p-2 rounded-lg ${selectedFileId === file.id ? 'bg-blue-100 text-blue-600' : 'bg-gray-100 text-gray-500 group-hover:bg-gray-200'}`}>
|
||||
<FileText size={18} />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className={`text-sm font-medium truncate ${selectedFileId === file.id ? 'text-blue-900' : 'text-gray-700'}`}>
|
||||
{file.filename}
|
||||
</p>
|
||||
<div className="flex items-center gap-2 mt-1">
|
||||
<span className="text-xs text-gray-400">
|
||||
{new Date(file.created_at).toLocaleDateString()}
|
||||
</span>
|
||||
<span className={`w-1.5 h-1.5 rounded-full ${file.status === 'completed' ? 'bg-green-500' :
|
||||
file.status === 'processing' ? 'bg-yellow-500' : 'bg-red-500'
|
||||
}`} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
{loadingMore && (
|
||||
<div className="flex items-center justify-center py-3 text-gray-400">
|
||||
<Loader2 size={18} className="animate-spin" />
|
||||
<span className="ml-2 text-xs">{t.common.loading}</span>
|
||||
</div>
|
||||
)}
|
||||
{!hasMore && files.length > 0 && (
|
||||
<div className="text-center py-3 text-xs text-gray-400">
|
||||
{t.sidebar.noMore}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
```
|
||||
|
||||
- [ ] **Step 7: Replace the bottom user/login area — remove login button, keep only logged-in user view**
|
||||
|
||||
Replace the entire `{/* Bottom Area: User/Login */}` div with:
|
||||
```tsx
|
||||
{/* Bottom Area: User info (only shown when logged in) */}
|
||||
{user && (
|
||||
<div className="p-4 border-t border-gray-100 bg-gray-50/30">
|
||||
<div className="flex items-center gap-3 p-2 rounded-lg bg-white border border-gray-100 shadow-sm">
|
||||
<div className="w-8 h-8 bg-gray-900 rounded-full flex items-center justify-center flex-shrink-0">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12 12C14.21 12 16 10.21 16 8C16 5.79 14.21 4 12 4C9.79 4 8 5.79 8 8C8 10.21 9.79 12 12 12ZM12 14C9.33 14 4 15.34 4 18V20H20V18C20 15.34 14.67 14 12 14Z" fill="white" />
|
||||
</svg>
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm font-medium text-gray-900 truncate">{displayName}</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => signOut()}
|
||||
className="p-1.5 text-gray-400 hover:text-red-500 hover:bg-red-50 rounded-md transition-colors"
|
||||
title={t.common.logout}
|
||||
>
|
||||
<LogOut size={16} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
```
|
||||
|
||||
- [ ] **Step 8: Fix collapsed view — remove LogIn icon button**
|
||||
|
||||
In the `if (isCollapsed)` return block, remove the bottom `<button>` that shows the `LogIn` icon:
|
||||
```tsx
|
||||
<button
|
||||
onClick={() => !user && setShowAuthModal(true)}
|
||||
className="p-3 rounded-lg text-gray-600 hover:bg-gray-200 transition-colors mt-auto"
|
||||
title={user ? 'Signed In' : t.common.login}
|
||||
>
|
||||
<LogIn size={20} />
|
||||
</button>
|
||||
```
|
||||
Replace with nothing (delete that button entirely). The `isAuthenticated` import can stay for now.
|
||||
|
||||
- [ ] **Step 9: Commit**
|
||||
```bash
|
||||
git add src/components/LeftSidebar.tsx
|
||||
git commit -m "feat: remove login button from sidebar, add history toggle"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 4: Wire history toggle state and mandatory auth in WorkspacePage
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/pages/WorkspacePage.tsx`
|
||||
|
||||
- [ ] **Step 1: Add `historyEnabled` state**
|
||||
|
||||
After the existing `const [loadingMore, setLoadingMore] = useState(false);` line, add:
|
||||
```ts
|
||||
const [historyEnabled, setHistoryEnabled] = useState(false);
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Add `handleToggleHistory` callback**
|
||||
|
||||
After the `openAuthModal` callback, add:
|
||||
```ts
|
||||
const handleToggleHistory = useCallback(() => {
|
||||
if (!historyEnabled) {
|
||||
// Turning on
|
||||
if (!user) {
|
||||
openAuthModal();
|
||||
return;
|
||||
}
|
||||
setHistoryEnabled(true);
|
||||
if (!hasLoadedFiles.current) {
|
||||
hasLoadedFiles.current = true;
|
||||
loadFiles();
|
||||
}
|
||||
} else {
|
||||
setHistoryEnabled(false);
|
||||
}
|
||||
}, [historyEnabled, user, openAuthModal]);
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Remove auto-load on auth — history is now opt-in**
|
||||
|
||||
Find the `useEffect` that auto-calls `loadFiles()` when `user` becomes available:
|
||||
```ts
|
||||
useEffect(() => {
|
||||
if (!initializing && user && !hasLoadedFiles.current) {
|
||||
hasLoadedFiles.current = true;
|
||||
loadFiles();
|
||||
}
|
||||
if (!user) {
|
||||
hasLoadedFiles.current = false;
|
||||
setFiles([]);
|
||||
setSelectedFileId(null);
|
||||
setCurrentPage(1);
|
||||
setHasMore(false);
|
||||
}
|
||||
}, [initializing, user]);
|
||||
```
|
||||
|
||||
Replace with (keep the reset on logout, remove the auto-load):
|
||||
```ts
|
||||
useEffect(() => {
|
||||
if (!user) {
|
||||
hasLoadedFiles.current = false;
|
||||
setHistoryEnabled(false);
|
||||
setFiles([]);
|
||||
setSelectedFileId(null);
|
||||
setCurrentPage(1);
|
||||
setHasMore(false);
|
||||
}
|
||||
}, [user]);
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Make the auth modal mandatory after 3rd upload**
|
||||
|
||||
Add a new state after `showAuthModal`:
|
||||
```ts
|
||||
const [authModalMandatory, setAuthModalMandatory] = useState(false);
|
||||
```
|
||||
|
||||
Change `openAuthModal` to accept an optional `mandatory` parameter:
|
||||
```ts
|
||||
const openAuthModal = useCallback((mandatory = false) => {
|
||||
setAuthModalMandatory(mandatory);
|
||||
setShowAuthModal(true);
|
||||
}, []);
|
||||
```
|
||||
|
||||
In `handleUpload`, after `incrementGuestUsage()`, add a mandatory modal trigger when the new count hits the limit:
|
||||
```ts
|
||||
if (!user && successfulUploads > 0) {
|
||||
incrementGuestUsage();
|
||||
// Force login after hitting the limit
|
||||
setGuestUsageCount(prev => {
|
||||
const next = prev + 1;
|
||||
if (next >= GUEST_USAGE_LIMIT) {
|
||||
openAuthModal(true);
|
||||
}
|
||||
return next;
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
Wait — `incrementGuestUsage` already increments. We need to check the new count after increment. Replace the `if (!user && successfulUploads > 0)` block at the end of `handleUpload` with:
|
||||
```ts
|
||||
if (!user && successfulUploads > 0) {
|
||||
const nextCount = guestUsageCount + successfulUploads;
|
||||
const newCount = Math.min(nextCount, GUEST_USAGE_LIMIT + 10);
|
||||
localStorage.setItem(GUEST_USAGE_COUNT_KEY, String(newCount));
|
||||
setGuestUsageCount(newCount);
|
||||
if (newCount >= GUEST_USAGE_LIMIT) {
|
||||
openAuthModal(true);
|
||||
}
|
||||
}
|
||||
```
|
||||
And remove the separate `incrementGuestUsage` call entirely (delete the `incrementGuestUsage` callback too).
|
||||
|
||||
- [ ] **Step 5: Update the two places that check upload limit to pass `mandatory=false` (keep non-mandatory for pre-upload checks)**
|
||||
|
||||
The `onUploadClick` handler and paste handler already call `openAuthModal()` with no arg (defaults to `false`) — that stays as-is.
|
||||
|
||||
- [ ] **Step 6: Update AuthModal JSX to pass `mandatory` prop and update close handler**
|
||||
|
||||
Find `{showAuthModal && (<AuthModal onClose={() => setShowAuthModal(false)} />)}` and replace with:
|
||||
```tsx
|
||||
{showAuthModal && (
|
||||
<AuthModal
|
||||
onClose={() => { setShowAuthModal(false); setAuthModalMandatory(false); }}
|
||||
mandatory={authModalMandatory}
|
||||
/>
|
||||
)}
|
||||
```
|
||||
|
||||
- [ ] **Step 7: Pass `historyEnabled` and `onToggleHistory` to LeftSidebar**
|
||||
|
||||
Find the `<LeftSidebar` JSX and add the two new props:
|
||||
```tsx
|
||||
historyEnabled={historyEnabled}
|
||||
onToggleHistory={handleToggleHistory}
|
||||
```
|
||||
|
||||
- [ ] **Step 8: After login via mandatory modal, auto-enable history**
|
||||
|
||||
In the `useEffect` that watches `user`, add history auto-enable when user logs in while `historyEnabled` is still false — actually simpler to auto-enable history when user is set and they just came from a mandatory flow. Instead, just handle this in the `useEffect` that watches user change after modal closes:
|
||||
|
||||
Replace the logout-only effect from Step 3 with:
|
||||
```ts
|
||||
useEffect(() => {
|
||||
if (user && !hasLoadedFiles.current && historyEnabled) {
|
||||
// user logged in while history was already toggled on
|
||||
hasLoadedFiles.current = true;
|
||||
loadFiles();
|
||||
}
|
||||
if (!user) {
|
||||
hasLoadedFiles.current = false;
|
||||
setHistoryEnabled(false);
|
||||
setFiles([]);
|
||||
setSelectedFileId(null);
|
||||
setCurrentPage(1);
|
||||
setHasMore(false);
|
||||
}
|
||||
}, [user]);
|
||||
```
|
||||
|
||||
- [ ] **Step 9: Commit**
|
||||
```bash
|
||||
git add src/pages/WorkspacePage.tsx
|
||||
git commit -m "feat: history toggle state, mandatory auth after 3 guest uploads"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 5: Add email verification to API types and authService
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/types/api.ts`
|
||||
- Modify: `src/lib/authService.ts`
|
||||
|
||||
- [ ] **Step 1: Update `RegisterRequest` and add `SendEmailCodeRequest` in `src/types/api.ts`**
|
||||
|
||||
Find the existing `RegisterRequest` interface:
|
||||
```ts
|
||||
export interface RegisterRequest {
|
||||
email: string;
|
||||
password: string;
|
||||
}
|
||||
```
|
||||
Replace with:
|
||||
```ts
|
||||
export interface RegisterRequest {
|
||||
email: string;
|
||||
password: string;
|
||||
code: string;
|
||||
}
|
||||
|
||||
export interface SendEmailCodeRequest {
|
||||
email: string;
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Add `sendEmailCode()` to `authService` in `src/lib/authService.ts`**
|
||||
|
||||
Add the import of `SendEmailCodeRequest` — it will be used below. Then add a new method inside `export const authService = { ... }` after the `login` method:
|
||||
```ts
|
||||
async sendEmailCode(email: string): Promise<void> {
|
||||
await http.post<null>('/user/email/code', { email } satisfies SendEmailCodeRequest, { skipAuth: true });
|
||||
},
|
||||
```
|
||||
|
||||
Also update the `register` method signature to accept `code`:
|
||||
```ts
|
||||
async register(credentials: RegisterRequest): Promise<{ user: UserInfo; token: string; expiresAt: number }> {
|
||||
const response = await http.post<AuthData>('/user/register', credentials, { skipAuth: true });
|
||||
|
||||
if (!response.data) {
|
||||
throw new ApiError(-1, '注册失败,请重试');
|
||||
}
|
||||
|
||||
return buildSession(response.data, credentials.email);
|
||||
},
|
||||
```
|
||||
(Signature stays the same — just ensuring `credentials` now includes `code` via the updated type.)
|
||||
|
||||
- [ ] **Step 3: Update the import in `authService.ts` to include `SendEmailCodeRequest`**
|
||||
|
||||
Find:
|
||||
```ts
|
||||
import type {
|
||||
AuthData,
|
||||
GoogleAuthUrlData,
|
||||
GoogleOAuthCallbackRequest,
|
||||
LoginRequest,
|
||||
RegisterRequest,
|
||||
UserInfoData,
|
||||
UserInfo,
|
||||
} from '../types/api';
|
||||
```
|
||||
Replace with:
|
||||
```ts
|
||||
import type {
|
||||
AuthData,
|
||||
GoogleAuthUrlData,
|
||||
GoogleOAuthCallbackRequest,
|
||||
LoginRequest,
|
||||
RegisterRequest,
|
||||
SendEmailCodeRequest,
|
||||
UserInfoData,
|
||||
UserInfo,
|
||||
} from '../types/api';
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Commit**
|
||||
```bash
|
||||
git add src/types/api.ts src/lib/authService.ts
|
||||
git commit -m "feat: add sendEmailCode API and code field to RegisterRequest"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 6: Update AuthContext to pass `code` through signUp
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/contexts/AuthContext.tsx`
|
||||
|
||||
- [ ] **Step 1: Update `signUp` to accept and forward `code`**
|
||||
|
||||
Find the `signUp` function/action in `AuthContext.tsx`. It currently calls `authService.register({ email, password })`. Update the `signUp` signature to accept a third `code` parameter and pass it:
|
||||
```ts
|
||||
signUp: async (email: string, password: string, code: string) => { ... }
|
||||
```
|
||||
Inside, change:
|
||||
```ts
|
||||
await authService.register({ email, password, code });
|
||||
```
|
||||
|
||||
Also update the `AuthContextValue` interface (or wherever `signUp` type is declared) to:
|
||||
```ts
|
||||
signUp: (email: string, password: string, code: string) => Promise<{ error: string | null }>;
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Commit**
|
||||
```bash
|
||||
git add src/contexts/AuthContext.tsx
|
||||
git commit -m "feat: thread verification code through AuthContext signUp"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 7: Add email verification UI to AuthModal
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/components/AuthModal.tsx`
|
||||
|
||||
- [ ] **Step 1: Add verification code state variables**
|
||||
|
||||
After the existing `useState` declarations in `AuthModal`, add:
|
||||
```ts
|
||||
const [verificationCode, setVerificationCode] = useState('');
|
||||
const [codeSent, setCodeSent] = useState(false);
|
||||
const [codeCountdown, setCodeCountdown] = useState(0);
|
||||
const [sendingCode, setSendingCode] = useState(false);
|
||||
const countdownRef = useRef<NodeJS.Timeout | null>(null);
|
||||
```
|
||||
|
||||
Add `useRef` to the React import if not already there.
|
||||
|
||||
Also import `authService` at the top:
|
||||
```ts
|
||||
import { authService } from '../lib/authService';
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Add `sendCode` handler**
|
||||
|
||||
After the `handleGoogleOAuth` function, add:
|
||||
```ts
|
||||
const handleSendCode = async () => {
|
||||
const normalizedEmail = email.trim();
|
||||
if (!normalizedEmail || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(normalizedEmail)) {
|
||||
setFieldErrors(prev => ({ ...prev, email: t.auth.emailInvalid }));
|
||||
return;
|
||||
}
|
||||
setSendingCode(true);
|
||||
try {
|
||||
await authService.sendEmailCode(normalizedEmail);
|
||||
setCodeSent(true);
|
||||
setCodeCountdown(60);
|
||||
countdownRef.current = setInterval(() => {
|
||||
setCodeCountdown(prev => {
|
||||
if (prev <= 1) {
|
||||
clearInterval(countdownRef.current!);
|
||||
return 0;
|
||||
}
|
||||
return prev - 1;
|
||||
});
|
||||
}, 1000);
|
||||
} catch {
|
||||
setLocalError(t.auth.sendCodeFailed);
|
||||
} finally {
|
||||
setSendingCode(false);
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
Add a cleanup `useEffect` for the countdown interval:
|
||||
```ts
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (countdownRef.current) clearInterval(countdownRef.current);
|
||||
};
|
||||
}, []);
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Update `handleSubmit` to validate code and pass it to `signUp`**
|
||||
|
||||
In the `handleSubmit` function, inside the `if (mode === 'signup')` validation block, add a check for the verification code:
|
||||
```ts
|
||||
if (!verificationCode.trim()) {
|
||||
nextFieldErrors.verificationCode = t.auth.verificationCodeRequired;
|
||||
}
|
||||
```
|
||||
|
||||
Update `fieldErrors` type to include `verificationCode`:
|
||||
```ts
|
||||
const [fieldErrors, setFieldErrors] = useState<{
|
||||
email?: string;
|
||||
password?: string;
|
||||
confirmPassword?: string;
|
||||
verificationCode?: string;
|
||||
}>({});
|
||||
```
|
||||
|
||||
Update the `signUp` call to pass the code:
|
||||
```ts
|
||||
const result = mode === 'signup'
|
||||
? await signUp(normalizedEmail, password, verificationCode.trim())
|
||||
: await signIn(normalizedEmail, password);
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Add Send Code button inline with the email field (signup mode only)**
|
||||
|
||||
In the JSX, find the email `<div style={s.fieldGroup}>` block. Replace it with a version that adds the send-code button when in signup mode:
|
||||
```tsx
|
||||
<div style={s.fieldGroup}>
|
||||
<label htmlFor="auth-email" style={s.label}>{t.auth.email}</label>
|
||||
<div style={{ display: 'flex', gap: '8px', alignItems: 'flex-start' }}>
|
||||
<div style={{ flex: 1 }}>
|
||||
<input
|
||||
id="auth-email"
|
||||
type="email"
|
||||
value={email}
|
||||
onChange={(e) => {
|
||||
setEmail(e.target.value);
|
||||
if (fieldErrors.email) setFieldErrors((prev) => ({ ...prev, email: undefined }));
|
||||
}}
|
||||
style={fieldErrors.email ? { ...s.input, ...s.inputError } : s.input}
|
||||
placeholder="your@email.com"
|
||||
required
|
||||
disabled={isBusy}
|
||||
/>
|
||||
{fieldErrors.email && <p style={s.fieldError}>{fieldErrors.email}</p>}
|
||||
{mode === 'signup' && <p style={s.fieldHint}>{t.auth.emailHint}</p>}
|
||||
</div>
|
||||
{mode === 'signup' && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleSendCode}
|
||||
disabled={isBusy || sendingCode || codeCountdown > 0}
|
||||
style={{
|
||||
flexShrink: 0,
|
||||
height: '42px',
|
||||
padding: '0 12px',
|
||||
background: codeCountdown > 0 ? '#F5DDD0' : '#C8622A',
|
||||
color: codeCountdown > 0 ? '#AA9685' : 'white',
|
||||
border: 'none',
|
||||
borderRadius: '12px',
|
||||
fontSize: '13px',
|
||||
fontWeight: 600,
|
||||
fontFamily: "'DM Sans', sans-serif",
|
||||
cursor: codeCountdown > 0 || sendingCode ? 'not-allowed' : 'pointer',
|
||||
whiteSpace: 'nowrap',
|
||||
transition: 'all 0.2s',
|
||||
}}
|
||||
>
|
||||
{codeCountdown > 0
|
||||
? `${codeCountdown}s`
|
||||
: codeSent
|
||||
? t.auth.resendCode
|
||||
: t.auth.sendCode}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Add the verification code input field (signup mode, after confirm password)**
|
||||
|
||||
After the confirm-password `{mode === 'signup' && (...)}` block, add:
|
||||
```tsx
|
||||
{mode === 'signup' && (
|
||||
<div style={s.fieldGroup}>
|
||||
<label htmlFor="auth-code" style={s.label}>{t.auth.verificationCode}</label>
|
||||
<input
|
||||
id="auth-code"
|
||||
type="text"
|
||||
inputMode="numeric"
|
||||
maxLength={6}
|
||||
value={verificationCode}
|
||||
onChange={(e) => {
|
||||
setVerificationCode(e.target.value.replace(/\D/g, ''));
|
||||
if (fieldErrors.verificationCode) setFieldErrors(prev => ({ ...prev, verificationCode: undefined }));
|
||||
}}
|
||||
style={fieldErrors.verificationCode ? { ...s.input, ...s.inputError } : s.input}
|
||||
placeholder={t.auth.verificationCodePlaceholder}
|
||||
disabled={isBusy}
|
||||
/>
|
||||
{fieldErrors.verificationCode
|
||||
? <p style={s.fieldError}>{fieldErrors.verificationCode}</p>
|
||||
: <p style={s.fieldHint}>{t.auth.verificationCodeHint}</p>
|
||||
}
|
||||
</div>
|
||||
)}
|
||||
```
|
||||
|
||||
- [ ] **Step 6: Reset code state when switching to signin tab**
|
||||
|
||||
In the `setMode('signin')` onClick handler, add resets:
|
||||
```ts
|
||||
onClick={() => {
|
||||
setMode('signin');
|
||||
setFieldErrors({});
|
||||
setLocalError('');
|
||||
setVerificationCode('');
|
||||
setCodeSent(false);
|
||||
setCodeCountdown(0);
|
||||
if (countdownRef.current) clearInterval(countdownRef.current);
|
||||
}}
|
||||
```
|
||||
|
||||
- [ ] **Step 7: Verify in browser — register flow should now require sending code first**
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
Open `/app`, click register. Confirm:
|
||||
1. Email field has "Send Code" button
|
||||
2. Clicking it (with valid email) triggers send and starts countdown
|
||||
3. Verification code field appears below confirm-password
|
||||
4. Submitting without code shows validation error
|
||||
5. Login tab does not show the code field
|
||||
|
||||
- [ ] **Step 8: Commit**
|
||||
```bash
|
||||
git add src/components/AuthModal.tsx
|
||||
git commit -m "feat: add email verification code step to registration flow"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 8: Final smoke test
|
||||
|
||||
- [ ] **Step 1: Run type check**
|
||||
```bash
|
||||
npm run typecheck
|
||||
```
|
||||
Expected: no errors
|
||||
|
||||
- [ ] **Step 2: Run unit tests**
|
||||
```bash
|
||||
npm run test
|
||||
```
|
||||
Expected: all pass (existing tests should still pass)
|
||||
|
||||
- [ ] **Step 3: Manual end-to-end check**
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
Verify:
|
||||
1. Workspace navbar has no logo — only Home link on left
|
||||
2. Sidebar bottom shows no login button when logged out
|
||||
3. History toggle is off by default, shows placeholder text
|
||||
4. Toggling history on while logged out opens auth modal
|
||||
5. After 3 guest uploads, mandatory auth modal appears (no close button)
|
||||
6. Register tab: Send Code button appears, countdown works, code field required
|
||||
7. Sign-in tab: no code field visible
|
||||
8. Logged-in user: history toggle on loads history; logout resets toggle to off
|
||||
Reference in New Issue
Block a user