diff --git a/src/app/@gifModal/(.)i/foundmedia/search/page.tsx b/src/app/@gifModal/(.)i/foundmedia/search/page.tsx index 77f10f4e..c451e38f 100644 --- a/src/app/@gifModal/(.)i/foundmedia/search/page.tsx +++ b/src/app/@gifModal/(.)i/foundmedia/search/page.tsx @@ -1,10 +1,6 @@ // 'use client'; import Gif from '@/features/media/components/Gif'; -import GifModal from '@/features/media/components/GifModal'; -// import Gif from '@/features/media/components/Gif'; export default async function Page() { - // export default function Page() { - // return ; return ; } diff --git a/src/app/[username]/ProfileProvider.tsx b/src/app/[username]/ProfileProvider.tsx index b0f3e817..21d8fd4d 100644 --- a/src/app/[username]/ProfileProvider.tsx +++ b/src/app/[username]/ProfileProvider.tsx @@ -1,8 +1,12 @@ 'use client'; -import React, { createContext, useContext, ReactNode } from 'react'; -import { use } from 'react'; -import { useProfileByUsername } from '@/features/profile/hooks'; -import { useMyProfile } from '@/features/profile/hooks'; +import React, { + createContext, + useContext, + ReactNode, + use, + useMemo, +} from 'react'; +import { useProfileByUsername, useMyProfile } from '@/features/profile/hooks'; import { useAuthStore } from '@/features/authentication/store/authStore'; import { UserProfile } from '@/features/profile/types/api'; import Loader from '@/components/generic/Loader'; @@ -25,14 +29,14 @@ export const useProfileContext = () => { }; interface ProfileProviderProps { - children: ReactNode; - params: Promise<{ username: string }>; + readonly children: ReactNode; + readonly params: Promise<{ username: string }>; } export function ProfileProvider({ children, params }: ProfileProviderProps) { const { username } = use(params); const currentUser = useAuthStore((s) => s.user); - const useMy = Boolean(currentUser && currentUser.username === username); + const useMy = Boolean(currentUser?.username === username); const myProfileQuery = useMyProfile(); const { @@ -49,6 +53,11 @@ export function ProfileProvider({ children, params }: ProfileProviderProps) { const error = useMy ? myProfileQuery.error : errorByUsername; + const contextValue = useMemo( + () => ({ profile, isLoading, error, username }), + [profile, isLoading, error, username] + ); + if (isLoading) { return (
@@ -71,7 +80,7 @@ export function ProfileProvider({ children, params }: ProfileProviderProps) { } return ( - + {children} ); diff --git a/src/app/[username]/followers-you-know/page.tsx b/src/app/[username]/followers-you-know/page.tsx index cc328ffd..4f342210 100644 --- a/src/app/[username]/followers-you-know/page.tsx +++ b/src/app/[username]/followers-you-know/page.tsx @@ -1,6 +1,5 @@ 'use client'; -import React, { useEffect } from 'react'; -import { use } from 'react'; +import React, { useEffect, use } from 'react'; import { useRouter } from 'next/navigation'; import Breadcrumb from '@/components/ui/Breadcrumb'; import { Tabs, GenericUserList } from '@/components/generic'; @@ -10,7 +9,7 @@ import { useAuthStore } from '@/features/authentication/store/authStore'; import Loader from '@/components/generic/Loader'; interface FollowersYouKnowPageProps { - params: Promise<{ + readonly params: Promise<{ username: string; }>; } diff --git a/src/app/[username]/followers/page.tsx b/src/app/[username]/followers/page.tsx index 6366ee0d..f8d5faaf 100644 --- a/src/app/[username]/followers/page.tsx +++ b/src/app/[username]/followers/page.tsx @@ -1,6 +1,5 @@ 'use client'; -import React from 'react'; -import { use } from 'react'; +import React, { use } from 'react'; import { useRouter } from 'next/navigation'; import Breadcrumb from '@/components/ui/Breadcrumb'; import { Tabs, GenericUserList } from '@/components/generic'; @@ -10,7 +9,7 @@ import { useAuthStore } from '@/features/authentication/store/authStore'; import Loader from '@/components/generic/Loader'; interface FollowersPageProps { - params: Promise<{ + readonly params: Promise<{ username: string; }>; } diff --git a/src/app/[username]/following/page.tsx b/src/app/[username]/following/page.tsx index 5c5a2f21..5c549b2d 100644 --- a/src/app/[username]/following/page.tsx +++ b/src/app/[username]/following/page.tsx @@ -1,6 +1,5 @@ 'use client'; -import React from 'react'; -import { use } from 'react'; +import React, { use } from 'react'; import { useRouter } from 'next/navigation'; import Breadcrumb from '@/components/ui/Breadcrumb'; import { Tabs, GenericUserList } from '@/components/generic'; @@ -10,7 +9,7 @@ import { useAuthStore } from '@/features/authentication/store/authStore'; import Loader from '@/components/generic/Loader'; interface FollowingPageProps { - params: Promise<{ + readonly params: Promise<{ username: string; }>; } diff --git a/src/app/[username]/layout.tsx b/src/app/[username]/layout.tsx index e318b043..c7fa4b97 100644 --- a/src/app/[username]/layout.tsx +++ b/src/app/[username]/layout.tsx @@ -2,8 +2,8 @@ import LayoutWrapper from '@/features/layout/components/LayoutWrapper'; import { ProfileProvider } from './ProfileProvider'; interface UsernameLayoutProps { - children: React.ReactNode; - params: Promise<{ username: string }>; + readonly children: React.ReactNode; + readonly params: Promise<{ username: string }>; } export default function UsernameLayout({ diff --git a/src/app/[username]/page.tsx b/src/app/[username]/page.tsx index 1ce0b55b..6dbcbbb5 100644 --- a/src/app/[username]/page.tsx +++ b/src/app/[username]/page.tsx @@ -15,10 +15,9 @@ const UserPage = () => { const { profile, username } = useProfileContext(); const { setCurrentProfile } = useProfileStore(); const currentUser = useAuthStore((s) => s.user); - const isMine = Boolean(currentUser && currentUser.username === username); + const isMine = Boolean(currentUser?.username === username); const [showBlockedPosts, setShowBlockedPosts] = useState(false); const router = useRouter(); - // Update page title with unread count (uses "H" branding, static favicon) usePageTitleNotifications('H', false); useEffect(() => { @@ -29,7 +28,7 @@ const UserPage = () => { }, [profile, setCurrentProfile]); const handleBack = () => { - window.history.back(); + router.push('/home'); }; if (!profile) { diff --git a/src/app/auth-debug/page.tsx b/src/app/auth-debug/page.tsx deleted file mode 100644 index c5a85c07..00000000 --- a/src/app/auth-debug/page.tsx +++ /dev/null @@ -1,185 +0,0 @@ -'use client'; -import { useEffect, useState } from 'react'; -import { useAuthStore } from '@/features/authentication/store/authStore'; - -export default function AuthDebugPage() { - const user = useAuthStore((s) => s.user); - const isAuthenticated = useAuthStore((s) => s.isAuthenticated); - const [cookieInfo, setCookieInfo] = useState(''); - - useEffect(() => { - // Check cookies - const cookies = document.cookie; - setCookieInfo(cookies || 'No cookies found'); - }, []); - - const testBackend = async () => { - try { - const response = await fetch( - 'https://api.hankers.myaddr.tools/api/v1.0/auth/me', - { - credentials: 'include', - } - ); - const data = await response.json(); - console.log('Backend /me response:', data); - alert(JSON.stringify(data, null, 2)); - } catch (error) { - console.error('Error calling /me:', error); - alert('Error: ' + error); - } - }; - - return ( -
-
-

Authentication Debug Page

- -
- {/* Frontend Auth State */} -
-

- Frontend Auth State (Zustand) -

-
-
- Is Authenticated: - - {isAuthenticated ? '✅ Yes' : '❌ No'} - -
-
- User ID: - - {(user as any)?.id || 'null'} - -
-
- Username: - - {(user as any)?.username || 'null'} - -
-
- Email: - - {(user as any)?.email || 'null'} - -
-
-
- - Full User Object - -
-                {JSON.stringify(user, null, 2)}
-              
-
-
- - {/* Browser Cookies */} -
-

Browser Cookies

-
-
- Has access_token: - - {cookieInfo.includes('access_token') ? '✅ Yes' : '❌ No'} - -
-
- - View All Cookies - -
-                  {cookieInfo}
-                
-
-
-
- - {/* Backend Test */} -
-

- Backend Authentication Test -

-

- Click the button below to check if the backend recognizes you as - authenticated. -

- -

- This will call GET /api/v1.0/auth/me and show the response -

-
- - {/* Instructions */} -
-

🔍 Debugging Steps

-
    -
  1. Check if "Is Authenticated" shows ✅ Yes
  2. -
  3. Check if "User ID" is a number (not null)
  4. -
  5. Check if "access_token" cookie exists
  6. -
  7. - Click "Test Backend /auth/me" to verify backend - recognizes you -
  8. -
  9. If backend returns 401, you need to login again
  10. -
-
- - {/* Common Issues */} -
-

❌ Common Issues

-
-
-
- Issue: User ID is null -
-
- → Backend is not returning user ID in the response. Check - backend /auth/login response. -
-
-
-
- Issue: Multiple users logging in -
-
- → Backend allows only one session per user. Second login kicks - out first session. -
→ Solution: Use different browsers or contact backend - team to allow multiple sessions. -
-
-
-
- Issue: No access_token cookie -
-
- → You're not logged in. Go to login page and - authenticate. -
-
-
-
-
-
-
- ); -} diff --git a/src/app/auth-demo/page.tsx b/src/app/auth-demo/page.tsx deleted file mode 100644 index b8a54f04..00000000 --- a/src/app/auth-demo/page.tsx +++ /dev/null @@ -1,223 +0,0 @@ -'use client'; - -import { useAuth } from '@/features/authentication/hooks'; -import { useAuthHandlers } from '@/features/authentication/hooks'; -import { useAuthModals } from '@/features/authentication/hooks'; -import { CheckIcon, CloseXIcon } from '@/components/ui/icons'; -import { useSearchParams } from 'next/navigation'; -import { useEffect, useState, Suspense } from 'react'; -import XLoader from '@/components/ui/XLoader'; - -function AuthDemoContent() { - const searchParams = useSearchParams(); - const [showSuccessMessage, setShowSuccessMessage] = useState(false); - const auth = useAuth(); - const { formState, clearFormState } = useAuthHandlers(); - const { openModal } = useAuthModals(); - - // Safe getters to avoid TS errors when auth.user can be a loose Record - const getField = (key: string) => { - const u = auth.user as Record | null; - if (!u) return 'N/A'; - const v = u[key]; - return typeof v === 'string' && v.length > 0 ? v : 'N/A'; - }; - - const getDateField = (key: string) => { - const u = auth.user as Record | null; - if (!u) return 'N/A'; - const v = u[key]; - if (typeof v === 'string' || typeof v === 'number') { - const d = new Date(v as string | number); - if (isNaN(d.getTime())) return 'N/A'; - return d.toLocaleDateString(); - } - return 'N/A'; - }; - - const userDisplay = (() => { - const name = getField('name'); - if (name !== 'N/A') return name; - const email = getField('email'); - if (email !== 'N/A') return email; - const username = getField('username'); - if (username !== 'N/A') return username; - return ''; - })(); - - useEffect(() => { - if (!searchParams) return; - - const loginSuccess = searchParams.get('login'); - const registerSuccess = searchParams.get('register'); - - if (loginSuccess === 'success') { - setShowSuccessMessage(true); - // Hide success message after 5 seconds - setTimeout(() => { - setShowSuccessMessage(false); - }, 5000); - } else if (registerSuccess === 'success') { - setShowSuccessMessage(true); - // Hide success message after 5 seconds - setTimeout(() => { - setShowSuccessMessage(false); - }, 5000); - } - }, [searchParams]); - - return ( -
-
-

Authentication Demo

- - {/* Success Message */} - {showSuccessMessage && ( -
-
-
- -
-
- {/*

- {searchParams?.get('login') === 'success' - ? `🎉 Login successful! Welcome back, ${auth.user?.name || auth.user?.email}!` - : searchParams?.get('register') === 'success' - ? `🎉 Registration successful! Welcome, ${auth.user?.name || auth.user?.email}!` - : '🎉 Success!'} -

*/} -
-
- -
-
-
- )} - - {/* Authentication Status */} -
-

Authentication Status

- {auth.isAuthenticated ? ( -
-

✅ Authenticated

-
-

- Name: {userDisplay || 'N/A'} -

-

- Username: {getField('username')} -

-

- Email: {getField('email')} -

-

- Role: {getField('role')} -

-

- Birth Date: {getDateField('birthDate')} -

-

- Location: {getField('location')} -

-

- Created At: {getDateField('createdAt')} -

-
- -
- ) : ( -
-

❌ Not authenticated

-
- - -
-
- )} -
- - {/* Form State */} -
-

Form State

-
-

- Loading: {formState.isLoading ? 'Yes' : 'No'} -

-

- Errors:{' '} - {Object.keys(formState.errors).length > 0 - ? JSON.stringify(formState.errors) - : 'None'} -

-

- Success: {formState.success ? 'Yes' : 'No'} -

-
- {Object.keys(formState.errors).length > 0 && ( - - )} -
- - {/* API Test */} -
-

API Integration

-
-
-

Backend Endpoints:

-
    -
  • POST /api/v1.0/auth/register - Register new user
  • -
  • POST /api/v1.0/auth/login - Login with email/password
  • -
  • GET /api/v1.0/auth/test - Test authentication
  • -
-
-
-

Features:

-
    -
  • ✅ React Query for API state management
  • -
  • ✅ Zustand for client state management
  • -
  • ✅ HTTPOnly cookie authentication
  • -
  • ✅ TypeScript types from OpenAPI spec
  • -
  • ✅ Error handling and loading states
  • -
  • ✅ Persistent authentication state
  • -
-
-
-
-
-
- ); -} - -export default function AuthDemoPage() { - return ( - }> - - - ); -} diff --git a/src/app/cookie-check/page.tsx b/src/app/cookie-check/page.tsx deleted file mode 100644 index 67dc5533..00000000 --- a/src/app/cookie-check/page.tsx +++ /dev/null @@ -1,71 +0,0 @@ -'use client'; - -import { useEffect, useState } from 'react'; -import Link from 'next/link'; - -export default function CookieCheckPage() { - const [cookies, setCookies] = useState(''); - const [accessToken, setAccessToken] = useState(null); - - useEffect(() => { - // Get all cookies - const allCookies = document.cookie; - setCookies(allCookies || 'No cookies found'); - - // Get access_token specifically - const getCookie = (name: string): string | undefined => { - const value = `; ${document.cookie}`; - const parts = value.split(`; ${name}=`); - if (parts.length === 2) return parts.pop()?.split(';').shift(); - }; - - const token = getCookie('access_token'); - setAccessToken(token || null); - }, []); - - return ( -
-
-

🍪 Cookie Check

- -
-
-

Access Token

- {accessToken ? ( -
-

✅ Found!

-
- {accessToken} -
-
- ) : ( -

❌ Not found - You need to login!

- )} -
- -
-

All Cookies

-
- {cookies} -
-
-
- -
-

💡 Instructions:

-
    -
  1. Open this page in BOTH browsers you want to test
  2. -
  3. - If access_token is missing, go to{' '} - - Login Page - -
  4. -
  5. After login, refresh this page to see the token
  6. -
  7. Once BOTH browsers show the token, you can test messaging
  8. -
-
-
-
- ); -} diff --git a/src/app/demo/auth-forms/page.tsx b/src/app/demo/auth-forms/page.tsx deleted file mode 100644 index 636b8133..00000000 --- a/src/app/demo/auth-forms/page.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import { GenericAuthFormDemo } from '../components/GenericAuthFormDemo'; - -export default function GenericAuthFormDemoPage() { - return ; -} diff --git a/src/app/demo/buttons/auth/page.tsx b/src/app/demo/buttons/auth/page.tsx deleted file mode 100644 index d12b30e5..00000000 --- a/src/app/demo/buttons/auth/page.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import { AuthButtonsDemo } from '../../components/AuthButtonsDemo'; - -export default function AuthButtonDemoPage() { - return ; -} diff --git a/src/app/demo/buttons/general/page.tsx b/src/app/demo/buttons/general/page.tsx deleted file mode 100644 index 43653ede..00000000 --- a/src/app/demo/buttons/general/page.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import { ButtonsDemo } from '../../components/ButtonsDemo'; - -export default function GeneralButtonsPage() { - return ; -} diff --git a/src/app/demo/buttons/page.tsx b/src/app/demo/buttons/page.tsx deleted file mode 100644 index 5abac6d0..00000000 --- a/src/app/demo/buttons/page.tsx +++ /dev/null @@ -1,107 +0,0 @@ -import Link from 'next/link'; -import { XLogo } from '@/components/ui/icons'; - -export default function ButtonsIndexPage() { - return ( -
-
-
-
- -
-

- Button Components -

-

- Explore our button components including general-purpose buttons and - authentication buttons. -

-
- -
-
- {/* General Buttons Card */} - -
-
- - - -
-

- General Buttons -

-

- Primary, secondary, outline, and ghost button variants with - different sizes and states. -

-
- - View Examples → - -
-
- - - {/* Auth Buttons Card */} - -
-
- - - -
-

- Auth Buttons -

-

- Social login buttons for Google, Apple, Facebook, GitHub, and - more authentication providers. -

-
- - View Examples → - -
-
- -
-
- - {/* Navigation */} -
-
- - ← Back to Demos - - - Input Components → - -
-
-
-
- ); -} diff --git a/src/app/demo/components/AuthButtonsDemo.tsx b/src/app/demo/components/AuthButtonsDemo.tsx deleted file mode 100644 index 1a3f721f..00000000 --- a/src/app/demo/components/AuthButtonsDemo.tsx +++ /dev/null @@ -1,386 +0,0 @@ -'use client'; - -import React, { useState } from 'react'; -import Link from 'next/link'; -import { AuthButton } from '@/components/ui/AuthButton'; -import { Divider } from '@/components/ui/Divider'; -import { - GoogleIcon, - AppleIcon, - GrokIcon, - FacebookIcon, - GitHubIcon, - XLogo, -} from '@/components/ui/icons'; - -export function AuthButtonsDemo() { - const [loading, setLoading] = useState(null); - - const handleButtonClick = (buttonName: string) => { - setLoading(buttonName); - setTimeout(() => setLoading(null), 2000); - }; - - return ( -
-
-
-
- -
-

- Button Components Demo -

-

- Explore all button variants with different states, sizes, and use - cases. All buttons follow X/Twitter design patterns and are fully - interactive. -

-
- - {/* Social Login Buttons Section */} -
-

- Social Login Buttons -

-
- } - className="w-full" - onClick={() => handleButtonClick('google')} - loading={loading === 'google'} - > - Sign in with Google - - - } - className="w-full" - onClick={() => handleButtonClick('apple')} - loading={loading === 'apple'} - > - Sign in with Apple - - - } - className="w-full" - onClick={() => handleButtonClick('grok')} - loading={loading === 'grok'} - > - Sign in with Grok - - - } - className="w-full" - onClick={() => handleButtonClick('facebook')} - loading={loading === 'facebook'} - > - Sign in with Facebook - - - } - className="w-full" - onClick={() => handleButtonClick('github')} - loading={loading === 'github'} - > - Sign in with GitHub - -
-
- - - - {/* Primary Buttons Section */} -
-

