diff --git a/prd.json b/prd.json
index 45673e1..196ae27 100644
--- a/prd.json
+++ b/prd.json
@@ -675,7 +675,7 @@
"Verify in browser using dev-browser skill"
],
"priority": 38,
- "passes": false,
+ "passes": true,
"notes": ""
},
{
@@ -692,8 +692,61 @@
"Verify in browser using dev-browser skill"
],
"priority": 39,
- "passes": false,
+ "passes": true,
"notes": ""
+ },
+ {
+ "id": "US-040",
+ "title": "Conditionals on choice options",
+ "description": "As a user, I want individual choice options to have variable conditions so that options are only visible when certain conditions are met (e.g., affection > 10).",
+ "acceptanceCriteria": [
+ "Each ChoiceOption can have optional condition (variableName, operator, value)",
+ "Update ChoiceNode UI to show 'Add condition' button per option",
+ "Condition editor modal for each option",
+ "Visual indicator (icon/badge) on options with conditions",
+ "Update TypeScript types: ChoiceOption gets optional condition field",
+ "Export includes per-option conditions in Ren'Py JSON",
+ "Typecheck passes",
+ "Verify in browser using dev-browser skill"
+ ],
+ "priority": 40,
+ "passes": true,
+ "notes": "Dependencies: US-018, US-019, US-025. Complexity: M"
+ },
+ {
+ "id": "US-041",
+ "title": "Change password for logged-in user",
+ "description": "As a user, I want to change my own password from a settings/profile page so that I can keep my account secure.",
+ "acceptanceCriteria": [
+ "Settings/profile page accessible from dashboard header",
+ "Form with: current password, new password, confirm new password fields",
+ "Calls Supabase updateUser with new password",
+ "Requires current password verification (re-authenticate)",
+ "Shows success/error messages",
+ "Typecheck passes",
+ "Verify in browser using dev-browser skill"
+ ],
+ "priority": 41,
+ "passes": true,
+ "notes": "Dependencies: US-004. Complexity: S"
+ },
+ {
+ "id": "US-042",
+ "title": "Password reset modal on token arrival",
+ "description": "As a user, I want a modal to automatically appear when a password reset token is detected so that I can set my new password seamlessly.",
+ "acceptanceCriteria": [
+ "Detect password reset token in URL (from Supabase email link)",
+ "Show modal/dialog automatically when token present",
+ "Modal has: new password, confirm password fields",
+ "Calls Supabase updateUser with token to complete reset",
+ "On success, close modal and redirect to login",
+ "On error, show error message",
+ "Typecheck passes",
+ "Verify in browser using dev-browser skill"
+ ],
+ "priority": 42,
+ "passes": true,
+ "notes": "Dependencies: US-006. Complexity: S"
}
]
}
diff --git a/progress.txt b/progress.txt
index 769e1e2..c5d0f32 100644
--- a/progress.txt
+++ b/progress.txt
@@ -24,6 +24,9 @@
- Register custom node types in nodeTypes object (memoized with useMemo) and pass to ReactFlow component
- FlowchartEditor uses ReactFlowProvider wrapper + inner component pattern for useReactFlow() hook access
- Use nanoid for generating unique node IDs (import from 'nanoid')
+- Reusable LoadingSpinner component in `src/components/LoadingSpinner.tsx` with size ('sm'|'md'|'lg') and optional message
+- Toast component supports an optional `action` prop: `{ label: string; onClick: () => void }` for retry/undo buttons
+- Settings page at `/dashboard/settings` reuses dashboard layout; re-auth via signInWithPassword before updateUser
---
@@ -552,3 +555,80 @@
- Validate JSON output with JSON.parse before download to ensure validity
- Use purple color scheme for Ren'Py-specific button to distinguish from generic export
---
+
+## 2026-01-22 - US-038
+- What was implemented: Unsaved changes warning with dirty state tracking, beforeunload, and navigation confirmation modal
+- Files changed:
+ - src/app/editor/[projectId]/FlowchartEditor.tsx - added isDirty tracking via useMemo comparing current state to lastSavedDataRef, beforeunload event handler, navigation warning modal, back button with handleBackClick, moved header from page.tsx into this component
+ - src/app/editor/[projectId]/page.tsx - simplified to only render FlowchartEditor (header moved to client component for dirty state access)
+- **Learnings for future iterations:**
+ - Dirty state tracking uses useMemo comparing JSON.stringify of current flowchart data to a lastSavedDataRef
+ - lastSavedDataRef is a useRef initialized with initialData and updated after successful save
+ - Browser beforeunload requires both event.preventDefault() and setting event.returnValue = '' for modern browsers
+ - Header with back navigation was moved from server component (page.tsx) to client component (FlowchartEditor.tsx) so it can access isDirty state
+ - Back button uses handleBackClick which checks isDirty before navigating or showing confirmation modal
+ - Navigation warning modal shows "Leave Page" (red) and "Stay" buttons for clear user action
+ - "(unsaved changes)" indicator shown next to project name when isDirty is true
+---
+
+## 2026-01-22 - US-039
+- What was implemented: Loading and error states with reusable spinner, error page, and toast retry
+- Files changed:
+ - src/components/LoadingSpinner.tsx - new reusable loading spinner component with size variants and optional message
+ - src/app/editor/[projectId]/loading.tsx - updated to use LoadingSpinner component
+ - src/app/editor/[projectId]/page.tsx - replaced notFound() with custom error UI showing "Project Not Found" with back to dashboard link
+ - src/components/Toast.tsx - added optional action prop for action buttons (e.g., retry)
+ - src/app/editor/[projectId]/FlowchartEditor.tsx - updated toast state type to include action, save error now shows retry button via handleSaveRef pattern
+- **Learnings for future iterations:**
+ - Use a ref (handleSaveRef) to break circular dependency when a useCallback needs to reference itself for retry logic
+ - Toast action prop uses `{ label: string; onClick: () => void }` for flexible action buttons
+ - Don't auto-dismiss toasts that have action buttons (users need time to click them)
+ - Replace `notFound()` with inline error UI when you need custom styling and navigation links
+ - LoadingSpinner uses size prop ('sm' | 'md' | 'lg') for flexibility across different contexts
+ - Link component from next/link is needed in server components for navigation (no useRouter in server components)
+---
+
+## 2026-01-22 - US-040
+- What was implemented: Conditionals on choice options - per-option visibility conditions
+- Files changed:
+ - src/types/flowchart.ts - moved Condition type before ChoiceOption, added optional condition field to ChoiceOption
+ - src/components/editor/nodes/ChoiceNode.tsx - added condition button per option, condition badge display, condition editing state management
+ - src/components/editor/OptionConditionEditor.tsx - new modal component for editing per-option conditions (variable name, operator, value)
+ - src/app/editor/[projectId]/FlowchartEditor.tsx - updated Ren'Py export to include per-option conditions (option condition takes priority over edge condition)
+- **Learnings for future iterations:**
+ - Per-option conditions use the same Condition type as edge conditions
+ - Condition type needed to be moved above ChoiceOption in types file since ChoiceOption now references it
+ - Use `delete obj.property` pattern instead of destructuring with unused variable to avoid lint warnings
+ - OptionConditionEditor is separate from ConditionEditor because it operates on option IDs vs edge IDs
+ - In Ren'Py export, option-level condition takes priority over edge condition since it represents visibility
+ - Condition button uses amber color scheme (bg-amber-100) when condition is set, neutral when not
+ - Condition badge below option shows "if variableName operator value" text in compact format
+---
+
+## 2026-01-22 - US-041
+- What was implemented: Change password for logged-in user from settings page
+- Files changed:
+ - src/app/dashboard/settings/page.tsx - new client component with password change form (current, new, confirm fields)
+ - src/components/Navbar.tsx - added "Settings" link to navbar
+- **Learnings for future iterations:**
+ - Settings page lives under /dashboard/settings to reuse the dashboard layout (navbar, auth check)
+ - Re-authentication uses signInWithPassword with current password before allowing updateUser
+ - Supabase getUser() returns current user email needed for re-auth signInWithPassword call
+ - Password validation: check match and minimum length (6 chars) before making API calls
+ - Clear form fields after successful password update for security
+ - Settings link in navbar uses neutral zinc colors to distinguish from admin/action links
+---
+
+## 2026-01-22 - US-042
+- What was implemented: Password reset modal that automatically appears when a recovery token is detected in the URL
+- Files changed:
+ - src/components/PasswordResetModal.tsx - new client component with modal that detects recovery tokens from URL hash, sets session, and provides password reset form
+ - src/app/login/page.tsx - integrated PasswordResetModal component on the login page
+- **Learnings for future iterations:**
+ - PasswordResetModal is a standalone component that can be placed on any page to detect recovery tokens
+ - Use window.history.replaceState to clean the URL hash after extracting the token (prevents re-triggering on refresh)
+ - Separate tokenError state from form error state to show different UI (expired link vs. form validation)
+ - Modal uses fixed positioning with z-50 to overlay above page content
+ - After successful password update, sign out the user and redirect to login with success message (same as reset-password page)
+ - The modal coexists with the existing /reset-password page - both handle recovery tokens but in different UX patterns
+---
diff --git a/src/app/dashboard/settings/page.tsx b/src/app/dashboard/settings/page.tsx
new file mode 100644
index 0000000..23898f1
--- /dev/null
+++ b/src/app/dashboard/settings/page.tsx
@@ -0,0 +1,161 @@
+'use client'
+
+import { useState } from 'react'
+import { createClient } from '@/lib/supabase/client'
+
+export default function SettingsPage() {
+ const [currentPassword, setCurrentPassword] = useState('')
+ const [newPassword, setNewPassword] = useState('')
+ const [confirmPassword, setConfirmPassword] = useState('')
+ const [error, setError] = useState('')
+ const [success, setSuccess] = useState('')
+ const [isLoading, setIsLoading] = useState(false)
+
+ const handleChangePassword = async (e: React.FormEvent) => {
+ e.preventDefault()
+ setError('')
+ setSuccess('')
+
+ if (newPassword !== confirmPassword) {
+ setError('New passwords do not match.')
+ return
+ }
+
+ if (newPassword.length < 6) {
+ setError('New password must be at least 6 characters.')
+ return
+ }
+
+ setIsLoading(true)
+
+ try {
+ const supabase = createClient()
+
+ // Re-authenticate with current password
+ const { data: { user } } = await supabase.auth.getUser()
+ if (!user?.email) {
+ setError('Unable to verify current user.')
+ setIsLoading(false)
+ return
+ }
+
+ const { error: signInError } = await supabase.auth.signInWithPassword({
+ email: user.email,
+ password: currentPassword,
+ })
+
+ if (signInError) {
+ setError('Current password is incorrect.')
+ setIsLoading(false)
+ return
+ }
+
+ // Update to new password
+ const { error: updateError } = await supabase.auth.updateUser({
+ password: newPassword,
+ })
+
+ if (updateError) {
+ setError(updateError.message)
+ setIsLoading(false)
+ return
+ }
+
+ setSuccess('Password updated successfully.')
+ setCurrentPassword('')
+ setNewPassword('')
+ setConfirmPassword('')
+ } catch {
+ setError('An unexpected error occurred.')
+ } finally {
+ setIsLoading(false)
+ }
+ }
+
+ return (
+