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 */}

- {managerName + {/* {managerName ? `Beacons for ${managerName}` - : `${username}'s Beacons`} + : `${username}'s Beacons`} */} + My Dashboard

diff --git a/src/components/layout/index.ts b/src/components/layout/index.ts new file mode 100644 index 0000000..8cb505e --- /dev/null +++ b/src/components/layout/index.ts @@ -0,0 +1,10 @@ +/** + * Barrel file for layout components + * + * Provides unified access to structural page components + * including header, footer, and main content containers. + */ + +export { default as Header } from './Header'; +export { default as Footer } from './Footer'; +export { default as MainPage } from './MainPage'; \ No newline at end of file diff --git a/src/components/modals/GratitudeModal.tsx b/src/components/modals/GratitudeModal.tsx index bc7c620..c1c68e8 100644 --- a/src/components/modals/GratitudeModal.tsx +++ b/src/components/modals/GratitudeModal.tsx @@ -5,10 +5,10 @@ import { SimpleDialog as Dialog, SimpleDialogContent as DialogContent, SimpleDialogDescription as DialogDescription, -} from '../ui/simple-dialog'; -import { Button } from '../ui/button'; +} from '../ui/Dialog'; +import { Button } from '../ui/Button'; import { Loader2, Heart } from 'lucide-react'; -import { sendGratitude } from '../../features/email/api/gratitudeApi'; +import { sendGratitude } from '../../features/email/api/emailGratitudeApi'; import { useEntries } from '../../features/statements/hooks/useEntries'; import { Action } from '../../types/entries'; @@ -16,6 +16,10 @@ interface GratitudeModalProps { onClose: () => void; statementId: string; action: Action; + statement?: { + input: string; + description?: string; + }; onGratitudeSent: ( statementId: string, actionId: string, @@ -27,6 +31,7 @@ const GratitudeModal: React.FC = ({ onClose, statementId, action, + statement, onGratitudeSent, }) => { const { data } = useEntries(); @@ -115,8 +120,8 @@ const GratitudeModal: React.FC = ({ > {/* Heart decoration - smaller on mobile */}
- - + +
@@ -140,11 +145,32 @@ const GratitudeModal: React.FC = ({ )} + {/* If statement is provided, show it */} + {statement && ( +
+

+ Statement +

+

+ {statement.input} +

+ + {/* Display description if available */} + {statement.description && statement.description.trim() !== '' && ( +
+ {statement.description} +
+ )} +
+ )} +

Action

-

{action.action}

+

+ {action.action} +

{action.byDate && (

Due by: {action.byDate} @@ -191,8 +217,8 @@ const GratitudeModal: React.FC = ({ )} - - ); +const SimpleDialogClose: React.FC<{ + children: React.ReactNode; + className?: string; + asChild?: boolean; +}> = ({ children, className }) => { + return ; }; -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 */} -
+
+
)} + {/* Resources Tab */} + {activeTab === 'resources' && ( +
+

+ Helpful Resources +

+ +
+ {helpContent.resources.map((resource, index) => ( + + ))} +
+
+ )} + {/* 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 */} @@ -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 (