- Primary Buttons -

-
- handleButtonClick('primary-lg')} - loading={loading === 'primary-lg'} - > - Create Account - - - handleButtonClick('primary-md')} - loading={loading === 'primary-md'} - > - Sign In - - - handleButtonClick('primary-sm')} - loading={loading === 'primary-sm'} - > - Continue - -
-
- - - - {/* Secondary Buttons Section */} -
-

- Secondary Buttons -

-
- handleButtonClick('secondary-lg')} - loading={loading === 'secondary-lg'} - > - Learn More - - - handleButtonClick('secondary-md')} - loading={loading === 'secondary-md'} - > - Get Started - - - handleButtonClick('secondary-sm')} - loading={loading === 'secondary-sm'} - > - Explore - -
-
- - - - {/* Outline Buttons Section */} -
-

- Outline Buttons -

-
- handleButtonClick('outline-lg')} - loading={loading === 'outline-lg'} - > - View Profile - - - handleButtonClick('outline-md')} - loading={loading === 'outline-md'} - > - Edit Settings - - - handleButtonClick('outline-sm')} - loading={loading === 'outline-sm'} - > - Share - -
-
- - - - {/* Ghost Buttons Section */} -
-

- Ghost Buttons -

-
- handleButtonClick('ghost-lg')} - loading={loading === 'ghost-lg'} - > - Follow - - - handleButtonClick('ghost-md')} - loading={loading === 'ghost-md'} - > - Message - - - handleButtonClick('ghost-sm')} - loading={loading === 'ghost-sm'} - > - Like - -
-
- - - - {/* Button States Section */} -
-

