diff --git a/.gitignore b/.gitignore index 40a7752..53b71fc 100644 --- a/.gitignore +++ b/.gitignore @@ -30,4 +30,5 @@ docs .aider* CLAUDE.md -better-auth.md \ No newline at end of file +better-auth.md +DescriptionPlan.md \ No newline at end of file diff --git a/src/App.tsx b/src/App.tsx index 476f13e..11a64ab 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,24 +1,23 @@ 'use client'; import React, { useEffect } from 'react'; -import { TooltipProvider } from './components/ui/better-tooltip'; +import { TooltipProvider } from './components/ui/BetterTooltip'; // Providers -import { AuthProvider } from './features/auth/AuthProvider'; +import { AuthProvider } from './features/auth/context/AuthProvider'; import { EntriesProvider } from './features/statements/context/EntriesProvider'; -import { QuestionsProvider } from './providers/QuestionsProvider'; -import { HelpProvider } from './components/ui/tour'; +import { QuestionsProvider } from './features/questions/context/QuestionsProvider'; +import { HelpProvider } from './features/help'; // Components import LoginPage from './features/auth/components/LoginPage'; -import Header from './layouts/components/Header'; -import MainPage from './layouts/components/MainPage'; +import { Header, MainPage } from './components/layout'; import MockNotification from './features/auth/components/MockNotification'; // Hooks and Utilities import { useEntries } from './features/statements/hooks/useEntries'; import { useAuth } from './features/auth/api/hooks'; -import { handleMagicLinkVerification } from './features/auth/authUtils'; +import { handleMagicLinkVerification } from './features/auth/utils/authUtils'; // Outer Component: Responsible only for setting up the environment (the providers) for the rest of the app. const AppContent: React.FC = () => { @@ -33,43 +32,59 @@ const AppContent: React.FC = () => { verifyToken(); }, []); - + // Force synchronization between auth state and entries state when component mounts useEffect(() => { if (authState.user && authState.isAuthenticated) { - console.log('AppContent: Found authenticated user, dispatching event:', authState.user); + console.log( + 'AppContent: Found authenticated user, dispatching event:', + authState.user + ); // Dispatch event to ensure EntriesProvider gets the user data - window.dispatchEvent(new CustomEvent('authStateChanged', { - detail: { user: authState.user } - })); + window.dispatchEvent( + new CustomEvent('authStateChanged', { + detail: { user: authState.user }, + }) + ); } }, [authState.user, authState.isAuthenticated]); - + // Listen for magic link verification and ensure user email is saved to entries context useEffect(() => { - const handleMagicLinkVerified = (event: any) => { + interface MagicLinkVerifiedEvent extends CustomEvent { + detail: { + user: { + email: string; + [key: string]: unknown; + }; + }; + } + + const handleMagicLinkVerified = (event: MagicLinkVerifiedEvent) => { if (event.detail?.user?.email) { - console.log('App: Magic link verified with email:', event.detail.user.email); + console.log( + 'App: Magic link verified with email:', + event.detail.user.email + ); // Dispatch event with user email to entries context - window.dispatchEvent(new CustomEvent('authStateChanged', { - detail: { user: { email: event.detail.user.email }} - })); + window.dispatchEvent( + new CustomEvent('authStateChanged', { + detail: { user: { email: event.detail.user.email } }, + }) + ); } }; - - window.addEventListener('magicLinkVerified', handleMagicLinkVerified); - return () => window.removeEventListener('magicLinkVerified', handleMagicLinkVerified); + + window.addEventListener('magicLinkVerified', handleMagicLinkVerified as EventListener); + return () => + window.removeEventListener('magicLinkVerified', handleMagicLinkVerified as EventListener); }, []); return ( // MainPage and Header receives the username from context. <> - {data.username ? ( - - ) : ( - - )} + {data.username ? : } > ); }; diff --git a/src/components/debug/TestButton.tsx b/src/components/debug/TestButton.tsx index d3dde6a..8c83210 100644 --- a/src/components/debug/TestButton.tsx +++ b/src/components/debug/TestButton.tsx @@ -1,9 +1,11 @@ // TestStatementButton.tsx import React from 'react'; -import { Button } from '../ui/button'; +import { Button } from '../ui/Button'; import type { Entry } from '@/types/entries'; import { useEntries } from '../../features/statements/hooks/useEntries'; // CHANGE: Import useEntries to update context +// Force Redeploy + const TestStatementButton: React.FC = () => { // Create a valid test statement with one sample action. const createTestStatement = (): Entry => ({ @@ -15,6 +17,7 @@ const TestStatementButton: React.FC = () => { verb: 'supports', object: 'the project', }, + description: 'El perro de San Roque no tiene rabo, porque ramon ramirez se lo ha cortado', category: 'wellbeing', actions: [ { diff --git a/src/layouts/components/Footer.tsx b/src/components/layout/Footer.tsx similarity index 100% rename from src/layouts/components/Footer.tsx rename to src/components/layout/Footer.tsx diff --git a/src/layouts/components/Header.tsx b/src/components/layout/Header.tsx similarity index 85% rename from src/layouts/components/Header.tsx rename to src/components/layout/Header.tsx index 5735ec0..9f4469c 100644 --- a/src/layouts/components/Header.tsx +++ b/src/components/layout/Header.tsx @@ -1,9 +1,9 @@ -// src/components/Header.tsx +// src/components/layout/Header.tsx import React, { useState } from 'react'; -import { useEntries } from '../../features/statements/hooks/useEntries'; -import SmallCircularQuestionCounter from '../../components/ui/questionCounter/smallCircularQuestionCounter'; -import UserDataModal from '../../components/modals/UserDataModal'; -// import { Tooltip, TooltipTrigger, TooltipContent } from '../../components/ui/better-tooltip'; +import { useEntries } from '@/features/statements'; +import { SmallCircularQuestionCounter } from '@/components/ui'; +import { UserDataModal } from '@/components/modals'; +// import { Tooltip, TooltipTrigger, TooltipContent } from '@/components/ui'; const Header: React.FC = () => { const { data } = useEntries(); diff --git a/src/layouts/components/MainPage.tsx b/src/components/layout/MainPage.tsx similarity index 85% rename from src/layouts/components/MainPage.tsx rename to src/components/layout/MainPage.tsx index 3a26a44..c9dfaaa 100644 --- a/src/layouts/components/MainPage.tsx +++ b/src/components/layout/MainPage.tsx @@ -5,19 +5,19 @@ import { Tooltip, TooltipTrigger, TooltipContent, -} from '../../components/ui/better-tooltip'; -import StatementList from '../../features/statements/components/StatementList'; -import { useEntries } from '../../features/statements/hooks/useEntries'; -import { Button } from '../../components/ui/button'; + Button, +} from '@/components/ui'; +import { StatementList } from '@/features/statements/components'; +import { useEntries } from '@/features/statements'; import { Mail } from 'lucide-react'; -import StatementWizard from '../../features/wizard/components/StatementWizard'; -import ShareEmailModal from '../../components/modals/ShareEmailModal'; -// import TestStatementButton from '../../components/debug/TestButton'; +import StatementWizard from '@/features/wizard/components/StatementWizard'; +import { ShareEmailModal } from '@/components/modals'; +// import TestStatementButton from '@/components/debug/TestButton'; import Footer from './Footer'; const MainPage: React.FC = () => { const { data } = useEntries(); - const { username, managerName, managerEmail, entries } = data; + const { username, managerEmail, entries } = data; const [isWizardOpen, setIsWizardOpen] = useState(false); const [isShareModalOpen, setIsShareModalOpen] = useState(false); @@ -42,9 +42,10 @@ const MainPage: React.FC = () => { {/* Fixed header layout with 1 or 2 rows */}
+ {statement.input} +
{action.action}
+ {action.action} +
Due by: {action.byDate} @@ -191,8 +217,8 @@ const GratitudeModal: React.FC = ({ > )} - diff --git a/src/components/modals/PrivacyModal.tsx b/src/components/modals/PrivacyModal.tsx index aa347a4..9d4d199 100644 --- a/src/components/modals/PrivacyModal.tsx +++ b/src/components/modals/PrivacyModal.tsx @@ -2,9 +2,10 @@ import React from 'react'; // import { SimpleDialog as Dialog, SimpleDialogContent as DialogContent } from '../ui/simple-dialog'; -import { Button } from '../ui/button'; +import { Button } from '../ui/Button'; import { X, Shield, CheckCircle, Lock, Eye } from 'lucide-react'; +// Force Redeploy interface PrivacyModalProps { onClose: () => void; } diff --git a/src/components/modals/ShareEmailModal.tsx b/src/components/modals/ShareEmailModal.tsx index f7780f2..1b54087 100644 --- a/src/components/modals/ShareEmailModal.tsx +++ b/src/components/modals/ShareEmailModal.tsx @@ -5,12 +5,12 @@ import { SimpleDialog as Dialog, SimpleDialogContent as DialogContent, SimpleDialogDescription as DialogDescription, -} from '../ui/simple-dialog'; -import { Button } from '../ui/button'; -import { useEntries } from '../../features/statements/hooks/useEntries'; -import { shareStatements } from '../../features/email/api/emailApi'; +} from '@/components/ui/Dialog'; +import { Button } from '@/components/ui/Button'; +import { useEntries } from '@/features/statements'; +import { shareStatements } from '@/features/email/api/emailStatementsApi'; import { Loader2 } from 'lucide-react'; -import { getVerbName } from '../../lib/utils/verbUtils'; +import { getEmailFormattedStatement } from '@/lib/utils/verbUtils'; import PrivacyModal from './PrivacyModal'; const ShareEmailModal: React.FC<{ onClose: () => void }> = ({ onClose }) => { @@ -124,10 +124,16 @@ const ShareEmailModal: React.FC<{ onClose: () => void }> = ({ onClose }) => { className='p-3 sm:p-4 border bg-white shadow-sm rounded-sm' > - {entry.atoms.subject} {getVerbName(entry.atoms.verb)}{' '} - {entry.atoms.object} + {getEmailFormattedStatement(entry, data.username || 'User')} + {/* Display description if available */} + {entry.description && entry.description.trim() !== '' && ( + + {entry.description} + + )} + {entry.actions && entry.actions.length > 0 && ( diff --git a/src/components/modals/TermsModal.tsx b/src/components/modals/TermsModal.tsx index d69a445..ad7e6ed 100644 --- a/src/components/modals/TermsModal.tsx +++ b/src/components/modals/TermsModal.tsx @@ -2,9 +2,10 @@ import React from 'react'; // import { SimpleDialog as Dialog, SimpleDialogContent as DialogContent } from '../ui/simple-dialog'; -import { Button } from '../ui/button'; +import { Button } from '../ui/Button'; import { X, FileText, Check, AlertTriangle, Scale } from 'lucide-react'; +// Force Redeploy interface TermsModalProps { onClose: () => void; } diff --git a/src/components/modals/UserDataModal.tsx b/src/components/modals/UserDataModal.tsx index 7a0e9c6..9548d00 100644 --- a/src/components/modals/UserDataModal.tsx +++ b/src/components/modals/UserDataModal.tsx @@ -1,13 +1,13 @@ 'use client'; import React, { useState, useEffect, useRef } from 'react'; -import { useEntries } from '../../features/statements/hooks/useEntries'; -import { useAuth } from '../../features/auth/api/hooks'; -import { Button } from '../ui/button'; +import { useEntries } from '@/features/statements'; +import { useAuth } from '@/features/auth/api/hooks'; +import { Button } from '@/components/ui/Button'; import { Save, X, User, Mail, Award, Edit2, LogOut } from 'lucide-react'; -import { validateEmail } from '../../lib/utils/validateEmail'; -import QuestionCounter from '../ui/questionCounter/QuestionCounter'; -import ProgressWithFeedback from '../ui/progress/ProgressWithFeedback'; +import { validateEmail } from '@/lib/utils/validateEmail'; +import { QuestionCounter } from '@/components/ui'; +import ProgressWithFeedback from '@/features/progress/components/ProgressWithFeedback'; interface UserDataModalProps { onOpenChange: (open: boolean) => void; diff --git a/src/components/modals/index.ts b/src/components/modals/index.ts new file mode 100644 index 0000000..e6e6b48 --- /dev/null +++ b/src/components/modals/index.ts @@ -0,0 +1,12 @@ +/** + * Barrel file for modal components + * + * Provides unified access to modal dialog components + * for various purposes throughout the application. + */ + +export { default as GratitudeModal } from './GratitudeModal'; +export { default as PrivacyModal } from './PrivacyModal'; +export { default as ShareEmailModal } from './ShareEmailModal'; +export { default as TermsModal } from './TermsModal'; +export { default as UserDataModal } from './UserDataModal'; \ No newline at end of file diff --git a/src/components/ui/better-tooltip.tsx b/src/components/ui/BetterTooltip.tsx similarity index 95% rename from src/components/ui/better-tooltip.tsx rename to src/components/ui/BetterTooltip.tsx index 75eba2d..25c5039 100644 --- a/src/components/ui/better-tooltip.tsx +++ b/src/components/ui/BetterTooltip.tsx @@ -142,11 +142,10 @@ export const TooltipContent: React.FC<{ children: React.ReactNode; className?: string; sideOffset?: number; -}> = ( - // We're intentionally not using the props here but TooltipContent element acts as a data container - { children: _children } -) => { - return null; // This doesn't render directly, content is passed to portal via context +}> = () => { + // This component doesn't render its children directly + // Content is passed to portal via context in the parent Tooltip component + return null; }; // Portal component that actually renders the tooltip @@ -212,7 +211,7 @@ const TooltipPortal: React.FC = () => { window.removeEventListener('resize', updatePosition); window.removeEventListener('scroll', updatePosition); }; - }, [open, triggerRef.current, tooltipRef.current, isMobile]); + }, [open, triggerRef, tooltipRef, isMobile]); if (!open) return null; diff --git a/src/components/ui/button.tsx b/src/components/ui/Button.tsx similarity index 88% rename from src/components/ui/button.tsx rename to src/components/ui/Button.tsx index dd5d961..f0ab423 100644 --- a/src/components/ui/button.tsx +++ b/src/components/ui/Button.tsx @@ -1,9 +1,9 @@ import * as React from 'react'; -import { Slot } from './simple-slot'; +import { Slot } from './Slot'; import type { VariantProps } from 'class-variance-authority'; import { cn } from '@/lib/utils'; -import { buttonVariants } from './buttonVariants'; +import { buttonVariants } from './ButtonVariants'; export interface ButtonProps extends React.ButtonHTMLAttributes, @@ -23,6 +23,7 @@ const Button = React.forwardRef( ); } ); + Button.displayName = 'Button'; export { Button }; diff --git a/src/components/ui/buttonVariants.ts b/src/components/ui/ButtonVariants.ts similarity index 100% rename from src/components/ui/buttonVariants.ts rename to src/components/ui/ButtonVariants.ts diff --git a/src/components/ui/confirmation-dialog.tsx b/src/components/ui/ConfirmationDialog.tsx similarity index 98% rename from src/components/ui/confirmation-dialog.tsx rename to src/components/ui/ConfirmationDialog.tsx index 3f82aab..ad5671f 100644 --- a/src/components/ui/confirmation-dialog.tsx +++ b/src/components/ui/ConfirmationDialog.tsx @@ -1,4 +1,4 @@ -import { Button } from './button'; +import { Button } from './Button'; interface ConfirmationDialogProps { isOpen: boolean; onClose: () => void; diff --git a/src/components/ui/simple-dialog.tsx b/src/components/ui/Dialog.tsx similarity index 51% rename from src/components/ui/simple-dialog.tsx rename to src/components/ui/Dialog.tsx index e13021d..8d64862 100644 --- a/src/components/ui/simple-dialog.tsx +++ b/src/components/ui/Dialog.tsx @@ -1,6 +1,6 @@ import React, { useEffect } from 'react'; import { cn } from '@/lib/utils'; -import { SimpleDialogContext } from './simple-dialog-context'; +import { SimpleDialogContext } from './DialogContext'; interface SimpleDialogProps { isOpen?: boolean; @@ -34,94 +34,107 @@ interface SimpleDialogDescriptionProps { className?: string; } -const SimpleDialogPortal: React.FC<{ children: React.ReactNode }> = ({ children }) => { +const SimpleDialogPortal: React.FC<{ children: React.ReactNode }> = ({ + children, +}) => { return <>{children}>; }; -const SimpleDialogOverlay: React.FC<{ className?: string; onClick?: () => void }> = ({ - className, - onClick -}) => { +const SimpleDialogOverlay: React.FC<{ + className?: string; + onClick?: () => void; +}> = ({ className, onClick }) => { return ( - ); }; -const SimpleDialogContent = React.forwardRef( - ({ className, children, headerTitle, onOpenAutoFocus, ...props }, ref) => { - // Always define the useEffect, but make its behavior conditional inside - useEffect(() => { - if (onOpenAutoFocus) { - const handler = (e: Event) => { - onOpenAutoFocus(e); - }; - document.addEventListener('focus', handler, { once: true }); - return () => document.removeEventListener('focus', handler); - } - // Return empty cleanup function when onOpenAutoFocus is not provided - return () => {}; - }, [onOpenAutoFocus]); - - // Stop click propagation to prevent closing the dialog when clicking content - const handleContentClick = (e: React.MouseEvent) => { - e.stopPropagation(); - }; +const SimpleDialogContent = React.forwardRef< + HTMLDivElement, + SimpleDialogContentProps +>(({ className, children, headerTitle, onOpenAutoFocus, ...props }, ref) => { + // Always define the useEffect, but make its behavior conditional inside + useEffect(() => { + if (onOpenAutoFocus) { + const handler = (e: Event) => { + onOpenAutoFocus(e); + }; + document.addEventListener('focus', handler, { once: true }); + return () => document.removeEventListener('focus', handler); + } + // Return empty cleanup function when onOpenAutoFocus is not provided + return () => {}; + }, [onOpenAutoFocus]); + + // Stop click propagation to prevent closing the dialog when clicking content + const handleContentClick = (e: React.MouseEvent) => { + e.stopPropagation(); + }; - return ( - - {/* If there's a header title, add it as a styled header */} - {headerTitle && ( - - {headerTitle} - - )} - {/* Main content with scroll capability */} - - {children} + return ( + + {/* If there's a header title, add it as a styled header */} + {headerTitle && ( + + + {headerTitle} + - - ); - } -); + )} + {/* Main content with scroll capability */} + {children} + + ); +}); SimpleDialogContent.displayName = 'SimpleDialogContent'; -const SimpleDialogClose: React.FC<{ children: React.ReactNode; className?: string; asChild?: boolean }> = ({ - children, - className, -}) => { - return ( - - {children} - - ); +const SimpleDialogClose: React.FC<{ + children: React.ReactNode; + className?: string; + asChild?: boolean; +}> = ({ children, className }) => { + return {children}; }; -const SimpleDialogTitle: React.FC = ({ children, className }) => { +const SimpleDialogTitle: React.FC = ({ + children, + className, +}) => { return ( - + {children} ); }; -const SimpleDialogDescription: React.FC = ({ children, className }) => { +const SimpleDialogDescription: React.FC = ({ + children, + className, +}) => { return ( - + {children} ); @@ -129,53 +142,61 @@ const SimpleDialogDescription: React.FC = ({ child // Context is now imported from separate file -const SimpleDialogTrigger: React.FC = ({ children }) => { +const SimpleDialogTrigger: React.FC = ({ + children, +}) => { // Get the dialog context to control the dialog's open state const { onOpenChange } = React.useContext(SimpleDialogContext); - + // Create a clickable wrapper that opens the dialog const handleClick = (e: React.MouseEvent) => { e.preventDefault(); console.log('SimpleDialogTrigger: Opening dialog'); onOpenChange(true); }; - + // If asChild is true, we'd clone the child element and add an onClick handler // For simplicity, we'll just wrap the children in a div with an onClick return ( - + {children} ); }; -const SimpleDialog: React.FC = ({ - isOpen, +const SimpleDialog: React.FC = ({ + isOpen, open, - onOpenChange, - children, - className + onOpenChange, + children, + className, }) => { // Support both isOpen and open props for compatibility - const isDialogOpen = open !== undefined ? open : isOpen !== undefined ? isOpen : false; - + const isDialogOpen = + open !== undefined ? open : isOpen !== undefined ? isOpen : false; + // Handle ESC key press useEffect(() => { if (!isDialogOpen) return; - + const onKeyDown = (e: KeyboardEvent) => { if (e.key === 'Escape') { onOpenChange(false); } }; - + window.addEventListener('keydown', onKeyDown); return () => window.removeEventListener('keydown', onKeyDown); }, [isDialogOpen, onOpenChange]); // Provide the dialog state to all children via context return ( - + {isDialogOpen ? ( onOpenChange(false)} /> @@ -190,10 +211,10 @@ const SimpleDialog: React.FC = ({ }; // For compatibility with existing code -const SimpleDialogHeader: React.FC<{ className?: string; children: React.ReactNode }> = ({ - className, - children -}) => ( +const SimpleDialogHeader: React.FC<{ + className?: string; + children: React.ReactNode; +}> = ({ className, children }) => ( ); -const SimpleDialogFooter: React.FC<{ className?: string; children: React.ReactNode }> = ({ - className, - children -}) => ( +const SimpleDialogFooter: React.FC<{ + className?: string; + children: React.ReactNode; +}> = ({ className, children }) => ( { children: React.ReactNode; asChild?: boolean; onClick?: (e: React.MouseEvent) => void; className?: string; - [key: string]: any; // Allow any other props } -interface SimpleDropdownMenuContentProps { +interface SimpleDropdownMenuContentProps extends React.HTMLAttributes { children: React.ReactNode; className?: string; sideOffset?: number; - [key: string]: any; + align?: string; } -interface SimpleDropdownMenuItemProps { +interface SimpleDropdownMenuItemProps extends React.HTMLAttributes { children: React.ReactNode; className?: string; onClick?: (e: React.MouseEvent) => void; - [key: string]: any; + disabled?: boolean; + title?: string; } -interface SimpleDropdownMenuSeparatorProps { +interface SimpleDropdownMenuSeparatorProps extends React.HTMLAttributes { className?: string; - [key: string]: any; } // The root dropdown component @@ -58,7 +57,7 @@ const SimpleDropdownMenu: React.FC = ({ children }) => // Pass the open state and toggle function to the children if (child.type === SimpleDropdownMenuTrigger) { - return React.cloneElement(child as React.ReactElement, { + return React.cloneElement(child as React.ReactElement, { onClick: (e: React.MouseEvent) => { e.stopPropagation(); // Prevent event bubbling setIsOpen(!isOpen); @@ -123,14 +122,20 @@ const SimpleDropdownMenuTrigger: React.FC = ({ // The dropdown content container const SimpleDropdownMenuContent = React.forwardRef( ({ children, className, sideOffset = 4, ...props }, ref) => { + // Use sideOffset for positioning + const offsetStyles = { + marginTop: `${sideOffset}px` + }; + return ( {children} diff --git a/src/components/ui/input.tsx b/src/components/ui/Input.tsx similarity index 97% rename from src/components/ui/input.tsx rename to src/components/ui/Input.tsx index 38d70f0..ab36a80 100644 --- a/src/components/ui/input.tsx +++ b/src/components/ui/Input.tsx @@ -18,6 +18,7 @@ const Input = React.forwardRef< /> ); }); + Input.displayName = 'Input'; -export { Input }; +export { Input }; \ No newline at end of file diff --git a/src/components/ui/simple-label.tsx b/src/components/ui/Label.tsx similarity index 100% rename from src/components/ui/simple-label.tsx rename to src/components/ui/Label.tsx diff --git a/src/components/ui/simple-slot.tsx b/src/components/ui/Slot.tsx similarity index 77% rename from src/components/ui/simple-slot.tsx rename to src/components/ui/Slot.tsx index cf335be..eed4f8c 100644 --- a/src/components/ui/simple-slot.tsx +++ b/src/components/ui/Slot.tsx @@ -2,11 +2,12 @@ import React from 'react'; // A simple Slot component that just renders its children // This is a simplified version that doesn't try to do ref forwarding -const Slot: React.FC<{ - children?: React.ReactNode; +interface SlotProps extends React.HTMLAttributes { + children?: React.ReactNode; className?: string; - [key: string]: any; -}> = ({ +} + +const Slot: React.FC = ({ children, ...props }) => { diff --git a/src/components/ui/simple-tooltip.tsx b/src/components/ui/Tooltip.tsx similarity index 100% rename from src/components/ui/simple-tooltip.tsx rename to src/components/ui/Tooltip.tsx diff --git a/src/components/ui/index.ts b/src/components/ui/index.ts new file mode 100644 index 0000000..41b89ba --- /dev/null +++ b/src/components/ui/index.ts @@ -0,0 +1,44 @@ +/** + * Barrel file for UI components + * + * Provides unified access to reusable UI components + * that serve as building blocks across the application. + */ + +// Export individual UI components +export { Button } from './Button'; +export { Input } from './Input'; +export { Slot } from './Slot'; +export { ConfirmationDialog } from './ConfirmationDialog'; + +// Export dialog components +export { + SimpleDialog, + SimpleDialogContent, + SimpleDialogTitle, + SimpleDialogDescription, + SimpleDialogFooter, + SimpleDialogHeader +} from './Dialog'; + +// Export dropdown components +export { + SimpleDropdownMenu, + SimpleDropdownMenuTrigger, + SimpleDropdownMenuContent, + SimpleDropdownMenuItem, + SimpleDropdownMenuSeparator +} from './Dropdown'; + +// Export tooltip components +export { + Tooltip, + TooltipTrigger, + TooltipContent, + TooltipProvider +} from './BetterTooltip'; + +// Export question counter components +export { default as QuestionCounter } from './questionCounter/QuestionCounter'; +export { default as LargeCircularQuestionCounter } from './questionCounter/LargeCircularQuestionCounter'; +export { default as SmallCircularQuestionCounter } from './questionCounter/SmallCircularQuestionCounter'; \ No newline at end of file diff --git a/src/components/ui/questionCounter/smallCircularQuestionCounter.tsx b/src/components/ui/questionCounter/SmallCircularQuestionCounter.tsx similarity index 58% rename from src/components/ui/questionCounter/smallCircularQuestionCounter.tsx rename to src/components/ui/questionCounter/SmallCircularQuestionCounter.tsx index 07e90b8..65933c2 100644 --- a/src/components/ui/questionCounter/smallCircularQuestionCounter.tsx +++ b/src/components/ui/questionCounter/SmallCircularQuestionCounter.tsx @@ -1,15 +1,34 @@ import React from 'react'; -import { useAnsweredCount } from '../../../features/questions/hooks/useAnsweredCount'; +import { useAnsweredCount, useAnsweredCountByCategory } from '../../../features/questions/hooks/useAnsweredCount'; interface SmallCircularQuestionCounterProps { size?: number; // default 24px strokeWidth?: number; // default 2px + categoryId?: string; // optional category ID to show progress for specific category } const SmallCircularQuestionCounter: React.FC< SmallCircularQuestionCounterProps -> = ({ size = 24, strokeWidth = 4 }) => { - const { answered, total } = useAnsweredCount(); +> = ({ size = 24, strokeWidth = 4, categoryId }) => { + // If categoryId is provided, get progress for that category, otherwise get overall progress + let answered = 0; + let total = 0; + + if (categoryId) { + const { categoryCounts } = useAnsweredCountByCategory(); + const normalizedCategoryId = categoryId.toLowerCase(); + const categoryCount = categoryCounts[normalizedCategoryId]; + + if (categoryCount) { + answered = categoryCount.answered; + total = categoryCount.total; + } + } else { + const counts = useAnsweredCount(); + answered = counts.answered; + total = counts.total; + } + const progress = total > 0 ? Math.round((answered / total) * 100) : 0; const radius = (size - strokeWidth) / 2; @@ -30,7 +49,7 @@ const SmallCircularQuestionCounter: React.FC< /> {/* Progress circle */} { const context = useContext(AuthContext); diff --git a/src/features/auth/components/LoginPage.tsx b/src/features/auth/components/LoginPage.tsx index 7822c4f..f186b46 100644 --- a/src/features/auth/components/LoginPage.tsx +++ b/src/features/auth/components/LoginPage.tsx @@ -4,9 +4,9 @@ import React, { useState, useEffect } from 'react'; import { useAuth } from '../api/hooks'; import { useEntries } from '../../statements/hooks/useEntries'; import MagicLinkForm from './MagicLinkForm'; -import { Input } from '../../../components/ui/input'; -import { Button } from '../../../components/ui/button'; -import { handleMagicLinkVerification } from '../authUtils'; +import { Input } from '../../../components/ui/Input'; +import { Button } from '../../../components/ui/Button'; +import { handleMagicLinkVerification } from '../utils/authUtils'; import { Loader2 } from 'lucide-react'; import PrivacyModal from '../../../components/modals/PrivacyModal'; import TermsModal from '../../../components/modals/TermsModal'; @@ -19,6 +19,7 @@ interface LoginPageProps { ) => void; } +// Forcing readd file to be tracked by github const LoginPage: React.FC = ({ onSubmit }) => { const { state } = useAuth(); const { setData } = useEntries(); @@ -30,7 +31,7 @@ const LoginPage: React.FC = ({ onSubmit }) => { const [isPrivacyModalOpen, setIsPrivacyModalOpen] = useState(false); const [isTermsModalOpen, setIsTermsModalOpen] = useState(false); - // Handle token verification on initial load and URL changes + // Force Redeploy useEffect(() => { const verifyToken = async () => { setVerifying(true); diff --git a/src/features/auth/components/MagicLinkForm.tsx b/src/features/auth/components/MagicLinkForm.tsx index 099e3b6..e9a2f0e 100644 --- a/src/features/auth/components/MagicLinkForm.tsx +++ b/src/features/auth/components/MagicLinkForm.tsx @@ -1,8 +1,8 @@ import React, { useState } from 'react'; import { useAuth } from '../api/hooks'; import { validateEmail } from '@/lib/utils/validateEmail'; -import { Input } from '@/components/ui/input'; -import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/Input'; +import { Button } from '@/components/ui/Button'; import { Mail, Loader2 } from 'lucide-react'; interface MagicLinkFormProps { diff --git a/src/features/auth/AuthContext.ts b/src/features/auth/context/AuthContext.ts similarity index 95% rename from src/features/auth/AuthContext.ts rename to src/features/auth/context/AuthContext.ts index 7410e25..707c020 100644 --- a/src/features/auth/AuthContext.ts +++ b/src/features/auth/context/AuthContext.ts @@ -1,5 +1,5 @@ import { createContext } from 'react'; -import { AuthState } from './api/authApi'; +import { AuthState } from '../api/authApi'; // Initial auth state export const initialAuthState: AuthState = { diff --git a/src/features/auth/AuthProvider.tsx b/src/features/auth/context/AuthProvider.tsx similarity index 99% rename from src/features/auth/AuthProvider.tsx rename to src/features/auth/context/AuthProvider.tsx index 8fec6c3..f925166 100644 --- a/src/features/auth/AuthProvider.tsx +++ b/src/features/auth/context/AuthProvider.tsx @@ -5,7 +5,7 @@ import { requestMagicLink as apiRequestMagicLink, signOut as apiSignOut, updateUserProfile -} from './api/authApi'; +} from '../api/authApi'; type AuthAction = | { type: 'AUTH_LOADING' } diff --git a/src/features/auth/index.ts b/src/features/auth/index.ts new file mode 100644 index 0000000..a02a11a --- /dev/null +++ b/src/features/auth/index.ts @@ -0,0 +1,20 @@ +/** + * Barrel file for auth feature + * + * Provides unified access to authentication-related functionality + * including context, providers, hooks, and components. + */ + +// Re-export context and provider +export { AuthContext } from './context/AuthContext'; +export { AuthProvider } from './context/AuthProvider'; + +// Re-export hooks +export { useAuth } from './api/hooks'; + +// Re-export utilities +export { handleMagicLinkVerification } from './utils/authUtils'; + +// Re-export components as needed +export { default as LoginPage } from './components/LoginPage'; +export { default as MagicLinkForm } from './components/MagicLinkForm'; \ No newline at end of file diff --git a/src/features/auth/authUtils.ts b/src/features/auth/utils/authUtils.ts similarity index 96% rename from src/features/auth/authUtils.ts rename to src/features/auth/utils/authUtils.ts index 5fd3bff..585bc20 100644 --- a/src/features/auth/authUtils.ts +++ b/src/features/auth/utils/authUtils.ts @@ -1,4 +1,4 @@ -import { verifyMagicLink } from './api/authApi'; +import { verifyMagicLink } from '../api/authApi'; /** * Extract and verify magic link token from URL diff --git a/src/features/email/api/gratitudeApi.ts b/src/features/email/api/emailGratitudeApi.ts similarity index 100% rename from src/features/email/api/gratitudeApi.ts rename to src/features/email/api/emailGratitudeApi.ts diff --git a/src/features/email/api/emailApi.ts b/src/features/email/api/emailStatementsApi.ts similarity index 80% rename from src/features/email/api/emailApi.ts rename to src/features/email/api/emailStatementsApi.ts index bbf84a4..0bb5c63 100644 --- a/src/features/email/api/emailApi.ts +++ b/src/features/email/api/emailStatementsApi.ts @@ -1,9 +1,9 @@ -import { Email } from "../../../types/emails"; +import { Email } from '../../../types/emails'; // Check if we should use mock implementation const shouldUseMock = () => { return ( - import.meta.env.VITE_MOCK_EMAIL_SENDING === 'true' || + import.meta.env.VITE_MOCK_EMAIL_SENDING === 'true' || typeof import.meta.env.VITE_MOCK_EMAIL_SENDING === 'undefined' ); }; @@ -11,15 +11,15 @@ const shouldUseMock = () => { // Mock implementation of sending email const mockSendEmail = async (email: Email) => { console.log('MOCK: Sending email with:', email); - + // Simulate network delay - await new Promise(resolve => setTimeout(resolve, 800)); - + await new Promise((resolve) => setTimeout(resolve, 800)); + // Return mock success response return { success: true, message: 'Email sent successfully (mock)', - id: `mock-email-${Date.now()}` + id: `mock-email-${Date.now()}`, }; }; @@ -29,7 +29,7 @@ export async function sendEmail(email: Email) { if (shouldUseMock()) { return mockSendEmail(email); } - + // Real implementation try { const response = await fetch('/api/email/send', { @@ -39,31 +39,31 @@ export async function sendEmail(email: Email) { }, body: JSON.stringify(email), }); - + if (!response.ok) { const errorData = await response.json(); throw new Error(errorData.message || 'Failed to send email'); } - + return await response.json(); } catch (error) { - console.error("Error sending email:", error); + console.error('Error sending email:', error); throw error; } } // Mock implementation of sharing statements const mockShareStatements = async (recipientEmail: string) => { - console.log('MOCK: Sharing statements with:', recipientEmail); - + console.log('MOCK: Sharing statements from:', recipientEmail); + // Simulate network delay - await new Promise(resolve => setTimeout(resolve, 800)); - + await new Promise((resolve) => setTimeout(resolve, 800)); + // Return mock success response return { success: true, message: 'Statements shared successfully (mock)', - id: `mock-share-${Date.now()}` + id: `mock-share-${Date.now()}`, }; }; @@ -73,7 +73,7 @@ export async function shareStatements(recipientEmail: string) { if (shouldUseMock()) { return mockShareStatements(recipientEmail); } - + // Real implementation try { const response = await fetch('/api/email/share-statements', { @@ -83,15 +83,15 @@ export async function shareStatements(recipientEmail: string) { }, body: JSON.stringify({ recipientEmail }), }); - + if (!response.ok) { const errorData = await response.json(); throw new Error(errorData.message || 'Failed to share statements'); } - + return await response.json(); } catch (error) { - console.error("Error sharing statements:", error); + console.error('Error sharing statements:', error); throw error; } -} \ No newline at end of file +} diff --git a/src/components/ui/tour/HelpButton.tsx b/src/features/help/components/HelpButton.tsx similarity index 100% rename from src/components/ui/tour/HelpButton.tsx rename to src/features/help/components/HelpButton.tsx diff --git a/src/components/ui/tour/HelpCenter.tsx b/src/features/help/components/HelpCenter.tsx similarity index 83% rename from src/components/ui/tour/HelpCenter.tsx rename to src/features/help/components/HelpCenter.tsx index e101149..9f108fd 100644 --- a/src/components/ui/tour/HelpCenter.tsx +++ b/src/features/help/components/HelpCenter.tsx @@ -1,8 +1,8 @@ 'use client'; import React, { useState } from 'react'; -import { X, Info, PlayCircle, BookOpen, History } from 'lucide-react'; -import { Button } from '@/components/ui/button'; +import { X, Info, PlayCircle, BookOpen, History, Link as LinkIcon } from 'lucide-react'; +import { Button } from '@/components/ui/Button'; import helpContent from '@/data/helpContent.json'; interface HelpCenterProps { @@ -10,7 +10,7 @@ interface HelpCenterProps { initialTab?: string; } -type TabType = 'welcome' | 'tutorials' | 'features' | 'versions'; +type TabType = 'welcome' | 'tutorials' | 'features' | 'resources' | 'versions'; const HelpCenter: React.FC = ({ onClose, @@ -73,7 +73,7 @@ const HelpCenter: React.FC = ({ {/* Tabs - grid for mobile, flex for desktop */} - + = ({ Features + handleTabChange('resources')} + > + + Resources + = ({ )} + {/* Resources Tab */} + {activeTab === 'resources' && ( + + + Helpful Resources + + + + {helpContent.resources.map((resource, index) => ( + + + + + {resource.title} + + + {resource.description} + + + + ))} + + + )} + {/* Versions Tab */} {activeTab === 'versions' && ( @@ -272,4 +313,4 @@ const HelpCenter: React.FC = ({ ); }; -export default HelpCenter; +export default HelpCenter; \ No newline at end of file diff --git a/src/components/ui/tour/WelcomePanel.tsx b/src/features/help/components/WelcomePanel.tsx similarity index 98% rename from src/components/ui/tour/WelcomePanel.tsx rename to src/features/help/components/WelcomePanel.tsx index cd73694..465f255 100644 --- a/src/components/ui/tour/WelcomePanel.tsx +++ b/src/features/help/components/WelcomePanel.tsx @@ -2,7 +2,7 @@ import React, { useEffect, useState } from 'react'; import { Info, PlayCircle, ArrowRight, X } from 'lucide-react'; -import { Button } from '@/components/ui/button'; +import { Button } from '@/components/ui/Button'; import helpContent from '@/data/helpContent.json'; interface WelcomePanelProps { diff --git a/src/components/ui/tour/index.ts b/src/features/help/components/index.ts similarity index 71% rename from src/components/ui/tour/index.ts rename to src/features/help/components/index.ts index 8aa8ba7..3ab2076 100644 --- a/src/components/ui/tour/index.ts +++ b/src/features/help/components/index.ts @@ -1,4 +1,3 @@ -export { default as HelpProvider, useHelp } from './HelpProvider'; export { default as HelpButton } from './HelpButton'; export { default as HelpCenter } from './HelpCenter'; export { default as WelcomePanel } from './WelcomePanel'; \ No newline at end of file diff --git a/src/features/help/context/HelpContext.ts b/src/features/help/context/HelpContext.ts new file mode 100644 index 0000000..b896f0b --- /dev/null +++ b/src/features/help/context/HelpContext.ts @@ -0,0 +1,9 @@ +'use client'; + +import { createContext } from 'react'; + +export interface HelpContextType { + showHelpCenter: (tab?: string) => void; +} + +export const HelpContext = createContext(undefined); diff --git a/src/components/ui/tour/HelpProvider.tsx b/src/features/help/context/HelpProvider.tsx similarity index 77% rename from src/components/ui/tour/HelpProvider.tsx rename to src/features/help/context/HelpProvider.tsx index f0deb58..01bbde3 100644 --- a/src/components/ui/tour/HelpProvider.tsx +++ b/src/features/help/context/HelpProvider.tsx @@ -1,23 +1,10 @@ 'use client'; -import React, { createContext, useContext, useState, useEffect } from 'react'; -import WelcomePanel from './WelcomePanel'; -import HelpButton from './HelpButton'; -import HelpCenter from './HelpCenter'; - -interface HelpContextType { - showHelpCenter: (tab?: string) => void; -} - -const HelpContext = createContext(undefined); - -export const useHelp = () => { - const context = useContext(HelpContext); - if (!context) { - throw new Error('useHelp must be used within a HelpProvider'); - } - return context; -}; +import React, { useState, useEffect } from 'react'; +import { HelpContext } from './HelpContext'; +import WelcomePanel from '../components/WelcomePanel'; +import HelpButton from '../components/HelpButton'; +import HelpCenter from '../components/HelpCenter'; interface HelpProviderProps { children: React.ReactNode; @@ -91,6 +78,4 @@ export const HelpProvider: React.FC = ({ children }) => { )} ); -}; - -export default HelpProvider; \ No newline at end of file +}; \ No newline at end of file diff --git a/src/features/help/hooks/useHelp.ts b/src/features/help/hooks/useHelp.ts new file mode 100644 index 0000000..8f051e7 --- /dev/null +++ b/src/features/help/hooks/useHelp.ts @@ -0,0 +1,12 @@ +'use client'; + +import { useContext } from 'react'; +import { HelpContext } from '../context/HelpContext'; + +export const useHelp = () => { + const context = useContext(HelpContext); + if (!context) { + throw new Error('useHelp must be used within a HelpProvider'); + } + return context; +}; diff --git a/src/features/help/index.ts b/src/features/help/index.ts new file mode 100644 index 0000000..70eef64 --- /dev/null +++ b/src/features/help/index.ts @@ -0,0 +1,10 @@ +/** + * Barrel file for help feature + * + * Provides unified access to help center functionality + * including context, providers, hooks, and components. + */ + +export { HelpProvider } from './context/HelpProvider'; +export { useHelp } from './hooks/useHelp'; +export { HelpButton, HelpCenter, WelcomePanel } from './components'; \ No newline at end of file diff --git a/src/components/ui/progress/ProgressWithFeedback.tsx b/src/features/progress/components/ProgressWithFeedback.tsx similarity index 84% rename from src/components/ui/progress/ProgressWithFeedback.tsx rename to src/features/progress/components/ProgressWithFeedback.tsx index 2d8c950..895afe4 100644 --- a/src/components/ui/progress/ProgressWithFeedback.tsx +++ b/src/features/progress/components/ProgressWithFeedback.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { useAnsweredCount } from '@/features/questions/hooks/useAnsweredCount'; -import { useProgressFeedback } from '@/hooks/useProgressFeedback'; -import LargeCircularQuestionCounter from '../questionCounter/LargeCircularQuestionCounter'; +import { useProgressFeedback } from '../hooks/useProgressFeedback'; +import { LargeCircularQuestionCounter } from '@/components/ui'; const ProgressWithFeedback: React.FC = () => { const { answered, total } = useAnsweredCount(); diff --git a/src/hooks/useProgressFeedback.ts b/src/features/progress/hooks/useProgressFeedback.ts similarity index 100% rename from src/hooks/useProgressFeedback.ts rename to src/features/progress/hooks/useProgressFeedback.ts diff --git a/src/providers/QuestionsContext.ts b/src/features/questions/context/QuestionsContext.ts similarity index 100% rename from src/providers/QuestionsContext.ts rename to src/features/questions/context/QuestionsContext.ts diff --git a/src/providers/QuestionsProvider.tsx b/src/features/questions/context/QuestionsProvider.tsx similarity index 100% rename from src/providers/QuestionsProvider.tsx rename to src/features/questions/context/QuestionsProvider.tsx diff --git a/src/features/questions/hooks/useQuestions.ts b/src/features/questions/hooks/useQuestions.ts index 02ce37c..0505e16 100644 --- a/src/features/questions/hooks/useQuestions.ts +++ b/src/features/questions/hooks/useQuestions.ts @@ -1,5 +1,5 @@ import { useContext } from 'react'; -import { QuestionsContext } from '../../../providers/QuestionsContext'; +import { QuestionsContext } from '../context/QuestionsContext'; export function useQuestions() { const context = useContext(QuestionsContext); diff --git a/src/features/questions/index.ts b/src/features/questions/index.ts new file mode 100644 index 0000000..80d2166 --- /dev/null +++ b/src/features/questions/index.ts @@ -0,0 +1,14 @@ +/** + * Barrel file for questions feature + * + * Provides unified access to questions-related functionality + * including context, providers, hooks, and components. + */ + +// Re-export context and provider +export { QuestionsContext } from './context/QuestionsContext'; +export { QuestionsProvider } from './context/QuestionsProvider'; + +// Re-export hooks +export { useQuestions } from './hooks/useQuestions'; +export { useAnsweredCount } from './hooks/useAnsweredCount'; \ No newline at end of file diff --git a/src/features/statements/components/ActionForm.tsx b/src/features/statements/components/ActionForm.tsx index caeb7b9..69db985 100644 --- a/src/features/statements/components/ActionForm.tsx +++ b/src/features/statements/components/ActionForm.tsx @@ -1,9 +1,10 @@ // src/components/statements/ActionForm.tsx import React from 'react'; -import { Input } from '../../../components/ui/input'; -import { Button } from '../../../components/ui/button'; +import { Input } from '../../../components/ui/Input'; +import { Button } from '../../../components/ui/Button'; import { Save, X } from 'lucide-react'; +// Force Redeploy export interface ActionFormProps { initialText?: string; initialDueDate?: string; diff --git a/src/features/statements/components/ActionLine.tsx b/src/features/statements/components/ActionLine.tsx index 9ab56db..3359f99 100644 --- a/src/features/statements/components/ActionLine.tsx +++ b/src/features/statements/components/ActionLine.tsx @@ -13,19 +13,19 @@ import { SimpleDropdownMenuContent as DropdownMenuContent, SimpleDropdownMenuItem as DropdownMenuItem, SimpleDropdownMenuSeparator as DropdownMenuSeparator, -} from '../../../components/ui/simple-dropdown'; +} from '../../../components/ui/Dropdown'; import ActionForm from './ActionForm'; -import { ConfirmationDialog } from '../../../components/ui/confirmation-dialog'; +import { ConfirmationDialog } from '../../../components/ui/ConfirmationDialog'; import type { Action } from '../../../types/entries'; import { CheckCircle2, XCircle } from 'lucide-react'; import GratitudeModal from '../../../components/modals/GratitudeModal'; -import { markGratitudeSent } from '../../../features/email/api/gratitudeApi'; +import { markGratitudeSent } from '../../email/api/emailGratitudeApi'; import { useEntries } from '../hooks/useEntries'; import { Tooltip, TooltipTrigger, TooltipContent, -} from '../../../components/ui/better-tooltip'; +} from '../../../components/ui/BetterTooltip'; export interface ActionLineProps { statementId: string; @@ -62,9 +62,12 @@ const ActionLine: React.FC = ({ action: Action | null; }>({ isOpen: false, action: null }); - // Get entries data to check for manager email + // Get entries data to check for manager email and find current statement const { data } = useEntries(); const hasManagerEmail = data.managerEmail && data.managerEmail.trim() !== ''; + + // Find the current statement to pass its details to the gratitude modal + const currentStatement = data.entries.find(entry => entry.id === statementId); // --- Handlers for Editing --- const handleStartEdit = (action: Action) => { @@ -114,7 +117,7 @@ const ActionLine: React.FC = ({ }; return ( - + {actions.map((action) => { const isEditing = editingActionId === action.id; if (!isEditing) { @@ -253,16 +256,24 @@ const ActionLine: React.FC = ({ {!action.gratitude?.sent && ( <> - + { if (hasManagerEmail) { setGratitudeModal({ isOpen: true, action }); } }} - className={hasManagerEmail ? "text-pink-600" : "text-pink-300 cursor-not-allowed"} + className={ + hasManagerEmail + ? 'text-pink-600' + : 'text-pink-300 cursor-not-allowed' + } disabled={!hasManagerEmail} - title={!hasManagerEmail ? "Manager's email is required to send gratitude" : ""} + title={ + !hasManagerEmail + ? "Manager's email is required to send gratitude" + : '' + } > Send gratitude @@ -314,6 +325,10 @@ const ActionLine: React.FC = ({ onClose={() => setGratitudeModal({ isOpen: false, action: null })} statementId={statementId} action={gratitudeModal.action} + statement={currentStatement ? { + input: currentStatement.input, + description: currentStatement.description + } : undefined} onGratitudeSent={async (stmtId, actionId, message) => { try { // Call the API to mark gratitude as sent diff --git a/src/features/statements/components/ActionsCounter.tsx b/src/features/statements/components/ActionsCounter.tsx index b91e2d3..c88930e 100644 --- a/src/features/statements/components/ActionsCounter.tsx +++ b/src/features/statements/components/ActionsCounter.tsx @@ -17,12 +17,12 @@ const ActionsCounter: React.FC = ({ // Tab-like styling similar to category tabs const baseClasses = 'inline-flex items-center px-3 py-1 text-sm transition-colors cursor-pointer whitespace-nowrap'; - + // Determine border radius and border styling based on expanded state - const borderStyles = expanded - ? 'rounded-t-lg border-t border-l border-r' + const borderStyles = expanded + ? 'rounded-t-lg border-t border-l border-r' : 'rounded-lg border'; - + const backgroundClasses = count > 0 ? `bg-brand-pink text-white ${borderStyles} border-brand-pink` diff --git a/src/features/statements/components/QuestionCard.tsx b/src/features/statements/components/QuestionCard.tsx index 95439f2..82e124a 100644 --- a/src/features/statements/components/QuestionCard.tsx +++ b/src/features/statements/components/QuestionCard.tsx @@ -8,7 +8,7 @@ import { Tooltip, TooltipTrigger, TooltipContent, -} from '../../../components/ui/better-tooltip'; +} from '../../../components/ui/BetterTooltip'; export interface QuestionCardProps { presetQuestion: SetQuestion; diff --git a/src/features/statements/components/StatementItem.tsx b/src/features/statements/components/StatementItem.tsx index b450135..a8654c9 100644 --- a/src/features/statements/components/StatementItem.tsx +++ b/src/features/statements/components/StatementItem.tsx @@ -1,5 +1,5 @@ import React, { useEffect } from 'react'; -import { Button } from '@/components/ui/button'; +import { Button } from '@/components/ui/Button'; import { getVerbName } from '@/lib/utils/verbUtils'; import { Trash2, @@ -19,23 +19,36 @@ import { SimpleDropdownMenuTrigger as DropdownMenuTrigger, SimpleDropdownMenuContent as DropdownMenuContent, SimpleDropdownMenuItem as DropdownMenuItem, -} from '@/components/ui/simple-dropdown'; +} from '@/components/ui/Dropdown'; import ActionsCounter from './ActionsCounter'; import ActionLine from './ActionLine'; import { Tooltip, TooltipTrigger, TooltipContent, -} from '@/components/ui/better-tooltip'; +} from '@/components/ui/BetterTooltip'; import statementsCategories from '@/data/statementsCategories.json'; import { formatCategoryName } from '@/lib/utils'; export interface StatementItemProps { statement: Entry; isEditing: boolean; - editingPart: 'subject' | 'verb' | 'object' | 'category' | 'privacy' | null; + editingPart: + | 'subject' + | 'verb' + | 'object' + | 'category' + | 'description' + | 'privacy' + | null; onPartClick: ( - part: 'subject' | 'verb' | 'object' | 'category' | 'privacy', + part: + | 'subject' + | 'verb' + | 'object' + | 'category' + | 'description' + | 'privacy', statementId: string ) => void; // When the green save icon is clicked, the updated entry (draft) is passed back @@ -133,6 +146,9 @@ const StatementItem: React.FC = ({ const [originalPrivacy, setOriginalPrivacy] = React.useState( null ); + const [originalDescription, setOriginalDescription] = React.useState< + string | null + >(null); // Local "draft" state to track current modifications const [draft, setDraft] = React.useState(statement); @@ -156,6 +172,7 @@ const StatementItem: React.FC = ({ setOriginalVerb(statement.atoms.verb); setOriginalObject(statement.atoms.object); setOriginalPrivacy(statement.isPublic); + setOriginalDescription(statement.description || null); } // Always keep draft updated with latest statement value @@ -167,6 +184,7 @@ const StatementItem: React.FC = ({ setOriginalVerb(null); setOriginalObject(null); setOriginalPrivacy(null); + setOriginalDescription(null); setDraft(JSON.parse(JSON.stringify(statement))); } @@ -232,6 +250,7 @@ const StatementItem: React.FC = ({ let hasObjectChanged = false; let hasPrivacyChanged = false; let hasCategoryChanged = false; + let hasDescriptionChanged = false; let hasChanged = false; if (isEditing) { @@ -246,6 +265,7 @@ const StatementItem: React.FC = ({ hasVerbChanged = draft.atoms.verb !== originalVerb; hasObjectChanged = draft.atoms.object !== originalObject; hasPrivacyChanged = draft.isPublic !== originalPrivacy; + hasDescriptionChanged = draft.description !== originalDescription; // Normalize categories for comparison const draftCategory = normalizeCategoryForComparison(draft.category); @@ -262,7 +282,8 @@ const StatementItem: React.FC = ({ hasVerbChanged || hasObjectChanged || hasPrivacyChanged || - hasCategoryChanged; + hasCategoryChanged || + hasDescriptionChanged; } } @@ -306,7 +327,7 @@ const StatementItem: React.FC = ({ onClick={() => onPartClick('verb', draft.id)} className='cursor-pointer px-2 py-1 rounded bg-verbSelector hover:bg-verbSelectorHover' > - {getVerbName(draft.atoms.verb)} + {getVerbName(draft.atoms.verb, draft.atoms.subject === 'I')} {/* Object */} = ({ className='cursor-pointer px-2 py-1 rounded bg-categorySelector text-black flex items-center gap-1 hover:bg-categorySelectorHover' > π - {/* Use formatted category name */} - {draft.category && - draft.category.toLowerCase() !== 'uncategorized' && - draft.category.toLowerCase() !== 'uncategorised' - ? getCategoryDisplayName(draft.category) - : 'Uncategorized'} + {/* Use formatted category name - truncated to 10 chars */} + + {draft.category && + draft.category.toLowerCase() !== 'uncategorized' && + draft.category.toLowerCase() !== 'uncategorised' + ? getCategoryDisplayName(draft.category) + : 'Uncategorized'} + + + + {/* Description - always show with placeholder if empty */} + onPartClick('description', draft.id)} + className='cursor-pointer px-2 py-1 rounded bg-[var(--description-input,#8BB8E8)] bg-opacity-20 text-black flex items-center gap-1 hover:bg-opacity-30 ' + > + {draft.description && draft.description.trim().length > 0 ? ( + + {draft.description} + + ) : ( + + No description + + )} @@ -344,7 +383,7 @@ const StatementItem: React.FC = ({ const updatedDraft = { ...draft }; updatedDraft.input = `${ draft.atoms.subject - } ${getVerbName(draft.atoms.verb)} ${draft.atoms.object}`; + } ${getVerbName(draft.atoms.verb, draft.atoms.subject === 'I')} ${draft.atoms.object}`; await onLocalSave(updatedDraft); setIsSaving(false); @@ -422,7 +461,7 @@ const StatementItem: React.FC = ({ className='cursor-pointer p-2 rounded bg-verbSelector hover:bg-verbSelectorHover' > - {getVerbName(draft.atoms.verb)} + {getVerbName(draft.atoms.verb, draft.atoms.subject === 'I')} @@ -440,7 +479,7 @@ const StatementItem: React.FC = ({ className='cursor-pointer p-2 rounded bg-categorySelector hover:bg-categorySelectorHover flex items-center' > π - + {draft.category && draft.category.toLowerCase() !== 'uncategorized' && draft.category.toLowerCase() !== 'uncategorised' @@ -448,6 +487,22 @@ const StatementItem: React.FC = ({ : 'Uncategorized'} + + {/* Description - always show with placeholder if empty */} + onPartClick('description', draft.id)} + className='cursor-pointer p-2 h-[38px] rounded bg-[var(--description-input,#8BB8E8)] bg-opacity-20 hover:bg-opacity-30 flex items-center' + > + {draft.description && draft.description.trim().length > 0 ? ( + + {draft.description} + + ) : ( + + No description + + )} + {/* Bottom row: Privacy toggle (left), Action buttons (right) */} @@ -513,7 +568,8 @@ const StatementItem: React.FC = ({ setIsSaving(true); const updatedDraft = { ...draft }; updatedDraft.input = `${draft.atoms.subject} ${getVerbName( - draft.atoms.verb + draft.atoms.verb, + draft.atoms.subject === 'I' )} ${draft.atoms.object}`; await onLocalSave(updatedDraft); @@ -547,7 +603,7 @@ const StatementItem: React.FC = ({ className='cursor-pointer p-3 rounded bg-verbSelector hover:bg-verbSelectorHover' > - {getVerbName(draft.atoms.verb)} + {getVerbName(draft.atoms.verb, draft.atoms.subject === 'I')} @@ -565,7 +621,7 @@ const StatementItem: React.FC = ({ className='cursor-pointer p-3 rounded bg-categorySelector hover:bg-categorySelectorHover flex items-center' > π - + {draft.category && draft.category.toLowerCase() !== 'uncategorized' && draft.category.toLowerCase() !== 'uncategorised' @@ -573,6 +629,22 @@ const StatementItem: React.FC = ({ : 'Uncategorized'} + + {/* Description - always show with placeholder if empty */} + onPartClick('description', draft.id)} + className='cursor-pointer p-3 h-[46px] rounded bg-[var(--description-input,#8BB8E8)] bg-opacity-20 hover:bg-opacity-30 flex items-center' + > + {draft.description && draft.description.trim().length > 0 ? ( + + {draft.description} + + ) : ( + + No description + + )} + {/* Action buttons - bottom fixed bar */} @@ -638,7 +710,8 @@ const StatementItem: React.FC = ({ setIsSaving(true); const updatedDraft = { ...draft }; updatedDraft.input = `${draft.atoms.subject} ${getVerbName( - draft.atoms.verb + draft.atoms.verb, + draft.atoms.subject === 'I' )} ${draft.atoms.object}`; await onLocalSave(updatedDraft); @@ -676,47 +749,59 @@ const StatementItem: React.FC = ({ )} - {/* Desktop layout (xs breakpoint and above) */} - - - {/* Privacy status icon */} - - - - {statement.isPublic ? ( - - ) : ( - - )} + {/* Desktop layout (xs breakpoint and above) - Two row layout */} + + {/* First row: Statement, privacy icon, and settings button */} + + + {/* Privacy status icon */} + + + + {statement.isPublic ? ( + + ) : ( + + )} + + + + {statement.isPublic + ? 'You are sharing this statement' + : 'This statement is private'} + + + + {/* Statement text and description in a column */} + + + {`${statement.atoms.subject} ${getVerbName( + statement.atoms.verb, + statement.atoms.subject === 'I' + )} ${statement.atoms.object}`} - - - {statement.isPublic - ? 'You are sharing this statement' - : 'This statement is private'} - - - - {/* Statement text with archived styling if needed */} - - - {`${statement.atoms.subject} ${getVerbName( - statement.atoms.verb - )} ${statement.atoms.object}`} - + + {/* Description (shown if exists) */} + {statement.description && statement.description.trim() !== '' && ( + + + {statement.description} + + + )} + - - + {/* Menu button */} e.stopPropagation()} - className='p-1.5 rounded-full hover:bg-gray-200 transition-colors' + className='p-1.5 rounded-full hover:bg-gray-200 transition-colors flex-shrink-0 mt-0.5' > @@ -755,11 +840,13 @@ const StatementItem: React.FC = ({ )} + - {/* Actions counter - now the rightmost element */} + {/* Second row: Actions counter tab aligned to the right */} + setIsActionsExpanded((prev) => !prev)} - className='cursor-pointer relative z-10 self-end' + className='cursor-pointer relative z-10' > = ({ } break-words line-clamp-2`} > {`${statement.atoms.subject} ${getVerbName( - statement.atoms.verb + statement.atoms.verb, + statement.atoms.subject === 'I' )} ${statement.atoms.object}`} + + {/* Description (mobile only) */} + {statement.description && statement.description.trim() !== '' && ( + + {statement.description} + + )} diff --git a/src/features/statements/components/StatementList.tsx b/src/features/statements/components/StatementList.tsx index 7fdf256..57dc7d9 100644 --- a/src/features/statements/components/StatementList.tsx +++ b/src/features/statements/components/StatementList.tsx @@ -2,7 +2,7 @@ import React, { useState } from 'react'; import { useEntries } from '../hooks/useEntries'; -import { ConfirmationDialog } from '../../../components/ui/confirmation-dialog'; +import { ConfirmationDialog } from '../../../components/ui/ConfirmationDialog'; import type { Entry, SetQuestion } from '@/types/entries'; import QuestionCard from './QuestionCard'; import StatementItem from './StatementItem'; @@ -14,7 +14,10 @@ import { formatCategoryName } from '@/lib/utils'; import { updateEntry } from '../api/entriesApi'; import { EditStatementModal } from '../../wizard/components/EditStatementModal'; import { BellOff, ChevronUp, ChevronDown, Plus } from 'lucide-react'; -import { Button } from '../../../components/ui/button'; +import { Button } from '../../../components/ui/Button'; +import { SmallCircularQuestionCounter } from '../../../components/ui'; + +// Force Redeploy // Helper function to normalize category IDs for consistent comparison const normalizeCategoryIdForGrouping = (id: string): string => { @@ -97,7 +100,13 @@ const StatementList: React.FC = ({ username }) => { // State for opening the modal for a specific part: const [editModalData, setEditModalData] = useState<{ statement: Entry; - editPart: 'subject' | 'verb' | 'object' | 'category' | 'privacy'; + editPart: + | 'subject' + | 'verb' + | 'object' + | 'category' + | 'privacy' + | 'description'; } | null>(null); // Keep a backup of the original entries when entering edit mode @@ -260,7 +269,13 @@ const StatementList: React.FC = ({ username }) => { // Callback for inline part clicks to open the modal: const handlePartClick = ( - part: 'subject' | 'verb' | 'object' | 'category' | 'privacy', + part: + | 'subject' + | 'verb' + | 'object' + | 'category' + | 'privacy' + | 'description', statementId: string ) => { const statementToEdit = entries.find((s) => s.id === statementId); @@ -337,6 +352,9 @@ const StatementList: React.FC = ({ username }) => { isSnoozedQuestionsSectionExpanded, setIsSnoozedQuestionsSectionExpanded, ] = useState(true); + + // State for managing the visibility of category sections + const [collapsedCategories, setCollapsedCategories] = useState>({}); // Move the hook call to the top level const { categoryCounts } = useAnsweredCountByCategory(); @@ -362,103 +380,132 @@ const StatementList: React.FC = ({ username }) => { const isComplete = categoryStatus.total > 0 && categoryStatus.answered === categoryStatus.total; + + // Check if category is collapsed + const isCollapsed = collapsedCategories[catId] || false; + + // Toggle collapsed state for this category + const toggleCollapsed = () => { + setCollapsedCategories(prev => ({ + ...prev, + [catId]: !prev[catId] + })); + }; return ( {/* Folder Tab Design */} - - {formatCategoryName(catLabel)} - + + + + + {formatCategoryName(catLabel)} + + + {isCollapsed ? ( + + ) : ( + + )} + {/* Folder Content */} - - {presetForCat.length > 0 && ( - - {presetForCat.map((presetQuestion) => ( - - - - ))} - - )} - {statementsForCat.length > 0 && ( - - {statementsForCat.map((statement) => ( - - { - // If we have the original, restore it - if (originalEntries[statement.id]) { - // Restore the original entry from our backup - setData({ - type: 'UPDATE_ENTRY', - payload: originalEntries[statement.id], - }); - - // Remove from originalEntries and originalCategories - setOriginalEntries((prev) => { - const newEntries = { ...prev }; - delete newEntries[statement.id]; - return newEntries; - }); - - // Also clear from original categories - setOriginalCategories((prev) => { - const newCategories = { ...prev }; - delete newCategories[statement.id]; - return newCategories; - }); + {!isCollapsed && ( + + {presetForCat.length > 0 && ( + + {presetForCat.map((presetQuestion) => ( + + + + ))} + + )} + {statementsForCat.length > 0 && ( + + {statementsForCat.map((statement) => ( + + { + // If we have the original, restore it + if (originalEntries[statement.id]) { + // Restore the original entry from our backup + setData({ + type: 'UPDATE_ENTRY', + payload: originalEntries[statement.id], + }); + + // Remove from originalEntries and originalCategories + setOriginalEntries((prev) => { + const newEntries = { ...prev }; + delete newEntries[statement.id]; + return newEntries; + }); + + // Also clear from original categories + setOriginalCategories((prev) => { + const newCategories = { ...prev }; + delete newCategories[statement.id]; + return newCategories; + }); + } + + // Exit edit mode + setEditingStatementId(null); + }} + onDelete={handleDeleteClick} + onEditClick={handleEditClick} + onAddAction={handleAddAction} + onEditAction={handleEditAction} + onDeleteAction={handleDeleteAction} + onReset={ + statement.presetId + ? () => handleResetClick(statement.id) + : undefined } - - // Exit edit mode - setEditingStatementId(null); - }} - onDelete={handleDeleteClick} - onEditClick={handleEditClick} - onAddAction={handleAddAction} - onEditAction={handleEditAction} - onDeleteAction={handleDeleteAction} - onReset={ - statement.presetId - ? () => handleResetClick(statement.id) - : undefined - } - onToggleArchived={handleToggleArchived} - onToggleActionResolved={(actionId: string) => - handleToggleActionResolved(statement.id, actionId) - } - /> - - ))} - - )} - + onToggleArchived={handleToggleArchived} + onToggleActionResolved={(actionId: string) => + handleToggleActionResolved(statement.id, actionId) + } + /> + + ))} + + )} + + )} ); }; diff --git a/src/features/statements/components/index.ts b/src/features/statements/components/index.ts new file mode 100644 index 0000000..35684c1 --- /dev/null +++ b/src/features/statements/components/index.ts @@ -0,0 +1,7 @@ +// Barrel file for statements components +export { default as ActionForm } from './ActionForm'; +export { default as ActionLine } from './ActionLine'; +export { default as ActionsCounter } from './ActionsCounter'; +export { default as QuestionCard } from './QuestionCard'; +export { default as StatementItem } from './StatementItem'; +export { default as StatementList } from './StatementList'; \ No newline at end of file diff --git a/src/features/statements/index.ts b/src/features/statements/index.ts new file mode 100644 index 0000000..7a59ed4 --- /dev/null +++ b/src/features/statements/index.ts @@ -0,0 +1,8 @@ +/** + * Barrel file for statements feature + * + * Provides unified access to statements-related functionality + * including context, providers, hooks, and components. + */ +export { useEntries } from './hooks/useEntries'; +export { EntriesProvider } from './context/EntriesProvider'; \ No newline at end of file diff --git a/src/features/wizard/components/EditStatementModal.tsx b/src/features/wizard/components/EditStatementModal.tsx index 1987da2..16035a1 100644 --- a/src/features/wizard/components/EditStatementModal.tsx +++ b/src/features/wizard/components/EditStatementModal.tsx @@ -4,19 +4,20 @@ import { SimpleDialogContent as DialogContent, SimpleDialogTitle as DialogTitle, SimpleDialogDescription as DialogDescription, -} from '@/components/ui/simple-dialog'; -import { Button } from '@/components/ui/button'; +} from '@/components/ui/Dialog'; +import { Button } from '@/components/ui/Button'; import type { Entry } from '@/types/entries'; import { SubjectStep } from './steps/SubjectStep'; import { VerbStep } from './steps/VerbStep'; import { ObjectStep } from './steps/ObjectStep'; import { CategoryStep } from './steps/CategoryStep'; +import { DescriptionStep } from './steps/DescriptionStep'; import { PrivacyStep } from './steps/PrivacyStep'; import { getVerbName } from '@/lib/utils/verbUtils'; interface EditStatementModalProps { statement: Entry; - editPart: 'subject' | 'verb' | 'object' | 'category' | 'privacy'; + editPart: 'subject' | 'verb' | 'object' | 'category' | 'privacy' | 'description'; username: string; onUpdate: (updatedStatement: Entry) => void; onClose: () => void; @@ -40,6 +41,8 @@ export const EditStatementModal: React.FC = ({ return statement.atoms.object; case 'category': return statement.category; + case 'description': + return statement.description || ''; case 'privacy': return statement.isPublic; default: @@ -77,7 +80,7 @@ export const EditStatementModal: React.FC = ({ // Create a completely new object with a deeper clone to ensure React detects the change // Force category to be a string to avoid type issues const categoryValue = localValue ? String(localValue) : ''; - + // Create a new object with the modified category const newStatement = { ...statement, @@ -86,13 +89,15 @@ export const EditStatementModal: React.FC = ({ _needsScroll: true, // Flag to indicate this needs scrolling category: categoryValue, }; - + // Deep clone to ensure all references are fresh updatedStatement = JSON.parse(JSON.stringify(newStatement)); console.log('Updated statement (category change):', updatedStatement); } else if (editPart === 'privacy') { updatedStatement = { ...statement, isPublic: localValue as boolean }; + } else if (editPart === 'description') { + updatedStatement = { ...statement, description: localValue as string }; } else { updatedStatement = statement; } @@ -108,7 +113,7 @@ export const EditStatementModal: React.FC = ({ object: 'border-[var(--object-input)]', category: 'border-[var(--category-selector)]', privacy: 'border-[var(--privacy-selector)]', - complement: 'border-gray-400', + description: 'border-[var(--description-input)]', }; const borderClass = borderClasses[editPart] || 'border-gray-400'; @@ -128,7 +133,8 @@ export const EditStatementModal: React.FC = ({ subject: { question: '', preset: false, - presetAnswer: null, + // Use "I" instead of username + presetAnswer: "I", allowDescriptors: true, }, verb: { @@ -218,6 +224,18 @@ export const EditStatementModal: React.FC = ({ }} /> ); + case 'description': + return ( + { + // Update the value immediately to show in preview + setLocalValue(val); + + // Don't automatically save - let the user click OK when ready + }} + /> + ); default: return null; } diff --git a/src/features/wizard/components/StatementPreview.tsx b/src/features/wizard/components/StatementPreview.tsx index b3df1e3..57767c6 100644 --- a/src/features/wizard/components/StatementPreview.tsx +++ b/src/features/wizard/components/StatementPreview.tsx @@ -17,8 +17,8 @@ const StatementPreview: React.FC = ({ selection }) => { // Get the current step from the wizard const currentStep = selection.currentStep ?? 'privacy'; - // Determine if we're using a preset question (indicated by the complement step being present) - const isPresetQuestion = !selection.currentStep || selection.currentStep === 'complement' || Boolean(selection.presetId); + // Determine if we're using a preset question + const isPresetQuestion = !selection.currentStep || Boolean(selection.presetId); // For custom statements, only show preview if we have at least a category // For preset questions, show if we have a subject @@ -26,7 +26,7 @@ const StatementPreview: React.FC = ({ selection }) => { // Only show privacy icon if we've reached or passed that step const showPrivacyIcon = - currentStep === 'privacy' || currentStep === 'complement'; + currentStep === 'privacy' || currentStep === 'description'; // Only show category if: // - For custom statements: on category step or beyond @@ -45,9 +45,9 @@ const StatementPreview: React.FC = ({ selection }) => { (currentStep === 'subject') || (currentStep === 'verb') || (currentStep === 'object') || - (currentStep === 'category' && isPresetQuestion) || - (currentStep === 'privacy') || - (currentStep === 'complement'); + (currentStep === 'privacy') || + (currentStep === 'description') || + (currentStep === 'category' && isPresetQuestion); // Get category display name from ID const getCategoryName = (categoryId: string) => { @@ -95,14 +95,14 @@ const StatementPreview: React.FC = ({ selection }) => { )} {/* Verb */} - {verb && (currentStep === 'verb' || currentStep === 'object' || currentStep === 'privacy' || currentStep === 'complement') && ( + {verb && (currentStep === 'verb' || currentStep === 'object' || currentStep === 'privacy' || currentStep === 'description') && ( - {getVerbName(verb)} + {getVerbName(verb, subject === 'I')} )} {/* Object */} - {object && (currentStep === 'object' || currentStep === 'privacy' || currentStep === 'complement') && ( + {object && (currentStep === 'object' || currentStep === 'privacy' || currentStep === 'description') && ( {object} @@ -130,6 +130,15 @@ const StatementPreview: React.FC = ({ selection }) => { {word} ))} + + {/* Description preview */} + {((selection.description && selection.description.trim().length > 0) || currentStep === 'description') && ( + + + {selection.description ? selection.description : 'Description (optional)'} + + + )} ); }; diff --git a/src/features/wizard/components/StatementWizard.tsx b/src/features/wizard/components/StatementWizard.tsx index 0195514..a3509a8 100644 --- a/src/features/wizard/components/StatementWizard.tsx +++ b/src/features/wizard/components/StatementWizard.tsx @@ -6,7 +6,7 @@ import { SimpleDialogContent as DialogContent, SimpleDialogDescription as DialogDescription, SimpleDialogTitle as DialogTitle, -} from '@/components/ui/simple-dialog'; +} from '@/components/ui/Dialog'; import { AnimatePresence, motion } from 'framer-motion'; import { useEntries } from '@/features/statements/hooks/useEntries'; import { postNewEntry } from '@/features/statements/api/entriesApi'; @@ -15,10 +15,10 @@ import { SubjectStep } from './steps/SubjectStep'; import { VerbStep } from './steps/VerbStep'; import { ObjectStep } from './steps/ObjectStep'; import { CategoryStep } from './steps/CategoryStep'; +import { DescriptionStep } from './steps/DescriptionStep'; import { PrivacyStep } from './steps/PrivacyStep'; -import { ComplementStep } from './steps/ComplementStep'; import StatementPreview from './StatementPreview'; -import { Button } from '@/components/ui/button'; +import { Button } from '@/components/ui/Button'; interface StatementWizardProps { username: string; @@ -36,20 +36,20 @@ const StatementWizard: React.FC = ({ const { setData } = useEntries(); const isPreset = Boolean(presetQuestion); - // Define steps: if preset, skip "category" and add "complement" + // Define steps: for both preset and custom statements, make description the final step // For custom statements, show category first, then subject const steps: Exclude[] = isPreset - ? ['subject', 'verb', 'object', 'privacy', 'complement'] - : ['category', 'subject', 'verb', 'object', 'privacy']; + ? ['subject', 'verb', 'object', 'privacy', 'description'] + : ['category', 'subject', 'verb', 'object', 'privacy', 'description']; // Use design tokens for border colors via Tailwindβs arbitrary value syntax: const stepBorderColors: Record, string> = { subject: 'border-[var(--subject-selector)]', verb: 'border-[var(--verb-selector)]', object: 'border-[var(--object-input)]', + description: 'border-[var(--description-input, #8BB8E8)]', category: 'border-[var(--category-selector)]', privacy: 'border-[var(--privacy-selector)]', - complement: 'border-gray-400', }; const [currentStepIndex, setCurrentStepIndex] = useState(0); @@ -75,19 +75,19 @@ const StatementWizard: React.FC = ({ if (presetQuestion?.steps?.subject?.preset) { setSelection((prev) => ({ ...prev, - atoms: { ...prev.atoms, subject: username }, + atoms: { ...prev.atoms, subject: "I" }, // For preset questions, use the category from the preset category: presetQuestion.category || 'uncategorised', // Add the presetId to identify this as a preset question in the preview presetId: presetQuestion.id, })); } else { - // For custom statements, set default category to "uncategorised" + // For custom statements, set default category to "uncategorised" // but still show the category screen first setSelection((prev) => ({ ...prev, - // Set default subject to username - atoms: { ...prev.atoms, subject: username }, + // Set default subject to "I" instead of username + atoms: { ...prev.atoms, subject: "I" }, // Set default category to 'uncategorised' category: 'uncategorised', })); @@ -174,10 +174,10 @@ const StatementWizard: React.FC = ({ // For custom statements (not preset), category must be selected return isPreset || selection.category.trim().length > 0; case 'privacy': - // Always valid since it's a boolean toggle. + // Always valid since it's a boolean toggle return true; - case 'complement': - // Complement is optional; consider it valid. + case 'description': + // Description is optional; always valid return true; default: return false; @@ -247,6 +247,28 @@ const StatementWizard: React.FC = ({ }} /> ); + case 'description': + return ( + { + // If the same value is selected again, move to next step + if (val === selection.description) { + goNext(); + } else { + // Update the description value + setSelection((prev) => ({ + ...prev, + description: val, + })); + + // Don't automatically advance - user will use Next button + // This is consistent with other steps and allows users + // to review their description before proceeding + } + }} + /> + ); case 'category': return ( = ({ }} /> ); - case 'complement': - return ; + // No complement step anymore default: return null; } @@ -313,7 +334,7 @@ const StatementWizard: React.FC = ({ )} Wizard Steps Wizard Steps - + {/* Scrollable Content Area */} @@ -328,7 +349,7 @@ const StatementWizard: React.FC = ({ - + {/* Bottom Section - Always Visible */} {/* Navigation Panel */} diff --git a/src/features/wizard/components/StepContainer.tsx b/src/features/wizard/components/StepContainer.tsx index 8fd463b..a84224b 100644 --- a/src/features/wizard/components/StepContainer.tsx +++ b/src/features/wizard/components/StepContainer.tsx @@ -1,9 +1,10 @@ 'use client'; import React from 'react'; -import { Button } from '../../../components/ui/button'; +import { Button } from '../../../components/ui/Button'; import { ArrowLeft } from 'lucide-react'; +// Force Redeploy interface StepContainerProps { subQuestion: string; showBack?: boolean; @@ -39,12 +40,12 @@ const StepContainer: React.FC = ({ {currentStep}/{totalSteps} - {subQuestion} + + {subQuestion} + - - {children} - + {children} ); diff --git a/src/features/wizard/components/SubjectTiles.tsx b/src/features/wizard/components/SubjectTiles.tsx index 2ef8bc2..1a08aaa 100644 --- a/src/features/wizard/components/SubjectTiles.tsx +++ b/src/features/wizard/components/SubjectTiles.tsx @@ -1,5 +1,5 @@ import React, { useMemo } from 'react'; -import { Button } from '../../../components/ui/button'; +import { Button } from '../../../components/ui/Button'; import descriptorsData from '../../../data/descriptors.json'; import type { SetQuestion, DescriptorsData } from '../../../types/entries'; @@ -17,7 +17,7 @@ interface SubjectTilesProps { } const getSubjectTiles = ( - username: string, + // username: string, activePresetQuestion?: SetQuestion, selectedCategory?: string ): SubjectTile[] => { @@ -25,7 +25,8 @@ const getSubjectTiles = ( // Use the selected category if available, or the top-level category from the preset question // defaulting to 'wellbeing' if neither is available - const categoryKey = selectedCategory || activePresetQuestion?.category || 'wellbeing'; + const categoryKey = + selectedCategory || activePresetQuestion?.category || 'wellbeing'; const data = descriptorsData as DescriptorsData; const category = data.descriptors.find( (d) => d.name.toLowerCase() === categoryKey.toLowerCase() @@ -34,24 +35,24 @@ const getSubjectTiles = ( descriptorOptions = category.options; } return [ - { label: username, value: username }, + { label: 'I', value: 'I' }, ...descriptorOptions.map((option) => ({ - label: `${username}'s ${option}`, - value: `${username}'s ${option}`, + label: `My ${option}`, + value: `My ${option}`, })), ]; }; export const SubjectTiles: React.FC = ({ - username, + // username, activePresetQuestion, selectedCategory, selectedValue, onSelect, }) => { const tiles = useMemo( - () => getSubjectTiles(username, activePresetQuestion, selectedCategory), - [username, activePresetQuestion, selectedCategory] + () => getSubjectTiles(activePresetQuestion, selectedCategory), + [activePresetQuestion, selectedCategory] ); return ( diff --git a/src/features/wizard/components/VerbGrid.tsx b/src/features/wizard/components/VerbGrid.tsx index c49706b..4f8abef 100644 --- a/src/features/wizard/components/VerbGrid.tsx +++ b/src/features/wizard/components/VerbGrid.tsx @@ -1,10 +1,11 @@ 'use client'; import React from 'react'; -import { Button } from '@/components/ui/button'; +import { Button } from '@/components/ui/Button'; import type { Verb, Category } from '@/types/entries'; import { getVerbColor } from '@/lib/utils/categoryUtils'; import { getContrastColor } from '@/lib/utils/colorUtils'; +import { getVerbTextSizeClass } from '@/lib/utils/verbUtils'; interface VerbGridProps { verbs: Verb[]; @@ -21,18 +22,19 @@ const VerbGrid: React.FC = ({ }) => { const sortedVerbs = [...verbs].sort((a, b) => a.name.localeCompare(b.name)); return ( - + {sortedVerbs.map((verb) => { const tileColor = getVerbColor(verb, rootCategory); const isSelected = verb.id === selectedVerbId; - + const textSizeClass = getVerbTextSizeClass(verb.name); + return ( onVerbSelect(verb)} variant={'outlineVerbs'} selected={isSelected} - className='flex items-center justify-center rounded-lg shadow-md' + className={`flex items-center justify-center rounded-lg shadow-md px-1 py-1 ${textSizeClass}`} style={ { '--tile-color': tileColor, diff --git a/src/features/wizard/components/index.ts b/src/features/wizard/components/index.ts new file mode 100644 index 0000000..68d4534 --- /dev/null +++ b/src/features/wizard/components/index.ts @@ -0,0 +1,10 @@ +// Barrel file for wizard components +export { default as StatementWizard } from './StatementWizard'; +export { default as StatementPreview } from './StatementPreview'; +export { EditStatementModal } from './EditStatementModal'; +export { default as FilterBar } from './FilterBar'; +export { default as SentimentVerbPicker } from './SentimentVerbPicker'; +export { default as StepContainer } from './StepContainer'; +export { default as SubjectTiles } from './SubjectTiles'; +export { default as VerbGrid } from './VerbGrid'; +export { default as VerbSelector } from './VerbSelector'; \ No newline at end of file diff --git a/src/features/wizard/components/steps/CategoryStep.tsx b/src/features/wizard/components/steps/CategoryStep.tsx index ec08d88..3226699 100644 --- a/src/features/wizard/components/steps/CategoryStep.tsx +++ b/src/features/wizard/components/steps/CategoryStep.tsx @@ -1,7 +1,7 @@ // src/components/statementWizard/steps/CategoryStep.tsx import React from 'react'; import StepContainer from '../StepContainer'; -import { Button } from '@/components/ui/button'; +import { Button } from '@/components/ui/Button'; import statementsCategories from '@/data/statementsCategories.json'; interface CategoryStepProps { diff --git a/src/features/wizard/components/steps/ComplementStep.tsx b/src/features/wizard/components/steps/ComplementStep.tsx deleted file mode 100644 index 4c9573d..0000000 --- a/src/features/wizard/components/steps/ComplementStep.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import React from 'react'; -import StepContainer from '../StepContainer'; - -interface ComplementStepProps { - currentStep?: number; - totalSteps?: number; -} - -export const ComplementStep: React.FC = ({ - currentStep = 5, - totalSteps = 5, -}) => { - const subQuestion = 'Add additional statement if needed'; - return ( - - - - If you feel your statement didn't fully answer the question, you can - later create your own statements to complement it. - - - - ); -}; diff --git a/src/features/wizard/components/steps/DescriptionStep.tsx b/src/features/wizard/components/steps/DescriptionStep.tsx new file mode 100644 index 0000000..f654aaf --- /dev/null +++ b/src/features/wizard/components/steps/DescriptionStep.tsx @@ -0,0 +1,67 @@ +import React, { useState } from 'react'; +import StepContainer from '../StepContainer'; + +interface DescriptionStepProps { + description?: string; + onUpdate: (val: string) => void; + currentStep?: number; + totalSteps?: number; +} + +export const DescriptionStep: React.FC = ({ + description = '', + onUpdate, + currentStep = 4, // After subject, verb, object, before privacy + totalSteps = 6, +}) => { + const [inputValue, setInputValue] = useState(description); + const maxLength = 500; // Maximum character limit + + const handleChange = (e: React.ChangeEvent) => { + const newValue = e.target.value; + if (newValue.length <= maxLength) { + setInputValue(newValue); + // Update the parent component in real-time to reflect changes in the preview + onUpdate(newValue); + } + }; + + // Handle Shift+Enter to save and advance, regular Enter adds a new line + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === 'Enter' && e.shiftKey) { + e.preventDefault(); + onUpdate(inputValue); + } + // Regular Enter will naturally add a new line in the textarea + }; + + const subQuestion = 'Details (optional)'; + const charCount = inputValue.length; + const remainingChars = maxLength - charCount; + + return ( + + + + + + + {remainingChars} characters remaining + + + + + ); +}; diff --git a/src/features/wizard/components/steps/ObjectStep.tsx b/src/features/wizard/components/steps/ObjectStep.tsx index 4aff0cd..a80295c 100644 --- a/src/features/wizard/components/steps/ObjectStep.tsx +++ b/src/features/wizard/components/steps/ObjectStep.tsx @@ -1,9 +1,10 @@ // src/components/statementWizard/steps/ObjectStep.tsx import React from 'react'; import StepContainer from '../StepContainer'; -import { Input } from '../../../../components/ui/input'; +import { Input } from '../../../../components/ui/Input'; import { getVerbName } from '@/lib/utils/verbUtils'; +// Force Redeploy interface ObjectStepProps { subject: string; verb: string; diff --git a/src/features/wizard/components/steps/PrivacyStep.tsx b/src/features/wizard/components/steps/PrivacyStep.tsx index c120edf..dc578c3 100644 --- a/src/features/wizard/components/steps/PrivacyStep.tsx +++ b/src/features/wizard/components/steps/PrivacyStep.tsx @@ -1,6 +1,6 @@ import React from 'react'; import StepContainer from '../StepContainer'; -import { Button } from '@/components/ui/button'; +import { Button } from '@/components/ui/Button'; import { MailX, MailPlus } from 'lucide-react'; import { useEntries } from '@/features/statements/hooks/useEntries'; diff --git a/src/features/wizard/components/steps/SubjectStep.tsx b/src/features/wizard/components/steps/SubjectStep.tsx index 0fb8c3e..5a6a844 100644 --- a/src/features/wizard/components/steps/SubjectStep.tsx +++ b/src/features/wizard/components/steps/SubjectStep.tsx @@ -23,9 +23,16 @@ export const SubjectStep: React.FC = ({ totalSteps = 5, }) => { // Use a default subject question text - const subQuestion = `This statement applies to ${username} or someone/something else?`; + const subQuestion = `Who is this statement about?`; // Check whether descriptors are allowed (if using a preset) const allowDescriptors = presetQuestion?.steps?.subject?.allowDescriptors; + + // If preset answer is "username" but descriptors aren't allowed, update to "I" + React.useEffect(() => { + if (allowDescriptors === false && presetQuestion?.steps?.subject?.presetAnswer === "username" && selection === username) { + onUpdate("I"); + } + }, [allowDescriptors, presetQuestion, username, selection, onUpdate]); return ( = ({ {allowDescriptors === false ? ( <> - {username} + I > ) : ( diff --git a/src/features/wizard/components/steps/VerbStep.tsx b/src/features/wizard/components/steps/VerbStep.tsx index 3977676..135b9a3 100644 --- a/src/features/wizard/components/steps/VerbStep.tsx +++ b/src/features/wizard/components/steps/VerbStep.tsx @@ -18,7 +18,9 @@ export const VerbStep: React.FC = ({ currentStep = 3, // Updated since category and subject now come first totalSteps = 5, }) => { - const subQuestion = `What's happening with ${subject}? How do they feel or what do they experience?`; + const subQuestion = subject === "I" + ? `What's happening with me? How do I feel or what do I experience?` + : `What's happening with ${subject}? How do they feel or what do they experience?`; return ( char.toUpperCase()); -} +export { cn, formatCategoryName }; diff --git a/src/lib/utils/formatUtils.ts b/src/lib/utils/formatUtils.ts new file mode 100644 index 0000000..c255e46 --- /dev/null +++ b/src/lib/utils/formatUtils.ts @@ -0,0 +1,24 @@ +import { type ClassValue, clsx } from 'clsx'; +import { twMerge } from 'tailwind-merge'; + +/** + * Utility function for conditionally joining class names together using clsx and tailwind-merge. + * This allows for dynamic class composition that works well with Tailwind CSS. + * + * @param inputs Class values to merge (strings, objects, arrays, etc.) + * @returns Merged class string + */ +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)); +} + +/** + * Formats a category name by replacing underscores with spaces and capitalizing the first letter of each word. + * @param category - The category name to format. + * @returns The formatted category name. + */ +export function formatCategoryName(category: string): string { + return category + .replace(/_/g, ' ') + .replace(/\b\w/g, (char) => char.toUpperCase()); +} \ No newline at end of file diff --git a/src/lib/utils/ssr-hooks.ts b/src/lib/utils/ssrHooks.ts similarity index 100% rename from src/lib/utils/ssr-hooks.ts rename to src/lib/utils/ssrHooks.ts diff --git a/src/lib/utils/verbUtils.ts b/src/lib/utils/verbUtils.ts index b9fd801..a2f6637 100644 --- a/src/lib/utils/verbUtils.ts +++ b/src/lib/utils/verbUtils.ts @@ -1,4 +1,4 @@ -import { Verb } from '../../types/entries'; +import { Entry, Verb } from '../../types/entries'; import nlp from 'compromise'; import verbData from '../../data/verbs.json'; @@ -7,7 +7,7 @@ import verbData from '../../data/verbs.json'; * Converts to present tense and lowercase. * Uses the presentTenseForm property if available. */ -const getVerbName = (verbId: string): string => { +const getVerbName = (verbId: string, subjectIsI = false): string => { const found = (verbData.verbs as Verb[]).find((v) => v.id === verbId); if (found) { // If the verb has a specified presentTenseForm, use it @@ -15,6 +15,11 @@ const getVerbName = (verbId: string): string => { return found.presentTenseForm; } + if (subjectIsI) { + // If subject is "I", use the infinitive form + return found.name.toLowerCase(); + } + // Convert the verb to present tense and lowercase it. const processedVerb = nlp(found.name).verbs().toPresentTense().out('text').toLowerCase(); @@ -28,4 +33,46 @@ const getVerbName = (verbId: string): string => { return verbId; // Fallback: return the id if not found. }; -export { getVerbName }; +/** + * Formats a statement for email display, replacing "I" with username + * and ensuring proper verb conjugation. + * + * @param entry The statement entry to format + * @param username The user's real name to display instead of "I" + * @returns Formatted statement text for email display + */ +const getEmailFormattedStatement = (entry: Entry, username: string): string => { + // Get verb in present tense form (force 3rd person present tense) + const verbForm = getVerbName(entry.atoms.verb, false); + + // Replace "I" with username + let formattedSubject = entry.atoms.subject; + + if (formattedSubject === 'I') { + formattedSubject = username; + } + // If subject starts with "My", replace with "{username}'s" + else if (formattedSubject.startsWith('My ')) { + formattedSubject = `${username}'s ${formattedSubject.substring(3)}`; + } + + // Return the complete formatted statement + return `${formattedSubject} ${verbForm} ${entry.atoms.object}`; +}; + +/** + * Determines appropriate Tailwind CSS text size class based on the length of verb text. + * Used to ensure verb text fits properly in buttons without overflowing. + * + * @param verbText The verb text to evaluate + * @returns The appropriate Tailwind CSS class for text size + */ +const getVerbTextSizeClass = (verbText: string): string => { + if (verbText.length >= 11) { + return 'text-xs'; // Small for longer texts + } else { + return 'text-sm'; // Default size + } +}; + +export { getVerbName, getEmailFormattedStatement, getVerbTextSizeClass }; diff --git a/src/main.tsx b/src/main.tsx index 7134079..340a6e5 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,7 +1,7 @@ import { StrictMode } from 'react'; import { createRoot } from 'react-dom/client'; import './index.css'; -import App from './App'; // Now your App component is the top-level one +import App from './App'; createRoot(document.getElementById('root')!).render( diff --git a/src/types/entries.ts b/src/types/entries.ts index ca8ddea..1190c7a 100644 --- a/src/types/entries.ts +++ b/src/types/entries.ts @@ -2,6 +2,7 @@ export interface Atoms { subject: string; verb: string; object: string; + description?: string; adverbial?: string[]; } @@ -23,6 +24,7 @@ export interface Entry { input: string; // The full statement as a single string (headline) isPublic: boolean; atoms: Atoms; // Nested grammatical components + description?: string; // Optional detailed description/expansion of the statement actions?: Action[]; category: string; presetId?: string; @@ -72,6 +74,7 @@ export interface SetQuestion { subject: SetQuestionStep; verb: SetQuestionStep; object: SetQuestionStep; + description?: SetQuestionStep; category?: SetQuestionStep; privacy: SetQuestionStep; }; @@ -96,6 +99,6 @@ export type Step = | 'subject' | 'verb' | 'object' + | 'description' | 'category' - | 'privacy' - | 'complement'; + | 'privacy'; diff --git a/tsconfig.app.json b/tsconfig.app.json index cf567a6..ec1467a 100644 --- a/tsconfig.app.json +++ b/tsconfig.app.json @@ -28,7 +28,9 @@ "noUnusedLocals": true, "noUnusedParameters": true, "noFallthroughCasesInSwitch": true, - "noUncheckedSideEffectImports": true + "noUncheckedSideEffectImports": true, + /* Case sensitivity */ + "forceConsistentCasingInFileNames": false }, "include": [ "src"
- {entry.atoms.subject} {getVerbName(entry.atoms.verb)}{' '} - {entry.atoms.object} + {getEmailFormattedStatement(entry, data.username || 'User')}
+
{children}
+ {resource.description} +
+ {selection.description ? selection.description : 'Description (optional)'} +
- If you feel your statement didn't fully answer the question, you can - later create your own statements to complement it. -
{username}
I