Skip to content
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
508243d
wip
simo6529 Jan 8, 2026
8392624
wip
simo6529 Jan 8, 2026
3ff5988
wip
simo6529 Jan 8, 2026
1e5426d
wip
simo6529 Jan 8, 2026
027fffe
Merge branch 'main' into index-redesign
simo6529 Jan 8, 2026
33b796d
wip
simo6529 Jan 9, 2026
283105f
wip
simo6529 Jan 9, 2026
165ad92
wip
simo6529 Jan 9, 2026
4fc3aa7
wip
simo6529 Jan 9, 2026
122da89
wip
simo6529 Jan 9, 2026
72197e1
wip
simo6529 Jan 9, 2026
a4c9fb5
Merge branch 'main' into index-redesign
simo6529 Jan 9, 2026
f227c4f
wip
simo6529 Jan 9, 2026
b6860f8
wip
simo6529 Jan 9, 2026
6dc7e3c
wip
simo6529 Jan 9, 2026
e06c61f
wip
simo6529 Jan 9, 2026
829c9dc
wip
simo6529 Jan 10, 2026
09e5e49
Merge branch 'main' into index-redesign
simo6529 Jan 10, 2026
5958626
wip
simo6529 Jan 12, 2026
5e3526e
wip
simo6529 Jan 12, 2026
25073bb
wip
simo6529 Jan 12, 2026
7d63b9d
wip
simo6529 Jan 12, 2026
0ba8171
wip
ragnep Jan 12, 2026
cbeadc6
Merge branch 'main' into index-redesign
ragnep Jan 12, 2026
d8461db
wip
ragnep Jan 13, 2026
ede1f58
Merge branch 'main' into index-redesign
ragnep Jan 13, 2026
8016a0d
wip
ragnep Jan 13, 2026
3dfc5a2
wip
ragnep Jan 14, 2026
38ba440
wip
ragnep Jan 14, 2026
f4202fc
wip
ragnep Jan 14, 2026
88fa05c
wip
ragnep Jan 15, 2026
0854974
wip
ragnep Jan 15, 2026
b2c7ae6
Merge branch 'main' into index-redesign
ragnep Jan 15, 2026
ff8d417
wip
ragnep Jan 15, 2026
d8bd5da
wip
ragnep Jan 16, 2026
54aa2c4
wip
simo6529 Jan 16, 2026
bdaf3d4
wip
ragnep Jan 16, 2026
1499116
wip
simo6529 Jan 16, 2026
09e60c5
wip
simo6529 Jan 16, 2026
60a07ed
wip
simo6529 Jan 16, 2026
90ccc1f
wip
ragnep Jan 16, 2026
15de448
Merge branch 'index-redesign-2' into index-redesign
simo6529 Jan 16, 2026
55f136e
wip
ragnep Jan 16, 2026
f6fad7e
Merge branch 'main' into index-redesign
simo6529 Jan 16, 2026
42cb7a5
wip
simo6529 Jan 16, 2026
0f4bb07
wip
simo6529 Jan 16, 2026
009671a
wip
simo6529 Jan 16, 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
54 changes: 35 additions & 19 deletions __tests__/hooks/useWaveDropsLeaderboard.extra.test.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,35 @@
import { renderHook, waitFor } from '@testing-library/react';
import { useWaveDropsLeaderboard, WaveDropsLeaderboardSort } from '@/hooks/useWaveDropsLeaderboard';
import { useInfiniteQuery, useQueryClient, useQuery } from '@tanstack/react-query';
import { renderHook, waitFor } from "@testing-library/react";
import {
useWaveDropsLeaderboard,
WaveDropsLeaderboardSort,
} from "@/hooks/useWaveDropsLeaderboard";
import {
useInfiniteQuery,
useQueryClient,
useQuery,
} from "@tanstack/react-query";