- Button States -

-
- - Disabled Button - - - - Loading Button - - - } - onClick={() => handleButtonClick('with-icon')} - loading={loading === 'with-icon'} - > - Button with Icon - -
-
- - {/* Usage Examples */} -
-

- Usage Examples -

-
-
-
-

Form Actions

-
- handleButtonClick('submit')} - loading={loading === 'submit'} - > - Submit Form - - handleButtonClick('cancel')} - loading={loading === 'cancel'} - > - Cancel - -
-
- -
-

- Social Actions -

-
- handleButtonClick('follow')} - loading={loading === 'follow'} - > - Follow @username - - handleButtonClick('message')} - loading={loading === 'message'} - > - Send Message - -
-
-
-
-
- - {/* Navigation */} -
-
- - ← Back to Demos - - - ← Buttons Home - - - ← General Buttons - -
-
-
-
- ); -} diff --git a/src/app/demo/components/ButtonsDemo.tsx b/src/app/demo/components/ButtonsDemo.tsx deleted file mode 100644 index afa4f50c..00000000 --- a/src/app/demo/components/ButtonsDemo.tsx +++ /dev/null @@ -1,307 +0,0 @@ -'use client'; - -import React, { useState } from 'react'; -import Link from 'next/link'; -import Button from '@/components/ui/Button'; -import { Divider } from '@/components/ui/Divider'; -import { XLogo } from '@/components/ui/icons'; - -export function ButtonsDemo() { - const [loading, setLoading] = useState(null); - - const handleButtonClick = (buttonName: string) => { - setLoading(buttonName); - setTimeout(() => setLoading(null), 2000); - }; - - return ( -
-
-
-
- -
-

- General Button Components -

-

- Explore all button variants including primary, secondary, outline, - and ghost buttons with different states and sizes. -

-
- - {/* Primary Buttons Section */} -
-

- Primary Buttons -

-
- - - - - -
-
- - - - {/* Secondary Buttons Section */} -
-

- Secondary Buttons -

-
- - - - - -
-
- - - - {/* Outline Buttons Section */} -
-

- Outline Buttons -

-
- - - - - -
-
- - - - {/* Ghost Buttons Section */} -
-

- Ghost Buttons -

-
- - - - - -
-
- - - - {/* Button States Section */} -
-

- Button States -

-
- - - - - -
-
- - - - {/* Usage Examples */} -
-

- Usage Examples -

-
-
-
-

Form Actions

-
- - -
-
- -
-

- Multiple Buttons -

-
- - - -
-
-
-
-
- - {/* Navigation */} -
-
- - ← Back to Demos - - - ← Buttons Home - - - Auth Buttons → - -
-
-
-
- ); -} diff --git a/src/app/demo/components/GenericAuthFormDemo.tsx b/src/app/demo/components/GenericAuthFormDemo.tsx deleted file mode 100644 index 863d648f..00000000 --- a/src/app/demo/components/GenericAuthFormDemo.tsx +++ /dev/null @@ -1,835 +0,0 @@ -'use client'; - -import React, { useState } from 'react'; -import Link from 'next/link'; -import { FormContainer, authFormConfigs } from '@/components/ui/forms'; -import { AuthButton } from '@/components/ui/AuthButton'; -import { Divider } from '@/components/ui/Divider'; -import { - XLogo, - LoginIcon, - UserIcon, - KeyIcon, - MailIcon, - ChatIcon, - ListIcon, -} from '@/components/ui/icons'; - -type FormType = - | 'login' - | 'register' - | 'forgotPassword' - | 'contact' - | 'newsletter' - | 'feedback' - | 'survey' - | 'support' - | 'newsletter-signup' - | 'user-profile' - | null; - -export function GenericAuthFormDemo() { - const [activeForm, setActiveForm] = useState(null); - const [submittedData, setSubmittedData] = useState | null>(null); - const [formState, setFormState] = useState({ - isLoading: false, - errors: {}, - success: false, - }); - - const handleSubmit = (data: Record) => { - console.log('Form submitted:', data); - setFormState({ isLoading: true, errors: {}, success: false }); - - // Simulate API call - setTimeout(() => { - setFormState({ isLoading: false, errors: {}, success: true }); - setSubmittedData(data); - - setTimeout(() => { - setSubmittedData(null); - setActiveForm(null); - setFormState({ isLoading: false, errors: {}, success: false }); - }, 2000); - }, 1000); - }; - - const handleSocialAuth = (providerId: string) => { - console.log('Social login:', providerId); - alert(`Social login with: ${providerId}`); - setActiveForm(null); - }; - - const handleForgotPassword = () => { - console.log('Forgot password clicked'); - setActiveForm('forgotPassword'); - }; - - const closeModal = () => { - setActiveForm(null); - setFormState({ isLoading: false, errors: {}, success: false }); - }; - - const clearFormState = () => { - setFormState({ isLoading: false, errors: {}, success: false }); - }; - - return ( -
-
-
-
- -
-

- Generic Forms Demo -

-

- Explore our comprehensive collection of responsive forms built with - generic components. All forms automatically adapt to screen size - with modal/full-page modes and follow X/Twitter design patterns. -

-
- - {/* Success Message */} - {submittedData && ( -
-

Form Submitted Successfully!

-
-              {JSON.stringify(submittedData, null, 2)}
-            
-
- )} - - {/* Authentication Forms Section */} -
-

- Authentication Forms -

-
- {/* Login Form */} -
-
- -
-

- Sign In -

-

- Login with email and password or social providers -

- setActiveForm('login')} - > - Try Login Form - -
- - {/* Register Form */} -
-
- -
-

- Sign Up -

-

- Create account with email, password, and date of birth -

- setActiveForm('register')} - > - Try Register Form - -
- - {/* Forgot Password Form */} -
-
- -
-

- Reset Password -

-

- Recover your account -

- setActiveForm('forgotPassword')} - > - Try Reset Form - -
-
-
- - - - {/* Generic Forms Section */} -
-

- Generic Forms -

-
- {/* Contact Form */} -
-
- -
-

- Contact -

-

- Get in touch with us -

- setActiveForm('contact')} - > - Try Contact Form - -
- - {/* Newsletter Form */} -
-
- -
-

- Newsletter -

-

- Subscribe to updates -

- setActiveForm('newsletter')} - > - Try Newsletter Form - -
- - {/* Feedback Form */} -
-
- -
-

- Feedback -

-

- Share your thoughts -

- setActiveForm('feedback')} - > - Try Feedback Form - -
- - {/* Survey Form */} -
-
- -
-

Survey

-

Help us improve

- setActiveForm('survey')} - > - Try Survey Form - -
-
-
- - - - {/* Advanced Forms Section */} -
-

- Advanced Forms -

-
- {/* Support Form */} -
-
- -
-

- Support -

-

- Technical support request -

- setActiveForm('support')} - > - Try Support Form - -
- - {/* Newsletter Signup */} -
-
- -
-

- Newsletter Signup -

-

- Simple email subscription -

- setActiveForm('newsletter-signup')} - > - Try Signup Form - -
- - {/* User Profile */} -
-
- -
-

- User Profile -

-

- Update profile information -

- setActiveForm('user-profile')} - > - Try Profile Form - -
-
-
- - {/* Form Features Section */} -
-

- Form Features -

-
-
-
-
- - - -
-

- Responsive Design -

-

- Forms automatically adapt to screen size with modal/full-page - modes -

-
- -
-
- - - -
-

- Form Validation -

-

- Built-in validation with real-time error feedback and field - validation -

-
- -
-
- - - -
-

- Social Login -

-

- Integrated social login buttons with customizable providers -

