diff --git a/src/components/ui/icons/UIIcons.tsx b/src/components/ui/icons/UIIcons.tsx
index 7af95d76..7bc4cfd3 100644
--- a/src/components/ui/icons/UIIcons.tsx
+++ b/src/components/ui/icons/UIIcons.tsx
@@ -541,3 +541,18 @@ export const CollapseIcon = ({ className = 'w-5 h-5' }: IconProps) => {
);
};
+
+export const PencilIcon = ({ className = 'w-5 h-5' }: IconProps) => {
+ return (
+
+
+
+
+
+ );
+};
diff --git a/src/components/ui/icons/index.ts b/src/components/ui/icons/index.ts
index 1dcf20f5..4126e935 100644
--- a/src/components/ui/icons/index.ts
+++ b/src/components/ui/icons/index.ts
@@ -45,6 +45,7 @@ export {
JoinDateIcon,
CameraIcon,
SettingsIcon,
+ PencilIcon,
} from './UIIcons';
// Brand Icons
diff --git a/src/features/authentication/configs/authFormConfigs.tsx b/src/features/authentication/configs/authFormConfigs.tsx
index db07ac69..39a58fb8 100644
--- a/src/features/authentication/configs/authFormConfigs.tsx
+++ b/src/features/authentication/configs/authFormConfigs.tsx
@@ -75,7 +75,7 @@ export const authFormConfigs = {
},
signup: {
- title: 'Join X today',
+ title: 'Join Hankers today',
fields: [],
socialProviders: [
{
diff --git a/src/features/authentication/constants/index.ts b/src/features/authentication/constants/index.ts
index 63aa1914..fcd96901 100644
--- a/src/features/authentication/constants/index.ts
+++ b/src/features/authentication/constants/index.ts
@@ -1,7 +1,7 @@
// Authentication feature constants
export const FOOTER_LINKS = [
'About',
- 'Download the X app',
+ 'Download the Hankers app',
'Grok',
'Help Center',
'Terms of Service',
diff --git a/src/features/authentication/hooks/__tests__/useAuth.test.tsx b/src/features/authentication/hooks/__tests__/useAuth.test.tsx
index f2705f6b..b512d74e 100644
--- a/src/features/authentication/hooks/__tests__/useAuth.test.tsx
+++ b/src/features/authentication/hooks/__tests__/useAuth.test.tsx
@@ -462,7 +462,7 @@ describe('useAuth Hooks', () => {
await act(async () => {
await result.current.changePassword({
- currentPassword: 'old',
+ oldPassword: 'old',
newPassword: 'new',
});
});
diff --git a/src/features/layout/components/GrokSummary.tsx b/src/features/layout/components/GrokSummary.tsx
index 19557829..2d2d8a30 100644
--- a/src/features/layout/components/GrokSummary.tsx
+++ b/src/features/layout/components/GrokSummary.tsx
@@ -93,7 +93,7 @@ function SummarySubTweet({
function SummaryButton({ setOpened }: { setOpened: (val: boolean) => void }) {
return (
setOpened(true)}
>
diff --git a/src/features/layout/components/LayoutWrapper.tsx b/src/features/layout/components/LayoutWrapper.tsx
index 477ddfe1..b4586c0a 100644
--- a/src/features/layout/components/LayoutWrapper.tsx
+++ b/src/features/layout/components/LayoutWrapper.tsx
@@ -4,6 +4,7 @@ import RightSidebar from './RightSidebar';
import MobileBottomBar from './MobileBottomBar';
import { Toaster } from 'react-hot-toast';
import GrokSummary from './GrokSummary';
+import MobilePostButton from './MobilePostButton';
interface LayoutWrapperProps {
children: React.ReactNode;
showRightSidebar?: boolean;
@@ -29,6 +30,7 @@ export default function LayoutWrapper({
{showMobileBottomBar && (
+ {showRightSidebar && }
)}
diff --git a/src/features/layout/components/MobilePostButton.tsx b/src/features/layout/components/MobilePostButton.tsx
new file mode 100644
index 00000000..58624009
--- /dev/null
+++ b/src/features/layout/components/MobilePostButton.tsx
@@ -0,0 +1,26 @@
+'use client';
+import { useState } from 'react';
+import ComposeModal from '@/features/timeline/components/ComposeModal';
+import { PencilIcon } from '@/components/ui/icons';
+
+export default function MobilePostButton() {
+ const [isComposeOpen, setIsComposeOpen] = useState(false);
+
+ return (
+ <>
+
setIsComposeOpen(true)}
+ className="fixed bottom-20 right-4 bg-primary hover:bg-primary-hover text-white font-bold rounded-full transition-colors w-14 h-14 flex items-center justify-center shadow-lg z-40"
+ aria-label="Create post"
+ >
+
+
+
+
setIsComposeOpen(false)}
+ />
+ >
+ );
+}
diff --git a/src/features/layout/components/MobileSidebar.tsx b/src/features/layout/components/MobileSidebar.tsx
index b84062c7..4a59ddba 100644
--- a/src/features/layout/components/MobileSidebar.tsx
+++ b/src/features/layout/components/MobileSidebar.tsx
@@ -74,24 +74,28 @@ export default function MobileSidebar({ isOpen, onClose }: MobileSidebarProps) {
-
{profile?.following_count ?? 0}
Following
-
-
+
{profile?.followers_count ?? 0}
Followers
-
+
diff --git a/src/features/layout/components/PostButton.tsx b/src/features/layout/components/PostButton.tsx
index d8714c8d..ccd52e97 100644
--- a/src/features/layout/components/PostButton.tsx
+++ b/src/features/layout/components/PostButton.tsx
@@ -1,7 +1,7 @@
'use client';
-import { Plus } from 'lucide-react';
import { useState } from 'react';
import ComposeModal from '@/features/timeline/components/ComposeModal';
+import { PencilIcon } from '@/components/ui/icons';
export default function PostButton() {
const [isComposeOpen, setIsComposeOpen] = useState(false);
@@ -14,7 +14,7 @@ export default function PostButton() {
className="bg-white hover:bg-gray-200 text-black font-bold rounded-full transition-colors mt-4 w-14 h-14 min-[1400px]:w-full min-[1400px]:h-auto min-[1400px]:py-3 flex items-center justify-center"
>
{/* Show + icon on small screens, "Post" text at 1400px+ */}
-
+
Post
diff --git a/src/features/onboarding/components/InterestsModal.test.tsx b/src/features/onboarding/components/InterestsModal.test.tsx
index 342cfddf..6748d816 100644
--- a/src/features/onboarding/components/InterestsModal.test.tsx
+++ b/src/features/onboarding/components/InterestsModal.test.tsx
@@ -108,7 +108,7 @@ describe('InterestsModal', () => {
expect(screen.getByTestId('x-modal')).toBeInTheDocument();
expect(
- screen.getByText('What do you want to see on X?')
+ screen.getByText('What do you want to see on Hankers?')
).toBeInTheDocument();
});
diff --git a/src/features/onboarding/components/InterestsModal.tsx b/src/features/onboarding/components/InterestsModal.tsx
index 7ec11755..5083736e 100644
--- a/src/features/onboarding/components/InterestsModal.tsx
+++ b/src/features/onboarding/components/InterestsModal.tsx
@@ -59,11 +59,11 @@ export default function InterestsModal({
{/* Header */}
- What do you want to see on X?
+ What do you want to see on Hankers?
- Choose what you like, and we'll customise your X experience
- with more of what you're interested in.
+ Choose what you like, and we'll customise your Hankers
+ experience with more of what you're interested in.
diff --git a/src/features/settings/components/SettingsDetail.tsx b/src/features/settings/components/SettingsDetail.tsx
index 263728ce..5e12ce10 100644
--- a/src/features/settings/components/SettingsDetail.tsx
+++ b/src/features/settings/components/SettingsDetail.tsx
@@ -12,6 +12,7 @@ import {
MuteIcon,
} from '@/components/ui/icons';
import type { SettingsOption } from '@/features/settings/constants/SETTINGs_ITEMS';
+import { useAuth } from '@/features/authentication/hooks/useAuth';
interface SettingsDetailProps {
selectedOption: SettingsOption | null;
@@ -46,7 +47,7 @@ export default function SettingsDetail({
const handleBack = () => {
router.push('/settings');
};
-
+ const { user } = useAuth();
if (!selectedOption) {
return (
({
useRouter: vi.fn(),
}));
+// Mock useAuth
+vi.mock('@/features/authentication/hooks/useAuth', () => ({
+ useAuth: vi.fn(() => ({
+ user: {
+ username: 'ahmedfathy0-0',
+ email: 'ahmed@example.com',
+ },
+ })),
+}));
+
// Mock components
vi.mock('@/components/ui/Breadcrumb', () => ({
default: ({ title, subtitle, description, onBack, ...props }: any) => (
@@ -142,7 +152,7 @@ describe('SettingsDetail', () => {
'Your Account'
);
expect(screen.getByTestId('breadcrumb-subtitle')).toHaveTextContent(
- '@ahmedfathy0-0'
+ 'ahmedfathy0-0'
);
expect(screen.getByTestId('breadcrumb-description')).toHaveTextContent(
'Manage your account settings'
diff --git a/src/features/timeline/components/ComposeModal.tsx b/src/features/timeline/components/ComposeModal.tsx
index 0ab312ad..e34befc3 100644
--- a/src/features/timeline/components/ComposeModal.tsx
+++ b/src/features/timeline/components/ComposeModal.tsx
@@ -10,64 +10,7 @@ interface ComposeModalProps {
}
export default function ComposeModal({ isOpen, onClose }: ComposeModalProps) {
- // const useStore = useMemo(() => createAddTweetStore(), []);
- // const selectors = useMemo(
- // () => createAddTweetSelectors(useStore),
- // [useStore]
- // );
- // const isSending = selectors.useIsSending();
-
- // const { clearMedia, setTweetText } = selectors.useActions();
- // const ref = useRef(null);
- // const wasSendingRef = useRef(false);
-
- // const hasText = selectors.useTweetText().length > 0 || false;
- // const hasmMedia = selectors.useMedia().length > 0;
-
- // useEffect(() => {
- // const unloadCallback = (event: BeforeUnloadEvent) => {
- // if (hasText || hasmMedia) {
- // console.log(event);
- // event.preventDefault();
- // return '';
- // }
- // };
-
- // window.addEventListener('beforeunload', unloadCallback);
- // return () => window.removeEventListener('beforeunload', unloadCallback);
- // }, [hasText, hasmMedia]);
-
- // Track when sending starts
- // useEffect(() => {
- // if (isSending) {
- // wasSendingRef.current = true;
- // }
- // }, [isSending]);
-
- // // Close modal after successful tweet send
- // useEffect(() => {
- // // Only close if we were sending and now we're done (tweet sent successfully)
- // if (
- // wasSendingRef.current &&
- // !isSending &&
- // !hasText &&
- // !hasmMedia &&
- // isOpen
- // ) {
- // wasSendingRef.current = false;
- // const timer = setTimeout(() => {
- // onClose();
- // }, 300);
- // return () => clearTimeout(timer);
- // }
- // }, [isSending, hasText, hasmMedia, isOpen, onClose]);
-
- // Clear draft when modal closes (if user cancels)
const handleClose = () => {
- // Clear the draft
- // setTweetText('');
- // clearMedia();
- // wasSendingRef.current = false;
onClose();
};
@@ -75,13 +18,16 @@ export default function ComposeModal({ isOpen, onClose }: ComposeModalProps) {
-
+
);
}
diff --git a/src/features/timeline/components/ShowTweets.tsx b/src/features/timeline/components/ShowTweets.tsx
index c17b5928..94c73fb1 100644
--- a/src/features/timeline/components/ShowTweets.tsx
+++ b/src/features/timeline/components/ShowTweets.tsx
@@ -4,7 +4,7 @@ export default function ShowTweets() {
data-testid="show-tweets-button"
className=" flex h-12 w-full p-3 justify-center text-l text-primary border-b-1 border-border hover:cursor-pointer hover:bg-border"
>
- Show X posts
+ Show Hankers posts
);
}
diff --git a/src/features/timeline/components/Timeline.tsx b/src/features/timeline/components/Timeline.tsx
index 7e3ca51f..dd1689a4 100644
--- a/src/features/timeline/components/Timeline.tsx
+++ b/src/features/timeline/components/Timeline.tsx
@@ -109,7 +109,7 @@ export default function Timeline() {
className="flex flex-col justify-items-center full-width relative"
data-testid="timeline-content"
>
-
+
{/*
*/}
diff --git a/src/features/tweets/components/Tweet.tsx b/src/features/tweets/components/Tweet.tsx
index 852d1e63..d436040a 100644
--- a/src/features/tweets/components/Tweet.tsx
+++ b/src/features/tweets/components/Tweet.tsx
@@ -188,7 +188,7 @@ export default function Tweet({
}
: undefined;
- const deleteTweetMutation = useDeleteTweet(
+ const deleteTweetMutation: ReturnType
= useDeleteTweet(
dataViewd.postId,
actionsStats.isRepost,
actionsStats.userId,
diff --git a/src/features/tweets/components/UserInfo.tsx b/src/features/tweets/components/UserInfo.tsx
index 9c874d5b..1192fde2 100644
--- a/src/features/tweets/components/UserInfo.tsx
+++ b/src/features/tweets/components/UserInfo.tsx
@@ -141,7 +141,12 @@ export default function UserInfo({
}, leaveDelay);
}}
>
- {data.username}
+ {data.username}
+
+ {data.username.length > 10
+ ? `${data.username.slice(0, 10)}...`
+ : data.username}
+
{usernameCardShow && (
diff --git a/src/features/tweets/tests/Tweet.test.tsx b/src/features/tweets/tests/Tweet.test.tsx
index d4d8b2a3..4e7b3c3c 100644
--- a/src/features/tweets/tests/Tweet.test.tsx
+++ b/src/features/tweets/tests/Tweet.test.tsx
@@ -113,7 +113,7 @@ describe('Tweet Component', () => {
);
expect(screen.getByText('Test User')).toBeInTheDocument();
- expect(screen.getByText('testuser')).toBeInTheDocument();
+ expect(screen.getAllByText('testuser')[0]).toBeInTheDocument();
expect(screen.getByText('Test tweet text')).toBeInTheDocument();
});
diff --git a/src/features/tweets/tests/UserInfo.test.tsx b/src/features/tweets/tests/UserInfo.test.tsx
index f47fda35..40384b05 100644
--- a/src/features/tweets/tests/UserInfo.test.tsx
+++ b/src/features/tweets/tests/UserInfo.test.tsx
@@ -22,7 +22,7 @@ describe('UserInfo Component', () => {
render( );
expect(screen.getByText('Test User')).toBeInTheDocument();
- expect(screen.getByText('testuser')).toBeInTheDocument();
+ expect(screen.getAllByText('testuser')[0]).toBeInTheDocument();
});
it('should show verified icon for verified users', () => {
diff --git a/src/utils/profileValidation.ts b/src/utils/profileValidation.ts
index b4b2f8a8..f568ead2 100644
--- a/src/utils/profileValidation.ts
+++ b/src/utils/profileValidation.ts
@@ -42,6 +42,17 @@ export const validateDisplayName = (name: string): ValidationResult => {
};
}
+ // Validate name format: only letters (any language), accent marks, spaces, hyphens, or apostrophes
+ // Reject emojis, numbers, or punctuation
+ const namePattern = /^[\p{L}\p{M}' -]+$/u;
+ if (!namePattern.test(trimmed)) {
+ return {
+ isValid: false,
+ error:
+ 'Name should only contain letters (from any language), accent marks, spaces, hyphens, or apostrophes',
+ };
+ }
+
// Check if only emojis (no alphanumeric characters)
// This regex matches if there are NO letters, numbers, or common punctuation
const hasAlphanumeric = /[a-zA-Z0-9]/.test(trimmed);