Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
076ee4e
fix: improve timeline input , ui improvement and fixes for participat…
Benjtalkshow Mar 4, 2026
6a457cd
Merge branch 'main' of https://github.com/Benjtalkshow/boundless into…
Benjtalkshow Mar 4, 2026
f660875
fix: implement 2fa for email and password login
Benjtalkshow Mar 5, 2026
5460d20
fix: fix conflict
Benjtalkshow Mar 5, 2026
7b8308d
fix: fix conflict
Benjtalkshow Mar 5, 2026
31adf9f
Merge branch 'main' of https://github.com/Benjtalkshow/boundless into…
Benjtalkshow Mar 5, 2026
b6122c4
Merge branch 'main' of https://github.com/Benjtalkshow/boundless into…
Benjtalkshow Mar 5, 2026
7e57ddb
Merge branch 'main' of https://github.com/Benjtalkshow/boundless into…
Benjtalkshow Mar 5, 2026
156b0a8
Merge branch 'main' of https://github.com/Benjtalkshow/boundless into…
Benjtalkshow Mar 6, 2026
a6599eb
fix: fix submission form
Benjtalkshow Mar 6, 2026
f24e050
Merge branch 'main' of https://github.com/Benjtalkshow/boundless into…
Benjtalkshow Mar 6, 2026
222cd45
fix: fix hackathon submission and participant page
Benjtalkshow Mar 7, 2026
acad424
fix: fix hackathon submission and participant page
Benjtalkshow Mar 7, 2026
efda241
fix: fix auto refresh ib submission page
Benjtalkshow Mar 7, 2026
310a353
fix: fix conflict
Benjtalkshow Mar 7, 2026
dffd842
fix: hackathon submission fixes
Benjtalkshow Mar 8, 2026
a2ddd1d
fix: fix coderabbit corrections
Benjtalkshow Mar 9, 2026
08b85d2
fix: fix coderabbit corrections
Benjtalkshow Mar 9, 2026
fb3e148
chore: write boundless on x challenge blog
Benjtalkshow Mar 9, 2026
3408994
fix: remove blog
Benjtalkshow Mar 9, 2026
555ff6d
fix: fix conflict
Benjtalkshow Mar 9, 2026
8201463
fix: my project dashbaord count and extend hackathon deadline
Benjtalkshow Mar 10, 2026
cfd9d54
fix: my project dashbaord count and extend hackathon deadline
Benjtalkshow Mar 10, 2026
04d5abc
fix: fix auto validate wallet address and user nav
Benjtalkshow Mar 10, 2026
cac0c04
fix: fix notification badge
Benjtalkshow Mar 10, 2026
a466f87
fix: fix conflict
Benjtalkshow Mar 10, 2026
f9671cf
fix: fix coderabbit corrections
Benjtalkshow Mar 10, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 0 additions & 4 deletions app/me/notifications/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -104,10 +104,6 @@ export default function NotificationsPage() {

const { setUnreadCount, clearUnreadCount } = useNotificationStore();

useEffect(() => {
setUnreadCount(unreadCount);
}, [unreadCount, setUnreadCount]);

useNotificationPolling(notificationsHook, {
interval: 30000,
enabled: true,
Expand Down
13 changes: 10 additions & 3 deletions components/app-sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,10 @@ import {
SidebarMenuItem,
} from '@/components/ui/sidebar';
import Image from 'next/image';
import Link from 'next/link';
import { useNotificationStore } from '@/lib/stores/notification-store';
import { Logo } from './landing-page/navbar';
import { useNotifications } from '@/hooks/useNotifications';
import { authClient } from '@/lib/auth-client';

const getNavigationData = (counts?: {
participating?: number;
Expand Down Expand Up @@ -115,8 +116,8 @@ const getNavigationData = (counts?: {
url: '/me/notifications',
icon: IconBell,
badge:
counts?.unreadNotifications && counts.unreadNotifications > 0
? counts.unreadNotifications.toString()
(counts?.unreadNotifications ?? 0) > 0
? String(counts?.unreadNotifications)
: undefined,
},
],
Expand All @@ -136,6 +137,12 @@ export function AppSidebar({
user: UserData;
counts?: { participating?: number; submissions?: number; projects?: number };
} & React.ComponentProps<typeof Sidebar>) {
const { data: session } = authClient.useSession();
const userId = session?.user?.id;

// Initialize notifications hook to ensure it fetches globally and syncs with store
useNotifications({ enabled: !!userId });

const unreadNotifications = useNotificationStore(state => state.unreadCount);

const navigationData = React.useMemo(
Expand Down
63 changes: 48 additions & 15 deletions components/nav-user.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ import {
useSidebar,
} from '@/components/ui/sidebar';
import { Badge } from './ui/badge';
import Link from 'next/link';
import { useAuthActions } from '@/hooks/use-auth';
import { useNotificationStore } from '@/lib/stores/notification-store';

export interface NavUserProps {
user: {
Expand All @@ -37,6 +40,14 @@ export interface NavUserProps {

export const NavUser = ({ user }: NavUserProps): React.ReactElement => {
const { isMobile } = useSidebar();
const { logout } = useAuthActions();
const { unreadCount: unreadNotifications, clearUnreadCount } =
useNotificationStore();

const handleLogout = () => {
logout();
clearUnreadCount();
};

return (
<SidebarMenu>
Expand Down Expand Up @@ -68,7 +79,7 @@ export const NavUser = ({ user }: NavUserProps): React.ReactElement => {
</SidebarMenuButton>
</DropdownMenuTrigger>
<DropdownMenuContent
className='w-56 rounded-lg'
className='bg-background-main-bg/98 w-56 rounded-lg border-white/10 text-white backdrop-blur-xl'
side={isMobile ? 'bottom' : 'right'}
align='end'
sideOffset={4}
Expand All @@ -85,33 +96,55 @@ export const NavUser = ({ user }: NavUserProps): React.ReactElement => {
</AvatarFallback>
</Avatar>
<div className='grid flex-1 text-left leading-tight'>
<span className='truncate text-sm font-semibold'>
<span className='truncate text-sm font-semibold text-white'>
{user.name}
</span>
<span className='text-muted-foreground truncate text-xs'>
<span className='truncate text-xs text-white/60'>
{user.email}
</span>
</div>
</div>
</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem className='cursor-pointer gap-2'>
<IconUserCircle className='h-4 w-4' />
<span>Account Settings</span>
<DropdownMenuItem asChild>
<Link
href='/me/settings'
className='flex cursor-pointer items-center gap-2 text-white/80 hover:bg-white/5 hover:text-white focus:bg-white/5 focus:text-white'
>
<IconUserCircle className='h-4 w-4' />
<span>Account Settings</span>
</Link>
</DropdownMenuItem>
<DropdownMenuItem className='cursor-pointer gap-2'>
<IconCreditCard className='h-4 w-4' />
<span>Billing</span>
<DropdownMenuItem asChild>
<Link
href='/coming-soon'
className='flex cursor-pointer items-center gap-2 text-white/80 hover:bg-white/5 hover:text-white focus:bg-white/5 focus:text-white'
>
<IconCreditCard className='h-4 w-4' />
<span>Billing</span>
</Link>
</DropdownMenuItem>
<DropdownMenuItem className='cursor-pointer gap-2'>
<IconBell className='h-4 w-4' />
<span>Notifications</span>
<Badge className='ml-auto h-5 min-w-5 rounded-full'>3</Badge>
<DropdownMenuItem asChild>
<Link
href='/me/notifications'
className='flex cursor-pointer items-center gap-2 text-white/80 hover:bg-white/5 hover:text-white focus:bg-white/5 focus:text-white'
>
<IconBell className='h-4 w-4' />
<span>Notifications</span>
{unreadNotifications > 0 && (
<Badge className='ml-auto flex h-5 min-w-5 items-center justify-center rounded-full bg-[#a7f950] text-xs font-bold text-black'>
{unreadNotifications}
</Badge>
)}
</Link>
</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuItem className='text-destructive focus:text-destructive cursor-pointer gap-2'>
<DropdownMenuSeparator className='bg-white/10' />
<DropdownMenuItem
className='text-destructive focus:text-destructive cursor-pointer gap-2 hover:bg-red-500/10'
onClick={handleLogout}
>
<IconLogout className='h-4 w-4' />
<span>Log out</span>
</DropdownMenuItem>
Expand Down
65 changes: 44 additions & 21 deletions components/wallet/FamilyWalletDrawer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,10 @@ export function FamilyWalletDrawer({
setValidateResult('idle');
try {
const result = await validateSendDestination(dest, currency);

// Protect against stale responses if the user has changed the destination
if (sendDestination.trim() !== dest) return;

if (result.valid) {
setValidateResult('valid');
setValidateError('');
Expand All @@ -222,15 +226,45 @@ export function FamilyWalletDrawer({
);
}
} catch (err: unknown) {
// Protect against stale responses
if (sendDestination.trim() !== dest) return;

const { message, details } = getErrorDisplay(err);
setValidateResult('invalid');
setValidateError(message);
setValidateErrorDetails(details);
} finally {
setValidateLoading(false);
// We still want to clear loading if it's the latest call
if (sendDestination.trim() === dest) {
setValidateLoading(false);
}
}
}, [sendDestination, sendCurrency, getErrorDisplay]);

// Auto-validate destination address
useEffect(() => {
const trimmedDest = sendDestination.trim();

// Reset state if empty
if (!trimmedDest) {
setValidateResult('idle');
setValidateError('');
return;
}

// Immediate trigger if 56 chars
if (trimmedDest.length === 56) {
handleValidateDestination();
return;
}

const timer = setTimeout(() => {
handleValidateDestination();
}, 500);

return () => clearTimeout(timer);
}, [sendDestination, sendCurrency, handleValidateDestination]);

const handleSendSubmit = useCallback(async () => {
const dest = sendDestination.trim();
const currency = sendCurrency || 'XLM';
Expand Down Expand Up @@ -833,37 +867,26 @@ export function FamilyWalletDrawer({
<Label htmlFor='send-destination'>
Destination (Stellar G...)
</Label>
<div className='flex gap-2'>
<div className='relative'>
<Input
id='send-destination'
placeholder='GABCD...'
value={sendDestination}
onChange={e => {
setSendDestination(e.target.value);
setValidateResult('idle');
setValidateError('');
}}
className='font-mono text-sm'
className='pr-10 font-mono text-sm'
/>
<Button
type='button'
variant='outline'
size='sm'
onClick={handleValidateDestination}
disabled={
validateLoading ||
!sendDestination.trim() ||
!sendCurrency
}
>
<div className='absolute top-1/2 right-3 -translate-y-1/2'>
{validateLoading ? (
<Loader2 className='h-4 w-4 animate-spin' />
<Loader2 className='text-muted-foreground h-4 w-4 animate-spin' />
) : validateResult === 'valid' ? (
<CheckCircle className='h-4 w-4 text-green-600' />
) : (
'Validate'
)}
</Button>
) : validateResult === 'invalid' &&
sendDestination.trim().length >= 56 ? (
<AlertCircle className='text-destructive h-4 w-4' />
) : null}
</div>
</div>
{validateResult === 'invalid' && validateError && (
<Alert variant='destructive' className='mt-2'>
Expand Down
22 changes: 21 additions & 1 deletion hooks/useNotifications.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@ import { useSocket } from './useSocket';
import { getNotifications } from '@/lib/api/notifications';
import { Notification } from '@/types/notifications';
import { reportError } from '@/lib/error-reporting';
import { useNotificationStore } from '@/lib/stores/notification-store';

interface UseNotificationsOptions {
page?: number;
limit?: number;
autoFetch?: boolean;
enabled?: boolean;
}

export interface UseNotificationsReturn {
Expand All @@ -32,11 +34,17 @@ export function useNotifications(
// Handle overloaded arguments
const userId = typeof input === 'string' ? input : undefined;
const options = typeof input === 'object' ? input : {};
const { page: initialPage = 1, limit = 10, autoFetch = true } = options;
const {
page: initialPage = 1,
limit = 10,
autoFetch = true,
enabled = true,
} = options;

const { socket, isConnected } = useSocket({
namespace: '/notifications',
userId,
autoConnect: enabled && autoFetch,
});

const [notifications, setNotifications] = useState<Notification[]>([]);
Expand All @@ -45,6 +53,16 @@ export function useNotifications(
const [error, setError] = useState<Error | null>(null);
const [total, setTotal] = useState(0);
const [currentPage, setCurrentPage] = useState(initialPage);
const [hasFetched, setHasFetched] = useState(false);

const { setUnreadCount: setGlobalUnreadCount } = useNotificationStore();

// Sync with global store
useEffect(() => {
if (hasFetched) {
setGlobalUnreadCount(unreadCount);
}
}, [unreadCount, setGlobalUnreadCount, hasFetched]);

// Merge server list with current state: dedupe by id, preserve optimistic read state (short rollback path)
const mergeNotifications = useCallback(
Expand Down Expand Up @@ -79,6 +97,7 @@ export function useNotifications(
mergeNotifications(prev, response.notifications)
);
setTotal(response.total || 0);
setHasFetched(true);
}
} catch (err) {
reportError(err, { context: 'notifications-fetch' });
Expand Down Expand Up @@ -142,6 +161,7 @@ export function useNotifications(
// Listen for unread count updates
const handleUnreadCount = (data: { count: number }) => {
setUnreadCount(data.count);
setHasFetched(true);
};

// Listen for notification updates
Expand Down
Loading