-
-
-
-
- - {/* Navigation */} -
-
- - ← Back to Demos - - - ← Button Components - - - Input Components → - -
-
- - {/* Form Modals */} - {activeForm === 'login' && ( - - )} - - {activeForm === 'register' && ( - - )} - - {activeForm === 'forgotPassword' && ( - - )} - - {activeForm === 'contact' && ( - - )} - - {activeForm === 'newsletter' && ( - - )} - - {activeForm === 'feedback' && ( - - )} - - {activeForm === 'survey' && ( - - )} - - {activeForm === 'support' && ( - - )} - - {activeForm === 'newsletter-signup' && ( - - )} - - {activeForm === 'user-profile' && ( - - )} -
-
- ); -} diff --git a/src/app/demo/components/InputFieldsDemo.tsx b/src/app/demo/components/InputFieldsDemo.tsx deleted file mode 100644 index bbbe4a0d..00000000 --- a/src/app/demo/components/InputFieldsDemo.tsx +++ /dev/null @@ -1,503 +0,0 @@ -'use client'; - -import React, { useState } from 'react'; -import Link from 'next/link'; -import { InputField, SearchInput } from '@/components/ui/input'; -import { SelectField } from '@/components/ui/SelectField'; -import { Divider } from '@/components/ui/Divider'; -import { XLogo, EyeIcon } from '@/components/ui/icons'; - -export function InputFieldsDemo() { - const [formData, setFormData] = useState({ - basic: '', - email: '', - password: '', - withIcon: '', - withCounter: '', - withError: '', - disabled: 'This field is disabled', - required: '', - withPlaceholder: '', - maxLengthField: '', - number: '', - tel: '', - url: '', - }); - - const [searchQuery, setSearchQuery] = useState(''); - - const [selectData, setSelectData] = useState({ - basic: '', - withError: '', - required: '', - disabled: 'option2', - }); - - const [errors, setErrors] = useState({ - withError: 'This field has an error message', - required: '', - email: '', - selectWithError: 'Please select a valid option', - }); - - const handleInputChange = - (field: string) => - ( - e: React.ChangeEvent< - HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement - > - ) => { - setFormData((prev) => ({ - ...prev, - [field]: e.target.value, - })); - - // Clear error when user starts typing - if (errors[field as keyof typeof errors]) { - setErrors((prev) => ({ - ...prev, - [field]: '', - })); - } - }; - - const handleSelectChange = - (field: string) => (e: React.ChangeEvent) => { - setSelectData((prev) => ({ - ...prev, - [field]: e.target.value, - })); - - // Clear error when user selects - if (errors[`select${field}` as keyof typeof errors]) { - setErrors((prev) => ({ - ...prev, - [`select${field}`]: '', - })); - } - }; - - const handleBlur = (field: string) => () => { - // Simple validation examples - if (field === 'email' && formData.email && !formData.email.includes('@')) { - setErrors((prev) => ({ - ...prev, - email: 'Please enter a valid email', - })); - } - }; - - const selectOptions = [ - { value: '', label: 'Select an option' }, - { value: 'option1', label: 'Option 1' }, - { value: 'option2', label: 'Option 2' }, - { value: 'option3', label: 'Option 3' }, - ]; - - const countryOptions = [ - { value: '', label: 'Select a country' }, - { value: 'us', label: 'United States' }, - { value: 'ca', label: 'Canada' }, - { value: 'uk', label: 'United Kingdom' }, - { value: 'de', label: 'Germany' }, - { value: 'fr', label: 'France' }, - ]; - - return ( -
-
-
-
- -
-

- Input Components Demo -

-

- Explore all input field variants including floating labels, - validation states, character counters, and different input types. - All components are fully interactive. -

-
- - {/* Basic Input Fields */} -
-

- Basic Input Fields -

-
- - - - - - - - - - - -
-
- - - - {/* Input Field States */} -
-

- Input Field States -

-
- - - - - - - -
-
- - - - {/* Input Field Features */} -
-

- Input Field Features -

-
- } - /> - - - - -
-
- - - - {/* Search Input */} -
-

- Search Input -

-
- - - - -
- Current search: {searchQuery || '(empty)'} -
-
-
- - - - {/* Select Fields */} -
-

- Select Fields -

-
- - - - - - - - - -
-
- - - - {/* Form Examples */} -
-

- Form Examples -

-
-
-
-

Contact Form

-
- - - -
-
- -
-

User Profile

-
- - - -
-
-
-
-
- - {/* Component Features */} -
-

- Component Features -

-
-
-
-
- - - -
-

- Floating Labels -

-

- Labels float above the input when focused or filled -

-
- -
-
- - - -
-

- Validation States -

-

- Built-in error handling and validation feedback -

-
- -
-
- - - -
-

- Character Counter -

-

- Real-time character counting with max length support -

-
-
-
-
- - {/* Navigation */} -
-
- - ← Back to Demos - - - ← Button Components - - - Auth Forms → - -
-
-
-
- ); -} diff --git a/src/app/demo/components/UserCardDemo.tsx b/src/app/demo/components/UserCardDemo.tsx deleted file mode 100644 index 0078797b..00000000 --- a/src/app/demo/components/UserCardDemo.tsx +++ /dev/null @@ -1,262 +0,0 @@ -// 'use client'; - -// import React, { useState } from 'react'; -// import Link from 'next/link'; -// import UserCard from '@/components/ui/UserCard'; -// import { Divider } from '@/components/ui/Divider'; -// import { XLogo } from '@/components/ui/icons'; - -// export function UserCardDemo() { -// const [loadingStates, setLoadingStates] = useState>( -// {} -// ); - -// const handleAction = (actionId: string, userName: string) => { -// setLoadingStates((prev) => ({ ...prev, [actionId]: true })); -// console.log(`Action: ${actionId} for ${userName}`); - -// // Simulate API call -// setTimeout(() => { -// setLoadingStates((prev) => ({ ...prev, [actionId]: false })); -// }, 1500); -// }; - -// return ( -//
-//
-//
-//
-// -//
-//

-// User Card Component -//

-//

-// A flexible user card component with customizable actions for -// different use cases. -//

-//
- -// {/* Follow Actions */} -//
-//

-// Follow/Unfollow Actions -//

-//
-// handleAction('follow-1', 'Bassem Youssef'), -// variant: 'secondary', -// loading: loadingStates['follow-1'], -// }} -// /> -// handleAction('unfollow-1', 'Ahmed Fathy'), -// variant: 'outline', -// loading: loadingStates['unfollow-1'], -// }} -// /> -//
-//
- -// - -// {/* Block Actions */} -//
-//

-// Block/Unblock Actions -//

-//
-// handleAction('block-1', 'Spam Account'), -// variant: 'outline', -// loading: loadingStates['block-1'], -// }} -// /> -// handleAction('unblock-1', 'Previously Blocked'), -// variant: 'secondary', -// loading: loadingStates['unblock-1'], -// }} -// /> -//
-//
- -// - -// {/* Mute Actions */} -//
-//

-// Mute/Unmute Actions -//

-//
-// handleAction('mute-1', 'Noisy User'), -// variant: 'outline', -// loading: loadingStates['mute-1'], -// }} -// /> -// handleAction('unmute-1', 'Previously Muted'), -// variant: 'secondary', -// loading: loadingStates['unmute-1'], -// }} -// /> -//
-//
- -// - -// {/* Different Button Variants */} -//
-//

-// Button Variants -//

-//
-// handleAction('primary-1', 'Primary Button'), -// variant: 'primary', -// loading: loadingStates['primary-1'], -// }} -// /> -// handleAction('secondary-1', 'Secondary Button'), -// variant: 'secondary', -// loading: loadingStates['secondary-1'], -// }} -// /> -// handleAction('outline-1', 'Outline Button'), -// variant: 'outline', -// loading: loadingStates['outline-1'], -// }} -// /> -// handleAction('ghost-1', 'Ghost Button'), -// variant: 'ghost', -// loading: loadingStates['ghost-1'], -// }} -// /> -//
-//
- -// - -// {/* With Avatar Images (placeholder) */} -//
-//

-// With Avatar Images -//

-//
-// handleAction('avatar-1', 'User With Avatar'), -// variant: 'secondary', -// loading: loadingStates['avatar-1'], -// }} -// /> -// handleAction('avatar-2', 'Another User'), -// variant: 'outline', -// loading: loadingStates['avatar-2'], -// }} -// /> -//
-//
- -// {/* Usage Code Example */} -//
-//

Usage Example

-//
-//
-//               {`import UserCard from '@/components/ui/UserCard';
-
-//  handleFollow(),
-//     variant: 'secondary',
-//     loading: isLoading,
-//   }}
-// />`}
-//             
-//
-//
- -// {/* Navigation */} -//
-// -// ← Back to Demos -// -//
-//
-//
-// ); -// } diff --git a/src/app/demo/components/XModalDemo.tsx b/src/app/demo/components/XModalDemo.tsx deleted file mode 100644 index 528aa26a..00000000 --- a/src/app/demo/components/XModalDemo.tsx +++ /dev/null @@ -1,471 +0,0 @@ -'use client'; - -import React, { useState } from 'react'; -import Link from 'next/link'; -import XModal from '@/components/ui/hoc/XModal'; -import Button from '@/components/ui/Button'; -import { InputField } from '@/components/ui/input'; -import { Divider } from '@/components/ui/Divider'; -import { XLogo } from '@/components/ui/icons'; - -export function XModalDemo() { - const [activeModal, setActiveModal] = useState(null); - const [formData, setFormData] = useState({ name: '', email: '' }); - - const openModal = (modalType: string) => setActiveModal(modalType); - const closeModal = () => setActiveModal(null); - - return ( -
-
-
-
- -
-

- XModal Components Demo -

-

- Explore all modal variants including confirmation, form, info, - error, success, and more. All modals are fully functional and - interactive. -

-
- - {/* Confirmation Modals */} -
-

- Confirmation Modals -

-
-
- - - -
-
-
- - - - {/* Info & Alert Modals */} -
-

- Info & Alert Modals -

-
-
- - - - -
-
-
- - - - {/* Form Modals */} -
-

- Form Modals -

-
-
- - -
-
-
- - - - {/* Size Variations */} -
-

- Size Variations -

-
-
- - - - -
-
-
- - {/* Delete Confirmation Modal */} - -

- This can't be undone and it will be removed from your profile, - the timeline of any accounts that follow you, and from search - results. -

-
- - -
-
- - {/* Logout Confirmation Modal */} - -

- You can always log back in at any time. If you just want to switch - accounts, you can do that by adding an existing account. -

-
- - -
-
- - {/* Discard Changes Modal */} - -

- This can't be undone and you'll lose your changes. -

-
- - -
-
- - {/* Success Modal */} - -
-
- - - -
-

- Your changes have been saved successfully! -

- -
-
- - {/* Error Modal */} - -
-
- - - -
-

- Something went wrong. Please try again later. -

- -
-
- - {/* Warning Modal */} - -
-
- - - -
-

- This action requires your attention. Please review before - proceeding. -

-
- - -
-
-
- - {/* Info Modal */} - -
-
- - - -
-

- This is an informational modal. It can be used to display - important information, tips, or announcements to users. -