jest.mock('@tanstack/react-query', () => ({
jest.mock("@tanstack/react-query", () => ({
useInfiniteQuery: jest.fn(),
useQuery: jest.fn(),
useQueryClient: jest.fn(),
keepPreviousData: {},
}));
jest.mock('react-use', () => ({ useDebounce: jest.fn() }));
jest.mock('@/services/api/common-api', () => ({ commonApiFetch: jest.fn() }));
jest.mock('@/hooks/useCapacitor', () => () => ({ isCapacitor: false }));
jest.mock('@/helpers/waves/wave-drops.helpers', () => ({
generateUniqueKeys: jest.fn((a:any)=>a),
mapToExtendedDrops: jest.fn((pages:any)=> pages.flatMap((p:any)=>p.drops)),
jest.mock("react-use", () => ({ useDebounce: jest.fn() }));
jest.mock("@/services/api/common-api", () => ({ commonApiFetch: jest.fn() }));
jest.mock("@/hooks/useCapacitor", () => () => ({ isCapacitor: false }));
jest.mock("@/helpers/waves/wave-drops.helpers", () => ({
generateUniqueKeys: jest.fn((a: any) => a),
mapToExtendedDrops: jest.fn((pages: any) =>
pages.flatMap((p: any) => p.drops)
),
}));

const fetchNextPage = jest.fn();
const queryClientMock = { prefetchInfiniteQuery: jest.fn(), removeQueries: jest.fn() };
const queryClientMock = {
prefetchInfiniteQuery: jest.fn(),
removeQueries: jest.fn(),
};
(useQueryClient as jest.Mock).mockReturnValue(queryClientMock);
(useQuery as jest.Mock).mockReturnValue({});

Expand All @@ -33,29 +45,33 @@ beforeEach(() => {
});
});

describe('useWaveDropsLeaderboard extra', () => {
it('maps pages to drops', async () => {
describe("useWaveDropsLeaderboard extra", () => {
it("maps pages to drops", async () => {
(useInfiniteQuery as jest.Mock).mockReturnValue({
data: { pages: [{ wave: {}, drops: [{ id: 'a' }, { id: 'b' }] }] },
data: { pages: [{ wave: {}, drops: [{ id: "a" }, { id: "b" }] }] },
fetchNextPage,
hasNextPage: true,
isFetching: false,
isFetchingNextPage: false,
refetch: jest.fn(),
});
const { result } = renderHook(() =>
useWaveDropsLeaderboard({ waveId: '1', connectedProfileHandle: 'h' })
useWaveDropsLeaderboard({ waveId: "1" })
);
await waitFor(() => result.current.drops.length === 2);
expect(result.current.drops).toEqual([{ id: 'a' }, { id: 'b' }]);
expect(result.current.drops).toEqual([{ id: "a" }, { id: "b" }]);
expect(result.current.isFetching).toBe(false);
});

it('prefetches with correct sort', () => {
it("prefetches with correct sort", () => {
renderHook(() =>
useWaveDropsLeaderboard({ waveId: '2', connectedProfileHandle: 'h', sort: WaveDropsLeaderboardSort.CREATED_AT })
useWaveDropsLeaderboard({
waveId: "2",
sort: WaveDropsLeaderboardSort.CREATED_AT,
})
);
const call = (queryClientMock.prefetchInfiniteQuery as jest.Mock).mock.calls[0][0];
const call = (queryClientMock.prefetchInfiniteQuery as jest.Mock).mock
.calls[0][0];
expect(call.queryKey[1].sort).toBe(WaveDropsLeaderboardSort.CREATED_AT);
});
});
49 changes: 32 additions & 17 deletions __tests__/hooks/useWaveDropsLeaderboard.test.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,29 @@
import { renderHook, act } from '@testing-library/react';
import { useWaveDropsLeaderboard } from '@/hooks/useWaveDropsLeaderboard';
import { useInfiniteQuery, useQuery, useQueryClient } from '@tanstack/react-query';
import { renderHook, act } from "@testing-library/react";
import { useWaveDropsLeaderboard } from "@/hooks/useWaveDropsLeaderboard";
import {
useInfiniteQuery,
useQuery,
useQueryClient,
} from "@tanstack/react-query";

jest.mock('@tanstack/react-query', () => ({
jest.mock("@tanstack/react-query", () => ({
useInfiniteQuery: jest.fn(),
useQuery: jest.fn(),
useQueryClient: jest.fn(),
keepPreviousData: {},
}));
jest.mock('react-use', () => ({ useDebounce: jest.fn() }));
jest.mock('@/services/api/common-api', () => ({ commonApiFetch: jest.fn() }));
jest.mock('@/hooks/useCapacitor', () => () => ({ isCapacitor: false }));
jest.mock('@/helpers/waves/wave-drops.helpers', () => ({
jest.mock("react-use", () => ({ useDebounce: jest.fn() }));
jest.mock("@/services/api/common-api", () => ({ commonApiFetch: jest.fn() }));
jest.mock("@/hooks/useCapacitor", () => () => ({ isCapacitor: false }));
jest.mock("@/helpers/waves/wave-drops.helpers", () => ({
generateUniqueKeys: jest.fn((a) => a),
mapToExtendedDrops: jest.fn((pages) => pages.flatMap((p: any) => p.drops)),
}));

const queryClientMock = { prefetchInfiniteQuery: jest.fn(), removeQueries: jest.fn() };
const queryClientMock = {
prefetchInfiniteQuery: jest.fn(),
removeQueries: jest.fn(),
};
(useQueryClient as jest.Mock).mockReturnValue(queryClientMock);
(useInfiniteQuery as jest.Mock).mockReturnValue({
data: { pages: [] },
Expand All @@ -28,8 +35,8 @@ const queryClientMock = { prefetchInfiniteQuery: jest.fn(), removeQueries: jest.
});
(useQuery as jest.Mock).mockReturnValue({});

describe('useWaveDropsLeaderboard', () => {
it('calls fetchNextPage via manualFetch when more pages', async () => {
describe("useWaveDropsLeaderboard", () => {
it("calls fetchNextPage via manualFetch when more pages", async () => {
const fetchNext = jest.fn();
(useInfiniteQuery as jest.Mock).mockReturnValue({
data: { pages: [] },
Expand All @@ -39,20 +46,26 @@ describe('useWaveDropsLeaderboard', () => {
isFetchingNextPage: false,
refetch: jest.fn(),
});
const { result } = renderHook(() => useWaveDropsLeaderboard({ waveId: '1', connectedProfileHandle: 'h' }));
const { result } = renderHook(() =>
useWaveDropsLeaderboard({ waveId: "1" })
);
await act(async () => {
await result.current.manualFetch();
});
expect(fetchNext).toHaveBeenCalled();
});

it('removes queries on unmount', () => {
const { unmount } = renderHook(() => useWaveDropsLeaderboard({ waveId: '2', connectedProfileHandle: 'h' }));
it("removes queries on unmount", () => {
const { unmount } = renderHook(() =>
useWaveDropsLeaderboard({ waveId: "2" })
);
unmount();
expect(queryClientMock.removeQueries).toHaveBeenCalledWith({ queryKey: ['DROPS', { waveId: '2' }] });
expect(queryClientMock.removeQueries).toHaveBeenCalledWith({
queryKey: ["DROPS", { waveId: "2" }],
});
});

it('does not fetch next page when none left', async () => {
it("does not fetch next page when none left", async () => {
const fetchNext = jest.fn();
(useInfiniteQuery as jest.Mock).mockReturnValue({
data: { pages: [] },
Expand All @@ -62,7 +75,9 @@ describe('useWaveDropsLeaderboard', () => {
isFetchingNextPage: false,
refetch: jest.fn(),
});
const { result } = renderHook(() => useWaveDropsLeaderboard({ waveId: '3', connectedProfileHandle: 'h' }));
const { result } = renderHook(() =>
useWaveDropsLeaderboard({ waveId: "3" })
);
await act(async () => {
await result.current.manualFetch();
});
Expand Down
45 changes: 5 additions & 40 deletions app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,52 +1,17 @@
import HomePage from "@/components/home/HomePage";
import { fetchInitialActivityData } from "@/components/latest-activity/fetchInitialActivityData";
import { isMintingActive } from "@/components/meme-calendar/meme-calendar.helpers";
import { fetchInitialTokens } from "@/components/nextGen/collections/collectionParts/hooks/fetchInitialTokens";
import { getAppMetadata } from "@/components/providers/metadata";
import { publicEnv } from "@/config/env";
import type { NFTWithMemesExtendedData } from "@/entities/INFT";
import type { NextGenCollection } from "@/entities/INextgen";
import { getAppCommonHeaders } from "@/helpers/server.app.helpers";
import { commonApiFetch } from "@/services/api/common-api";
import styles from "@/styles/Home.module.scss";
import HomePageContent from "@/components/home/HomePageContent";
import type { Metadata } from "next";

export default async function Page() {
const headers = await getAppCommonHeaders();
// First, fetch featured data and activity data in parallel
const [featuredNft, featuredNextgen, initialActivityData] = await Promise.all(
[
commonApiFetch<NFTWithMemesExtendedData>({
endpoint: `memes_latest`,
headers,
}),
commonApiFetch<NextGenCollection>({
endpoint: `nextgen/featured`,
headers,
}),
fetchInitialActivityData(1, 12),
]
);

// Then fetch initial tokens for the featured NextGen collection
const initialTokens = featuredNextgen?.id
? await fetchInitialTokens(featuredNextgen.id)
: [];

export default function Page() {
return (
<main className={styles["main"]}>
<HomePage
featuredNft={featuredNft}
isMemeMintingActive={isMintingActive()}
featuredNextgen={featuredNextgen}
initialActivityData={initialActivityData}
initialTokens={initialTokens}
/>
<main className="tw-min-h-screen tw-bg-black">
<HomePageContent />
</main>
);
}

export async function generateMetadata(): Promise<Metadata> {
export function generateMetadata(): Metadata {
return getAppMetadata({
ogImage: `${publicEnv.BASE_ENDPOINT}/6529io-banner.png`,
twitterCard: "summary_large_image",
Expand Down
20 changes: 9 additions & 11 deletions components/brain/my-stream/votes/MyStreamWaveMyVotes.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
"use client";

import React, { useContext, useState, useMemo } from "react";
import React, { useState, useMemo } from "react";
import type { ApiWave } from "@/generated/models/ApiWave";
import type { ExtendedDrop } from "@/helpers/waves/drop.helpers";
import { AuthContext } from "@/components/auth/Auth";
import {
useWaveDropsLeaderboard,
WaveDropsLeaderboardSort,
Expand All @@ -23,12 +22,10 @@ const MyStreamWaveMyVotes: React.FC<MyStreamWaveMyVotesProps> = ({
wave,
onDropClick,
}) => {
const { connectedProfile } = useContext(AuthContext);
const [pausePolling, setPausePolling] = useState(false);
const { drops, fetchNextPage, hasNextPage, isFetching, isFetchingNextPage } =
useWaveDropsLeaderboard({
waveId: wave.id,
connectedProfileHandle: connectedProfile?.handle ?? null,
sort: WaveDropsLeaderboardSort.MY_REALTIME_VOTE,
pausePolling,
});
Expand Down Expand Up @@ -71,24 +68,25 @@ const MyStreamWaveMyVotes: React.FC<MyStreamWaveMyVotesProps> = ({
});
};

const intersectionElementRef = useIntersectionObserver(() => {
const intersectionElementRef = useIntersectionObserver(async () => {
if (hasNextPage && !isFetching && !isFetchingNextPage) {
fetchNextPage();
await fetchNextPage();
}
});

return (
<div
className="tw-space-y-4 lg:tw-space-y-6 tw-px-2 sm:tw-px-4 tw-overflow-y-auto tw-scrollbar-thin tw-scrollbar-thumb-iron-500 tw-scrollbar-track-iron-800 hover:tw-scrollbar-thumb-iron-300"
style={myVotesViewStyle}>
className="tw-space-y-4 tw-overflow-y-auto tw-px-2 tw-scrollbar-thin tw-scrollbar-track-iron-800 tw-scrollbar-thumb-iron-500 hover:tw-scrollbar-thumb-iron-300 sm:tw-px-4 lg:tw-space-y-6"
style={myVotesViewStyle}
>
{drops.length === 0 && !isFetching ? (
<div className="tw-mt-10">
<p className="tw-text-iron-500 tw-text-sm tw-text-center">
You haven't voted on any submissions in this wave yet.
<p className="tw-text-center tw-text-sm tw-text-iron-500">
You haven&apos;t voted on any submissions in this wave yet.
</p>
</div>
) : (
<div className="tw-space-y-4 tw-mt-4">
<div className="tw-mt-4 tw-space-y-4">
<MyStreamWaveMyVotesReset
haveDrops={!!drops.length}
selected={checkedDrops}
Expand Down
17 changes: 17 additions & 0 deletions components/home/HomePageContent.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
"use client";

import SubmissionCarousel from "./carousel/SubmissionCarousel";
import { NowMintingSection } from "./now-minting";
import { NextMintLeadingSection } from "./next-mint-leading";
import { BoostedSection } from "./boosted";

export default function HomePageContent() {
return (
<div className="tw-py-8">
<SubmissionCarousel />
<NowMintingSection />
<NextMintLeadingSection />
<BoostedSection />
</div>
);
}
77 changes: 77 additions & 0 deletions components/home/boosted/BoostedDropCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
"use client";

import { memo } from "react";
import BoostIcon from "@/components/common/icons/BoostIcon";
import ProfileAvatar, {
ProfileBadgeSize,
} from "@/components/common/profile/ProfileAvatar";
import DropListItemContentMedia from "@/components/drops/view/item/content/media/DropListItemContentMedia";
import type { ApiDrop } from "@/generated/models/ApiDrop";

interface BoostedDropCardProps {
readonly drop: ApiDrop;
readonly onClick?: () => void;
}

const BoostedDropCard = memo(({ drop, onClick }: BoostedDropCardProps) => {
const media = drop.parts[0]?.media[0];
const textContent = drop.parts[0]?.content ?? "";
const truncatedContent =
textContent.length > 120 ? `${textContent.slice(0, 120)}...` : textContent;

const author = drop.author;
const waveName = drop.wave.name;

return (
<button
type="button"
onClick={onClick}
className="tw-group tw-relative tw-flex tw-w-60 tw-flex-shrink-0 tw-cursor-pointer tw-flex-col tw-overflow-hidden tw-rounded-xl tw-border tw-border-iron-800 tw-bg-iron-950 tw-p-0 tw-text-left tw-transition-all tw-duration-200 hover:tw-border-iron-700 hover:tw-bg-iron-900"
>
{/* Boost badge */}
<div className="tw-absolute tw-right-2 tw-top-2 tw-z-10 tw-flex tw-items-center tw-gap-1 tw-rounded-lg tw-bg-black/60 tw-px-2 tw-py-1 tw-backdrop-blur-sm">
<BoostIcon className="tw-size-3.5 tw-text-amber-500" variant="filled" />
<span className="tw-text-xs tw-font-semibold tw-text-amber-400">
{drop.boosts}
</span>
</div>

{/* Content area */}
<div className="tw-aspect-[3/4] tw-w-full tw-overflow-hidden">
{media ? (
<DropListItemContentMedia
media_mime_type={media.mime_type}
media_url={media.url}
/>
) : (
<div className="tw-flex tw-size-full tw-items-center tw-justify-center tw-bg-iron-900 tw-p-4">
<p className="tw-m-0 tw-line-clamp-6 tw-text-center tw-text-sm tw-leading-relaxed tw-text-iron-300">
{truncatedContent || "View drop..."}
</p>
</div>
)}
</div>

{/* Footer */}
<div className="tw-flex tw-flex-col tw-gap-0.5 tw-border-t tw-border-iron-800 tw-bg-iron-950 tw-px-3 tw-py-2.5">
<div className="tw-flex tw-items-center tw-gap-2">
<ProfileAvatar
pfpUrl={author.pfp}
alt={author.handle ?? "User"}
size={ProfileBadgeSize.SMALL}
/>
<span className="tw-truncate tw-text-sm tw-font-medium tw-text-iron-200">
{author.handle ?? "Anonymous"}
</span>
</div>
<span className="tw-truncate tw-pl-9 tw-text-xs tw-text-iron-500">
{waveName}
</span>
</div>
</button>
);
});
Comment thread
simo6529 marked this conversation as resolved.
Outdated

BoostedDropCard.displayName = "BoostedDropCard";

export default BoostedDropCard;
Loading