- -
-
- - {/* Contact Form Modal */} - -
- - setFormData({ ...formData, name: e.target.value }) - } - /> - - setFormData({ ...formData, email: e.target.value }) - } - /> -
- - -
-
-
- - {/* Edit Profile Modal */} - -
- {}} - maxLength={50} - showCharCount - /> - {}} - maxLength={160} - showCharCount - /> - {}} - /> -
- - -
-
-
- - {/* Size Variations Modals */} - -

- This is a small modal (max-w-sm). Perfect for simple confirmations - and alerts. -

- -
- - -

- This is a medium modal (max-w-md). Great for forms and moderate - content. -

- -
- - -

- This is a large modal (max-w-lg). Suitable for detailed forms and - extensive content. -

- -
- - -

- This is an extra large modal (max-w-xl). Best for complex forms and - rich content displays. -

- -
- - {/* Navigation */} -
-
- - ← Back to Demos - - - Button Components - - - Input Components - -
-
-
-
- ); -} diff --git a/src/app/demo/inputs/page.tsx b/src/app/demo/inputs/page.tsx deleted file mode 100644 index b290f67c..00000000 --- a/src/app/demo/inputs/page.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import { InputFieldsDemo } from '../components/InputFieldsDemo'; - -export default function InputFieldsDemoPage() { - return ; -} diff --git a/src/app/demo/modals/page.tsx b/src/app/demo/modals/page.tsx deleted file mode 100644 index 7a46114e..00000000 --- a/src/app/demo/modals/page.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import { XModalDemo } from '../components/XModalDemo'; - -export default function ModalDemoPage() { - return ; -} diff --git a/src/app/demo/page.tsx b/src/app/demo/page.tsx deleted file mode 100644 index 3522747d..00000000 --- a/src/app/demo/page.tsx +++ /dev/null @@ -1,278 +0,0 @@ -import Link from 'next/link'; -import { XLogo } from '@/components/ui/icons'; - -export default function DemoIndexPage() { - return ( -
-
-
-
- -
-

- Component Library Demo -

-

- Explore our comprehensive collection of reusable UI components built - with clean architecture principles. All components follow X/Twitter - design patterns and are fully responsive. -

-
- -
- {/* Generic Auth Forms */} - -
-
- - - -
-
-

- Generic Auth Forms -

-

- Responsive authentication forms with modal/full-page modes. - Includes login, register, password reset, and custom form - examples. -

-
- Explore Forms → -
- - - {/* Button Components */} - -
-
- - - -
-
-

- Button Components -

-

- All button variants including social login, primary, secondary, - outline, and ghost styles with loading states. -

-
- View Buttons → -
- - - {/* Input Components */} - -
-
- - - - -
-
-

- Input Components -

-

- Input fields, select dropdowns, floating labels, password toggles, - character counters, and validation states. -

-
- View Inputs → -
- - - {/* Modal Components */} - -
-
- - - -
-
-

- Modal Components -

-

- Confirmation, info, error, success, warning, and form modals with - multiple size variations and interactive examples. -

-
- View Modals → -
- - - {/* User Card Component */} - -
-
- - - -
-
-

- User Card Component -

-

- Flexible user cards with customizable actions: Follow, Unfollow, - Block, Unblock, Mute, Unmute with avatar support. -

-
- View User Cards → -
- -
- - {/* Features Section */} -
-

- Key Features -

-
-
-
- - - -
-

- Responsive Design -

-

- Components automatically adapt to screen size with - modal/full-page modes -

-
-
-
- - - -
-

TypeScript

-

- Fully typed components with comprehensive interfaces and type - safety -

-
-
-
- - - -
-

Reusable

-

- Generic components that can be used anywhere in your project -

-
-
-
- - {/* Navigation */} -
-
- - Login Page - - - Register Page - - - Home - -
-
-
-
- ); -} diff --git a/src/app/demo/usercard/page.tsx b/src/app/demo/usercard/page.tsx deleted file mode 100644 index 1c1b7376..00000000 --- a/src/app/demo/usercard/page.tsx +++ /dev/null @@ -1,6 +0,0 @@ -// import { UserCardDemo } from '../components/UserCardDemo'; - -export default function UserCardPage() { - return
User Card Demo Page
; - // return ; -} diff --git a/src/app/explore/layout.tsx b/src/app/explore/layout.tsx index e23c2fea..30d44169 100644 --- a/src/app/explore/layout.tsx +++ b/src/app/explore/layout.tsx @@ -3,7 +3,7 @@ import LayoutWrapper from '@/features/layout/components/LayoutWrapper'; import { ReactNode } from 'react'; import { usePathname } from 'next/navigation'; -export default function Layout({ children }: { children: ReactNode }) { +export default function Layout({ children }: { readonly children: ReactNode }) { const pathname = usePathname(); const isOnExploreTabs = pathname?.startsWith('/explore/tabs'); diff --git a/src/app/explore/tabs/[tab]/page.tsx b/src/app/explore/tabs/[tab]/page.tsx index 18ad3572..2a6a556b 100644 --- a/src/app/explore/tabs/[tab]/page.tsx +++ b/src/app/explore/tabs/[tab]/page.tsx @@ -5,7 +5,11 @@ import { exploreTabs, FOR_YOU_TAB } from '@/features/explore/constants/tabs'; import { useActions } from '@/features/explore/store/useExploreStore'; import React, { useEffect } from 'react'; -export default function Page({ params }: { params: Promise<{ tab: string }> }) { +export default function Page({ + params, +}: { + readonly params: Promise<{ tab: string }>; +}) { const { tab } = React.use(params); const { selectTab, setSearchQuery } = useActions(); diff --git a/src/app/home/[full-tweet]/page.tsx b/src/app/home/[full-tweet]/page.tsx index eef57c30..7cad5ee6 100644 --- a/src/app/home/[full-tweet]/page.tsx +++ b/src/app/home/[full-tweet]/page.tsx @@ -3,7 +3,6 @@ import React from 'react'; import FullTweet from '@/features/tweets/components/FullTweet'; import { useParams } from 'next/navigation'; import { useTweetById } from '@/features/tweets/hooks/tweetQueries'; -import Loader from '@/components/generic/Loader'; function Page() { const id = Number(useParams()?.['full-tweet']); @@ -11,9 +10,6 @@ function Page() { const tweet = tweetQuery.data?.data[0] || null; const isError = tweetQuery.isError; - // if (tweetQuery.isLoading) { - // return ; - // } if (isError) return (
diff --git a/src/app/home/layout.tsx b/src/app/home/layout.tsx index 4eaa4f6d..9362c116 100644 --- a/src/app/home/layout.tsx +++ b/src/app/home/layout.tsx @@ -1,12 +1,6 @@ import LayoutWrapper from '@/features/layout/components/LayoutWrapper'; import { ReactNode } from 'react'; -export default function layout({ - children, -}: { - children: ReactNode; - // compose: ReactNode; -}) { - // return <>{children}; +export default function layout({ children }: { children: ReactNode }) { return {children}; } diff --git a/src/app/i/foundmedia/search/page.tsx b/src/app/i/foundmedia/search/page.tsx index 1cbdb887..04d3f7a8 100644 --- a/src/app/i/foundmedia/search/page.tsx +++ b/src/app/i/foundmedia/search/page.tsx @@ -7,7 +7,6 @@ import { useEffect, useState } from 'react'; import Loader from '@/components/generic/Loader'; import LayoutWrapper from '@/features/layout/components/LayoutWrapper'; export default function Page() { - // const { open } = useGifACtions(); const router = useRouter(); const [isLoading, setIsLoading] = useState(true); useEffect( @@ -19,12 +18,11 @@ export default function Page() { 'navigation' )[0] as PerformanceNavigationTiming; if (navEntry?.type === 'reload') { - // setIsReload(true); handleOpenSchedule(); setIsLoading(false); } else router.push('/home'); }, - [open, router] + [router] ); if (isLoading) return ( diff --git a/src/app/interests/[interest]/[tab]/page.tsx b/src/app/interests/[interest]/[tab]/page.tsx index 927d12cf..49a9b41e 100644 --- a/src/app/interests/[interest]/[tab]/page.tsx +++ b/src/app/interests/[interest]/[tab]/page.tsx @@ -8,7 +8,7 @@ import React, { useEffect } from 'react'; export default function Page({ params, }: { - params: Promise<{ interest: string; tab: string }>; + readonly params: Promise<{ interest: string; tab: string }>; }) { const { tab, interest } = React.use(params); const { selectInterestTab, setInterest } = useActions(); diff --git a/src/app/interests/[interest]/page.tsx b/src/app/interests/[interest]/page.tsx index 30403337..b3200bad 100644 --- a/src/app/interests/[interest]/page.tsx +++ b/src/app/interests/[interest]/page.tsx @@ -9,7 +9,7 @@ import React, { useEffect } from 'react'; export default function Page({ params, }: { - params: Promise<{ interest: string }>; + readonly params: Promise<{ interest: string }>; }) { const { selectInterestTab, setInterest } = useActions(); const { interest } = React.use(params); diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 074d5ebf..10e4949d 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,7 +1,6 @@ import type { Metadata } from 'next'; import { Inter } from 'next/font/google'; import './globals.css'; -// import './globals.css'; import { Providers } from '@/lib/providers'; import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; const inter = Inter({ subsets: ['latin'], variable: '--font-inter' }); @@ -31,7 +30,7 @@ export const metadata: Metadata = { export default function RootLayout({ children, }: { - children: React.ReactNode; + readonly children: React.ReactNode; }) { return ( diff --git a/src/app/messages/layout.tsx b/src/app/messages/layout.tsx index a6e934dc..f84f48a4 100644 --- a/src/app/messages/layout.tsx +++ b/src/app/messages/layout.tsx @@ -5,7 +5,7 @@ import LayoutWrapper from '@/features/layout/components/LayoutWrapper'; export default function MessagesRootLayout({ children, }: { - children: React.ReactNode; + readonly children: React.ReactNode; }) { const pathname = usePathname(); const isConversationView = pathname !== '/messages'; diff --git a/src/app/notifications/layout.tsx b/src/app/notifications/layout.tsx index b641428e..f8944b99 100644 --- a/src/app/notifications/layout.tsx +++ b/src/app/notifications/layout.tsx @@ -4,7 +4,7 @@ import { ReactNode } from 'react'; export default function NotificationsLayout({ children, }: { - children: ReactNode; + readonly children: ReactNode; }) { return {children}; } diff --git a/src/app/settings/layout.tsx b/src/app/settings/layout.tsx index 95ef2315..6f79103d 100644 --- a/src/app/settings/layout.tsx +++ b/src/app/settings/layout.tsx @@ -4,7 +4,7 @@ import { SettingsLayout } from '@/features/settings/components'; export default function SettingsRootLayout({ children, }: { - children: React.ReactNode; + readonly children: React.ReactNode; }) { return ( diff --git a/src/app/test-backend/page.tsx b/src/app/test-backend/page.tsx deleted file mode 100644 index 923e806d..00000000 --- a/src/app/test-backend/page.tsx +++ /dev/null @@ -1,141 +0,0 @@ -'use client'; -import { useState } from 'react'; -import { - fetchConversations, - fetchMessages, - createMessage, -} from '@/features/messages/api/messages'; - -export default function BackendTestPage() { - const [results, setResults] = useState({}); - const [loading, setLoading] = useState(null); - - const runTest = async (testName: string, testFn: () => Promise) => { - setLoading(testName); - try { - const result = await testFn(); - setResults((prev: any) => ({ - ...prev, - [testName]: { status: 'success', data: result }, - })); - } catch (error: any) { - setResults((prev: any) => ({ - ...prev, - [testName]: { status: 'error', error: error.message }, - })); - } finally { - setLoading(null); - } - }; - - const tests = [ - { - name: 'GET /conversations', - description: 'Fetch all conversations', - fn: () => fetchConversations(), - }, - { - name: 'GET /conversations/1/messages', - description: 'Fetch messages for conversation 1', - fn: () => fetchMessages(1), - }, - { - name: 'POST /conversations/1/messages', - description: 'Send a test message', - fn: () => createMessage(1, 'Test message from frontend test page'), - }, - ]; - - return ( -
-
-

Backend API Test Page

-

- Test all messaging endpoints to verify backend fixes -

- -
- {tests.map((test) => ( -
-
-
-

{test.name}

-

{test.description}

-
- -
- - {results[test.name] && ( -
-
- - {results[test.name].status === 'success' - ? '✅ SUCCESS' - : '❌ FAILED'} - -
-
-                    {JSON.stringify(
-                      results[test.name].status === 'success'
-                        ? results[test.name].data
-                        : { error: results[test.name].error },
-                      null,
-                      2
-                    )}
-                  
-
- )} -
- ))} -
- -
-

✅ What Should Work:

-
    -
  • • GET /conversations → Returns array of conversations
  • -
  • • GET /conversations/1/messages → Returns array of messages
  • -
  • - • POST /conversations/1/messages → Creates and returns new message -
  • -
-
- -
-

⚠️ Common Issues:

-
    -
  • • 404 Error → Endpoint not implemented yet
  • -
  • • 401 Error → Not authenticated (login first)
  • -
  • • 500 Error → Backend server error
  • -
  • • CORS Error → Backend CORS config still has issues
  • -
-
- -
-

🧪 How to Use:

-
    -
  1. Make sure you're logged in (user ID 6)
  2. -
  3. Click "Run Test" on each endpoint
  4. -
  5. Check if status is SUCCESS (green) or FAILED (red)
  6. -
  7. Review the response data
  8. -
  9. Share results with backend team if any fail
  10. -
-
-
-
- ); -} diff --git a/src/app/tweet/page.tsx b/src/app/tweet/page.tsx deleted file mode 100644 index 5758e2c4..00000000 --- a/src/app/tweet/page.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import React from 'react'; -import Tweet from '../../features/tweets/components/Tweet'; -function Page() { - const time = new Date(Date.now() - 250 * 30 * 10 * 100 * 1000 * 60); - const data = { - id: '1', - content: { - text: ' tweet to demonstrate the layout.', - image: '/Personal photo.jpeg', - }, - user: { - name: 'Omda Hancker', - username: '@mohamedemad', - avatar: '/apple.png', - bio: 'Developer at XYZ. Love coding and coffee.', - following: 150, - followers: '2.5K', - isVerified: true, - isFollowed: false, - }, - time: time, - Actions: { - replies: 2, - retweets: 4, - likes: 24, - bookmarks: 10, - views: '1.5K', - booked: true, - shared: false, - liked: false, - reposted: false, - }, - }; - return ( - <> - {/* */} - {/* - - */} - - ); -} - -export default Page; - -// merged with fulltweet branch diff --git a/src/components/generic/Avatar.tsx b/src/components/generic/Avatar.tsx index f9e6181c..9365514b 100644 --- a/src/components/generic/Avatar.tsx +++ b/src/components/generic/Avatar.tsx @@ -49,16 +49,20 @@ const Avatar = ({ const initial = getInitial(); - const positionStyle = - position === 'absolute' && !customPosition - ? { left: '12px', top: '80px', zIndex: 1 } - : position === 'absolute' - ? { zIndex: 1 } - : {}; + let positionStyle: React.CSSProperties = {}; + if (position === 'absolute' && !customPosition) { + positionStyle = { left: '12px', top: '80px', zIndex: 1 }; + } else if (position === 'absolute') { + positionStyle = { zIndex: 1 }; + } + + const borderClass = className.includes('border-') + ? className + : `border-2 sm:border-4 ${className}`; return (
void; - testId?: string; - menuClassName?: string; - triggerClassName?: string; - showBackdrop?: boolean; + readonly children: ReactNode; + readonly items: readonly DropdownItemType[]; + readonly onOpened?: (opened: boolean) => void; + readonly testId?: string; + readonly menuClassName?: string; + readonly triggerClassName?: string; + readonly showBackdrop?: boolean; }; export default function GenericDropdown({ @@ -52,16 +51,24 @@ export default function GenericDropdown({ }; return ( -
+
{/* Backdrop to prevent clicks from propagating */} {isOpened && showBackdrop && (
{ e.stopPropagation(); setIsOpened(false); }} + onKeyDown={(e) => { + if (e.key === 'Escape' || e.key === 'Enter' || e.key === ' ') { + e.stopPropagation(); + setIsOpened(false); + } + }} className="fixed inset-0 bg-transparent z-40 cursor-default pointer-events-auto" - style={{ pointerEvents: isOpened ? 'auto' : 'none' }} + aria-label="Close dropdown" /> )} { e.stopPropagation(); setIsOpened(true); }} + onKeyDown={(e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + e.stopPropagation(); + setIsOpened(true); + } + }} + aria-haspopup="menu" + aria-expanded={isOpened} > {children} diff --git a/src/components/generic/GenericUserList.tsx b/src/components/generic/GenericUserList.tsx index 0525dcb1..94638058 100644 --- a/src/components/generic/GenericUserList.tsx +++ b/src/components/generic/GenericUserList.tsx @@ -16,8 +16,8 @@ interface UserListItem { } interface GenericUserListProps { - query: any; - 'data-testid'?: string; + readonly query: any; + readonly 'data-testid'?: string; } export default function GenericUserList({ diff --git a/src/components/generic/ImageModal.tsx b/src/components/generic/ImageModal.tsx index bc287674..a0b48319 100644 --- a/src/components/generic/ImageModal.tsx +++ b/src/components/generic/ImageModal.tsx @@ -1,22 +1,19 @@ 'use client'; import React from 'react'; import Image from 'next/image'; -import Icon from '@/components/ui/home/Icon'; -type MediaItem = { - url: string; - type: string | 'image' | 'video'; -}; +import Icon from '@/components/ui/home/Icon'; +import type { MediaItem } from '@/features/tweets/types'; interface ImageModalProps { - isOpen: boolean; - onClose: (e: React.MouseEvent) => void; - media: MediaItem[]; - currentIndex: number; - onNext?: (e: React.MouseEvent) => void; - onPrev?: (e: React.MouseEvent) => void; - showNavigation?: boolean; - showCounter?: boolean; + readonly isOpen: boolean; + readonly onClose: (e: React.MouseEvent) => void; + readonly media: MediaItem[]; + readonly currentIndex: number; + readonly onNext?: (e: React.MouseEvent) => void; + readonly onPrev?: (e: React.MouseEvent) => void; + readonly showNavigation?: boolean; + readonly showCounter?: boolean; } export default function ImageModal({ @@ -38,8 +35,17 @@ export default function ImageModal({ return (
{ + if (e.key === 'Escape') { + onClose(e as any); + } + }} + aria-label="Image viewer" data-testid="image-modal" > {/* Close button */} @@ -95,8 +101,10 @@ export default function ImageModal({ {/* Image container */}
e.stopPropagation()} + onKeyDown={(e) => e.stopPropagation()} data-testid="image-modal-content" > {currentMedia?.type.toLowerCase() === 'image' ? ( @@ -122,7 +130,9 @@ export default function ImageModal({ src={currentMedia?.url} onClick={(e) => e.stopPropagation()} data-testid="image-modal-video" - /> + > + + )}
diff --git a/src/components/generic/Tabs.tsx b/src/components/generic/Tabs.tsx index 7ea9c845..bf784b8d 100644 --- a/src/components/generic/Tabs.tsx +++ b/src/components/generic/Tabs.tsx @@ -7,11 +7,11 @@ interface TabItem { } interface TabsProps { - tabs: TabItem[]; - selectedValue: string | number; - onClick: (value: string) => void; - height: string; - 'data-testid'?: string; + readonly tabs: TabItem[]; + readonly selectedValue: string | number; + readonly onClick: (value: string) => void; + readonly height: string; + readonly 'data-testid'?: string; } export default function Tabs({ diff --git a/src/components/generic/__tests__/Avatar.test.tsx b/src/components/generic/__tests__/Avatar.test.tsx new file mode 100644 index 00000000..a7dbee2b --- /dev/null +++ b/src/components/generic/__tests__/Avatar.test.tsx @@ -0,0 +1,136 @@ +import { describe, it, expect } from 'vitest'; +import { render, screen } from '@testing-library/react'; +import Avatar from '../Avatar'; + +describe('Avatar Component', () => { + it('should render avatar with image', () => { + render( + + ); + + const image = screen.getByAltText('Test User'); + expect(image).toBeInTheDocument(); + expect(image).toHaveAttribute('src'); + }); + + it('should render initial when no image is provided', () => { + render(); + + expect(screen.getByText('T')).toBeInTheDocument(); + }); + + it('should not render initial when name is not provided', () => { + const { container } = render(); + + expect(container.querySelector('span')).not.toBeInTheDocument(); + }); + + it('should apply correct size classes for xs size', () => { + const { container } = render( + + ); + + const avatarDiv = container.firstChild as HTMLElement; + expect(avatarDiv).toHaveClass('w-[38px]', 'h-[38px]'); + }); + + it('should apply correct size classes for lg size', () => { + const { container } = render( + + ); + + const avatarDiv = container.firstChild as HTMLElement; + expect(avatarDiv).toHaveClass('w-[100px]', 'h-[100px]'); + }); + + it('should apply absolute positioning by default', () => { + const { container } = render(); + + const avatarDiv = container.firstChild as HTMLElement; + expect(avatarDiv).toHaveClass('absolute'); + }); + + it('should apply relative positioning when specified', () => { + const { container } = render( + + ); + + const avatarDiv = container.firstChild as HTMLElement; + expect(avatarDiv).toHaveClass('relative'); + }); + + it('should apply custom className', () => { + const { container } = render( + + ); + + const avatarDiv = container.firstChild as HTMLElement; + expect(avatarDiv).toHaveClass('custom-border', 'border-8'); + }); + + it('should render children', () => { + render( + +
Child Content
+
+ ); + + expect(screen.getByTestId('child-element')).toBeInTheDocument(); + expect(screen.getByText('Child Content')).toBeInTheDocument(); + }); + + it('should apply default border classes when className does not include border', () => { + const { container } = render( + + ); + + const avatarDiv = container.firstChild as HTMLElement; + expect(avatarDiv.className).toContain('border-2'); + expect(avatarDiv.className).toContain('sm:border-4'); + }); + + it('should use provided border class when className includes border', () => { + const { container } = render( + + ); + + const avatarDiv = container.firstChild as HTMLElement; + expect(avatarDiv).toHaveClass('border-8'); + expect(avatarDiv.className).not.toContain('border-2'); + }); + + it('should apply custom position style when customPosition is true', () => { + const { container } = render( + + ); + + const avatarDiv = container.firstChild as HTMLElement; + expect(avatarDiv.style.left).toBe(''); + expect(avatarDiv.style.top).toBe(''); + }); + + it('should apply default position style when position is absolute and customPosition is false', () => { + const { container } = render( + + ); + + const avatarDiv = container.firstChild as HTMLElement; + expect(avatarDiv.style.left).toBe('12px'); + expect(avatarDiv.style.top).toBe('80px'); + expect(avatarDiv.style.zIndex).toBe('1'); + }); +}); diff --git a/src/components/generic/__tests__/BlockBtn.test.tsx b/src/components/generic/__tests__/BlockBtn.test.tsx new file mode 100644 index 00000000..81845dcd --- /dev/null +++ b/src/components/generic/__tests__/BlockBtn.test.tsx @@ -0,0 +1,164 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { render, screen, fireEvent, waitFor } from '@/test/test-utils'; +import BlockBtn from '../buttons/BlockBtn'; + +vi.mock('@/hooks/useInteractions', () => ({ + useInteractions: () => ({ + blockUser: vi.fn().mockResolvedValue(undefined), + unblockUser: vi.fn().mockResolvedValue(undefined), + isBlockLoading: false, + }), +})); + +vi.mock('@/components/ui/hoc/ConfirmModal', () => ({ + default: ({ + isOpen, + onConfirm, + title, + }: { + isOpen: boolean; + onConfirm: () => void; + title: string; + }) => + isOpen ? ( +
+
{title}
+ +
+ ) : null, +})); + +describe('BlockBtn Component', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('should render block button when not blocked', () => { + render(); + + const button = screen.getByRole('button'); + expect(button).toBeInTheDocument(); + expect(button.textContent).toBe('Block'); + }); + + it('should render blocked button when blocked', () => { + render(); + + const button = screen.getByRole('button'); + expect(button).toBeInTheDocument(); + expect(button.textContent).toBe('Blocked'); + }); + + it('should show "Unblock" on hover when already blocked', async () => { + render(); + + const button = screen.getByRole('button'); + fireEvent.mouseEnter(button); + + await waitFor(() => { + expect(button.textContent).toBe('Unblock'); + }); + }); + + it('should show "Blocked" when mouse leaves after hover', async () => { + render(); + + const button = screen.getByRole('button'); + fireEvent.mouseEnter(button); + await waitFor(() => expect(button.textContent).toBe('Unblock')); + + fireEvent.mouseLeave(button); + await waitFor(() => { + expect(button.textContent).toBe('Blocked'); + }); + }); + + it('should show confirmation modal when clicking block button', async () => { + render(); + + const button = screen.getByRole('button'); + fireEvent.click(button); + + await waitFor(() => { + expect(screen.getByTestId('confirm-modal')).toBeInTheDocument(); + expect(screen.getByText('Block user?')).toBeInTheDocument(); + }); + }); + + it('should show confirmation modal when clicking unblock button', async () => { + render(); + + const button = screen.getByRole('button'); + fireEvent.click(button); + + await waitFor(() => { + expect(screen.getByTestId('confirm-modal')).toBeInTheDocument(); + expect(screen.getByText('Unblock user?')).toBeInTheDocument(); + }); + }); + + it('should block user when confirmation is accepted', async () => { + render(); + + const button = screen.getByRole('button'); + fireEvent.click(button); + + await waitFor(() => { + expect(screen.getByTestId('confirm-modal')).toBeInTheDocument(); + }); + + const confirmButton = screen.getByTestId('confirm-button'); + fireEvent.click(confirmButton); + + await waitFor(() => { + expect(button.textContent).toBe('Blocked'); + }); + }); + + it('should unblock user when confirmation is accepted', async () => { + render(); + + const button = screen.getByRole('button'); + fireEvent.click(button); + + await waitFor(() => { + expect(screen.getByTestId('confirm-modal')).toBeInTheDocument(); + }); + + const confirmButton = screen.getByTestId('confirm-button'); + fireEvent.click(confirmButton); + + await waitFor(() => { + expect(button.textContent).toBe('Block'); + }); + }); + + it('should stop propagation when button is clicked', () => { + const mockParentClick = vi.fn(); + + render( +
+ +
+ ); + + const button = screen.getByRole('button'); + fireEvent.click(button); + + expect(mockParentClick).not.toHaveBeenCalled(); + }); + + it('should update state when isBlocked prop changes', () => { + const { rerender } = render(); + + let button = screen.getByRole('button'); + expect(button.textContent).toBe('Block'); + + rerender(); + + button = screen.getByRole('button'); + expect(button.textContent).toBe('Blocked'); + }); +}); diff --git a/src/components/generic/__tests__/Cover.test.tsx b/src/components/generic/__tests__/Cover.test.tsx new file mode 100644 index 00000000..f4ee46db --- /dev/null +++ b/src/components/generic/__tests__/Cover.test.tsx @@ -0,0 +1,70 @@ +import { describe, it, expect } from 'vitest'; +import { render, screen } from '@testing-library/react'; +import Cover from '../Cover'; + +describe('Cover Component', () => { + it('should render with default styles when no cover image is provided', () => { + const { container } = render(); + + const coverDiv = container.firstChild as HTMLElement; + expect(coverDiv).toBeInTheDocument(); + expect(coverDiv.style.backgroundImage).toBe('none'); + expect(coverDiv.style.backgroundColor).toBe('rgb(51, 54, 57)'); + }); + + it('should render with cover image when provided', () => { + const imageUrl = 'https://example.com/cover.jpg'; + const { container } = render(); + + const coverDiv = container.firstChild as HTMLElement; + expect(coverDiv.style.backgroundImage).toBe(`url("${imageUrl}")`); + expect(coverDiv.style.backgroundSize).toBe('cover'); + expect(coverDiv.style.backgroundPosition).toBe('center center'); + }); + + it('should render children', () => { + render( + +
Child Content
+
+ ); + + expect(screen.getByTestId('child-element')).toBeInTheDocument(); + expect(screen.getByText('Child Content')).toBeInTheDocument(); + }); + + it('should apply custom className', () => { + const { container } = render(); + + const coverDiv = container.firstChild as HTMLElement; + expect(coverDiv).toHaveClass('custom-class'); + }); + + it('should have default height classes', () => { + const { container } = render(); + + const coverDiv = container.firstChild as HTMLElement; + expect(coverDiv).toHaveClass('h-[120px]', 'sm:h-[200px]'); + }); + + it('should have default padding classes', () => { + const { container } = render(); + + const coverDiv = container.firstChild as HTMLElement; + expect(coverDiv).toHaveClass('p-4', 'sm:p-8'); + }); + + it('should have full width', () => { + const { container } = render(); + + const coverDiv = container.firstChild as HTMLElement; + expect(coverDiv).toHaveClass('w-full'); + }); + + it('should be a relative positioned flex container', () => { + const { container } = render(); + + const coverDiv = container.firstChild as HTMLElement; + expect(coverDiv).toHaveClass('relative', 'flex', 'flex-row'); + }); +}); diff --git a/src/components/generic/__tests__/Dropdown.test.tsx b/src/components/generic/__tests__/Dropdown.test.tsx new file mode 100644 index 00000000..77327558 --- /dev/null +++ b/src/components/generic/__tests__/Dropdown.test.tsx @@ -0,0 +1,169 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { render, screen, fireEvent } from '@testing-library/react'; +import GenericDropdown, { DropdownItemType } from '../Dropdown'; + +// Mock @heroui/react components +vi.mock('@heroui/react', () => ({ + Dropdown: ({ children, onOpenChange }: any) => ( +
onOpenChange && onOpenChange(true)} + > + {children} +
+ ), + DropdownTrigger: ({ children }: any) => ( +
{children}
+ ), + DropdownMenu: ({ children, 'data-testid': testId }: any) => ( +
{children}
+ ), + DropdownItem: ({ children, onClick, 'data-testid': testId }: any) => ( + + ), +})); + +describe('GenericDropdown Component', () => { + const mockOnOpened = vi.fn(); + const mockItemClick = vi.fn(); + + const mockItems: DropdownItemType[] = [ + { + key: 'edit', + label: 'Edit', + onClick: mockItemClick, + }, + { + key: 'delete', + label: 'Delete', + color: 'danger', + onClick: mockItemClick, + }, + { + key: 'share', + label: 'Share', + }, + ]; + + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('should render trigger children', () => { + render( + + + + ); + + expect(screen.getByText('Open Menu')).toBeInTheDocument(); + }); + + it('should render all dropdown items', () => { + render( + + + + ); + + expect(screen.getByText('Edit')).toBeInTheDocument(); + expect(screen.getByText('Delete')).toBeInTheDocument(); + expect(screen.getByText('Share')).toBeInTheDocument(); + }); + + it('should call item onClick when item is clicked', () => { + render( + + + + ); + + fireEvent.click(screen.getByText('Edit')); + expect(mockItemClick).toHaveBeenCalled(); + }); + + it('should use custom testId when provided', () => { + render( + + + + ); + + expect(screen.getByTestId('custom-dropdown-menu')).toBeInTheDocument(); + }); + + it('should use default testId when not provided', () => { + render( + + + + ); + + expect(screen.getAllByTestId('dropdown').length).toBeGreaterThan(0); + }); + + it('should render item test ids correctly', () => { + render( + + + + ); + + expect(screen.getByTestId('test-dropdown-item-edit')).toBeInTheDocument(); + expect(screen.getByTestId('test-dropdown-item-delete')).toBeInTheDocument(); + expect(screen.getByTestId('test-dropdown-item-share')).toBeInTheDocument(); + }); + + it('should handle items without onClick', () => { + render( + + + + ); + + // Should not throw when clicking item without onClick + expect(() => { + fireEvent.click(screen.getByText('Share')); + }).not.toThrow(); + }); + + it('should apply custom menuClassName', () => { + render( + + + + ); + + expect(screen.getByTestId('dropdown-menu')).toBeInTheDocument(); + }); + + it('should apply custom triggerClassName', () => { + const { container } = render( + + + + ); + + const spanElement = container.querySelector('span.custom-trigger-class'); + expect(spanElement).toBeDefined(); + expect(spanElement).not.toBeNull(); + }); +}); diff --git a/src/components/generic/__tests__/EditProfileAvatar.test.tsx b/src/components/generic/__tests__/EditProfileAvatar.test.tsx new file mode 100644 index 00000000..bd855bc2 --- /dev/null +++ b/src/components/generic/__tests__/EditProfileAvatar.test.tsx @@ -0,0 +1,117 @@ +import { describe, it, expect, vi } from 'vitest'; +import { render, screen } from '@testing-library/react'; +import EditProfileAvatar from '../components/EditProfileAvatar'; + +vi.mock('../Avatar', () => ({ + default: ({ + avatarImage, + className, + position, + customPosition, + children, + 'data-testid': testId, + }: any) => ( +
+ {children} +
+ ), +})); + +vi.mock('@/components/ui/UploadImage', () => ({ + default: ({ + onFileSelect, + 'data-testid': testId, + }: { + onFileSelect: (file: File | null) => void; + 'data-testid': string; + }) => ( + + ), +})); + +describe('EditProfileAvatar Component', () => { + const mockOnFileSelect = vi.fn(); + + it('should render avatar with default props', () => { + render(); + + const avatar = screen.getByTestId('edit-profile-avatar'); + expect(avatar).toBeInTheDocument(); + expect(avatar).toHaveAttribute('data-avatar-image', 'null'); + }); + + it('should render avatar with provided image', () => { + const avatarUrl = 'https://example.com/avatar.jpg'; + render( + + ); + + const avatar = screen.getByTestId('edit-profile-avatar'); + expect(avatar).toHaveAttribute('data-avatar-image', avatarUrl); + }); + + it('should render with correct avatar position', () => { + render(); + + const avatar = screen.getByTestId('edit-profile-avatar'); + expect(avatar).toHaveAttribute('data-position', 'absolute'); + expect(avatar).toHaveAttribute('data-custom-position', 'true'); + }); + + it('should render with correct avatar className', () => { + render(); + + const avatar = screen.getByTestId('edit-profile-avatar'); + expect(avatar).toHaveAttribute( + 'data-classname', + '-top-[66px] left-3 border-2' + ); + }); + + it('should render upload image button', () => { + render(); + + const uploadButton = screen.getByTestId('edit-profile-avatar-upload'); + expect(uploadButton).toBeInTheDocument(); + }); + + it('should call onFileSelect when file is selected', () => { + render(); + + const uploadButton = screen.getByTestId('edit-profile-avatar-upload'); + uploadButton.click(); + + expect(mockOnFileSelect).toHaveBeenCalled(); + const callArgs = mockOnFileSelect.mock.calls[0][0]; + expect(callArgs).toBeInstanceOf(File); + expect(callArgs.name).toBe('test.jpg'); + }); + + it('should render upload button inside centered div', () => { + const { container } = render( + + ); + + const centeredDiv = container.querySelector( + '.absolute.top-1\\/2.left-1\\/2.transform.-translate-x-1\\/2.-translate-y-1\\/2' + ); + expect(centeredDiv).toBeInTheDocument(); + }); +}); diff --git a/src/components/generic/__tests__/EditProfileCover.test.tsx b/src/components/generic/__tests__/EditProfileCover.test.tsx new file mode 100644 index 00000000..067e57f5 --- /dev/null +++ b/src/components/generic/__tests__/EditProfileCover.test.tsx @@ -0,0 +1,192 @@ +import { describe, it, expect, vi } from 'vitest'; +import { render, screen, fireEvent } from '@testing-library/react'; +import EditProfileCover from '../components/EditProfileCover'; + +vi.mock('../Cover', () => ({ + default: ({ + coverImage, + className, + children, + 'data-testid': testId, + }: any) => ( +
+ {children} +
+ ), +})); + +vi.mock('@/components/ui/UploadImage', () => ({ + default: ({ + onFileSelect, + showClearButton, + onClear, + 'data-testid': testId, + }: { + onFileSelect: (file: File | null) => void; + showClearButton: boolean; + onClear: () => void; + 'data-testid': string; + }) => ( +
+ + {showClearButton && ( + + )} +
+ ), +})); + +describe('EditProfileCover Component', () => { + const mockOnFileSelect = vi.fn(); + const mockOnClear = vi.fn(); + + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('should render cover with default props', () => { + render( + + ); + + const cover = screen.getByTestId('edit-profile-cover'); + expect(cover).toBeInTheDocument(); + expect(cover).toHaveAttribute('data-cover-image', 'none'); + }); + + it('should render cover with provided image', () => { + const coverUrl = 'https://example.com/cover.jpg'; + render( + + ); + + const cover = screen.getByTestId('edit-profile-cover'); + expect(cover).toHaveAttribute('data-cover-image', coverUrl); + }); + + it('should render with correct className', () => { + render( + + ); + + const cover = screen.getByTestId('edit-profile-cover'); + expect(cover).toHaveAttribute('data-classname', 'mt-4'); + }); + + it('should render upload image button', () => { + render( + + ); + + const uploadButton = screen.getByTestId('edit-profile-cover-upload'); + expect(uploadButton).toBeInTheDocument(); + }); + + it('should call onFileSelect when file is selected', () => { + render( + + ); + + const uploadButton = screen.getByTestId('edit-profile-cover-upload'); + fireEvent.click(uploadButton); + + expect(mockOnFileSelect).toHaveBeenCalled(); + const callArgs = mockOnFileSelect.mock.calls[0][0]; + expect(callArgs).toBeInstanceOf(File); + expect(callArgs.name).toBe('cover.jpg'); + }); + + it('should not show clear button when showClearButton is false', () => { + render( + + ); + + const clearButton = screen.queryByTestId('edit-profile-cover-upload-clear'); + expect(clearButton).not.toBeInTheDocument(); + }); + + it('should show clear button when showClearButton is true', () => { + render( + + ); + + const clearButton = screen.getByTestId('edit-profile-cover-upload-clear'); + expect(clearButton).toBeInTheDocument(); + }); + + it('should call onClear when clear button is clicked', () => { + render( + + ); + + const clearButton = screen.getByTestId('edit-profile-cover-upload-clear'); + fireEvent.click(clearButton); + + expect(mockOnClear).toHaveBeenCalled(); + }); + + it('should render upload button inside centered div', () => { + const { container } = render( + + ); + + const centeredDiv = container.querySelector( + '.absolute.top-1\\/2.left-1\\/2.transform.-translate-x-1\\/2.-translate-y-1\\/2' + ); + expect(centeredDiv).toBeInTheDocument(); + }); +}); diff --git a/src/components/generic/__tests__/EditProfileForm.test.tsx b/src/components/generic/__tests__/EditProfileForm.test.tsx new file mode 100644 index 00000000..00a54d3f --- /dev/null +++ b/src/components/generic/__tests__/EditProfileForm.test.tsx @@ -0,0 +1,401 @@ +import { describe, it, expect, vi } from 'vitest'; +import { render, screen, fireEvent } from '@testing-library/react'; +import EditProfileForm from '../components/EditProfileForm'; + +vi.mock('@/components/ui/input/InputField', () => ({ + InputField: ({ + label, + value, + onChange, + maxLength, + showCharCount, + error, + type, + 'data-testid': testId, + }: any) => ( +
+ + {type === 'textarea' ? ( +