Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions __tests__/components/waves/list/WaveItemDropped.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { render, screen } from '@testing-library/react';
import WaveItemDropped from '@/components/waves/list/WaveItemDropped';

jest.mock('next/link', () => ({ __esModule: true, default: ({ href, children }: any) => <a href={href}>{children}</a> }));
jest.mock('next/navigation', () => ({ useRouter: () => ({ push: jest.fn() }) }));

jest.mock('@/helpers/Helpers', () => ({ numberWithCommas: (n: number) => n.toString() }));
jest.mock('@/helpers/image.helpers', () => ({ getScaledImageUri: (u: string) => `scaled-${u}`, ImageScale: { W_AUTO_H_50: 'scale' } }));
Expand Down
1 change: 1 addition & 0 deletions codex/STATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ This table is the single source of truth for active and historical tickets. Keep
| TKT-0018 | Clean up UrlGuardHooks export | In-Progress | P1 | openai-assistant | — | 2025-10-28 |
| TKT-0019 | Make Wave card fully clickable | In-Progress | P1 | openai-assistant | — | 2025-10-27 |
| TKT-0020 | Harden CustomTooltip positioning robustness | In-Progress | P1 | openai-assistant | — | 2025-10-28 |
| TKT-0021 | Restore Discover create wave modal | In-Progress | P1 | openai-assistant | — | 2025-10-29 |

## Usage Guidelines

Expand Down
39 changes: 39 additions & 0 deletions codex/tickets/TKT-0021.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
---
created: 2025-10-29
id: TKT-0021
owner: openai-assistant
priority: P1
status: In-Progress
title: Restore Discover create wave modal
---

## Context

> The Discover page's `Create Wave` CTA should open the modal overlay, but a recent redesign causes the main content to swap to the inline creation form instead. Need to restore the modal behavior without regressing the existing direct message modal or in-app handling.

## Plan

- [ ] Reintroduce the wave modal in `Waves` when running on web while keeping app behavior intact.
- [ ] Ensure the inline view is only used where appropriate and URL query strings still toggle modal visibility.
- [ ] Verify both `Create Wave` and `Create DM` buttons behave correctly on Discover.

## Acceptance

- [ ] Clicking `Create Wave` on Discover opens the modal overlay and leaves the list view visible.
- [ ] Closing/success from the modal clears the `create` query parameter and returns to the list.
- [ ] `Create DM` button continues to open its modal.
- [ ] `npm run lint`, `npm run type-check`, and targeted tests succeed when run.

## Links

- Primary PR: _(add when available)_
- Follow-ups: _(reference additional tickets or TODO items)_

## Log

- 2025-10-29T07:06:06Z – Created ticket and triaged regression scope.
- 2025-10-29T07:08:01Z – Wired `CreateWaveModal` back into `Waves` for web, keeping inline create for app, and ran `npm run lint`.
- 2025-10-29T07:10:05Z – Confirmed `npm run type-check` and `npm run test:cov:changed` pass (pre-existing console warnings only).
- 2025-10-29T07:15:11Z – Resolved nested link warning by making the card container a focusable div with keyboard/middle-click routing and re-ran checks.
- 2025-10-29T07:48:13Z – Restored outer `<Link>` semantics with internal button replacements, updated contributor avatars to use buttons, adjusted tests, and reran lint/type-check/tests.
- 2025-10-29T08:22:29Z – Added missing `"use client"` directive to `WaveItemDropped` per hook usage guidance.
24 changes: 15 additions & 9 deletions components/waves/Waves.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { useRouter } from "next/navigation";
import { useCallback, useMemo, type JSX } from "react";
import CreateDirectMessage from "./create-dm/CreateDirectMessage";
import CreateWave from "./create-wave/CreateWave";
import CreateWaveModal from "./create-wave/CreateWaveModal";
import WavesList from "./list/WavesList";
import ConnectWallet from "@/components/common/ConnectWallet";
import { getWavesBaseRoute } from "@/helpers/navigation.helpers";
Expand Down Expand Up @@ -45,6 +46,7 @@ export default function Waves({
openDirectMessage,
close,
isApp,
isWaveModalOpen,
} = useCreateModalState();

useSetTitle(documentTitle);
Expand Down Expand Up @@ -173,21 +175,25 @@ export default function Waves({
),
};

const activeView =
isApp || viewMode === WavesViewMode.CREATE
? viewMode
: WavesViewMode.VIEW;
const activeView = isApp ? viewMode : WavesViewMode.VIEW;

return (
<div className="tailwind-scope">
{components[activeView]}

{!isApp && connectedProfile && (
<CreateDirectMessageModal
isOpen={isDirectMessageModalOpen}
onClose={handleViewReset}
profile={connectedProfile}
/>
<>
<CreateWaveModal
isOpen={isWaveModalOpen}
onClose={handleViewReset}
profile={connectedProfile}
/>
<CreateDirectMessageModal
isOpen={isDirectMessageModalOpen}
onClose={handleViewReset}
profile={connectedProfile}
/>
</>
)}
</div>
);
Expand Down
71 changes: 51 additions & 20 deletions components/waves/list/WaveItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ function CardContainer({
}: CardContainerProps) {
const className = `${CARD_BASE_CLASSES} ${
isInteractive ? CARD_INTERACTIVE_CLASSES : ""
}`;
} tw-no-underline`;

if (isInteractive && href) {
return (
Expand All @@ -69,7 +69,6 @@ function CardContainer({
prefetch={false}
className={className}
aria-label={ariaLabel}
style={{ textDecoration: "none" }}
onClick={onClick}
onKeyDown={onKeyDown}
>
Expand Down Expand Up @@ -176,6 +175,36 @@ export default function WaveItem({
"tw-text-sm tw-font-semibold tw-text-white desktop-hover:group-hover/author:tw-text-iron-400 tw-transition tw-duration-300 tw-ease-out";
const staticAuthorNameClass = "tw-text-sm tw-font-semibold tw-text-white";

const handleAuthorClick = useCallback(
(event: MouseEvent<HTMLButtonElement>) => {
if (!authorHref) {
return;
}
if (event.metaKey || event.ctrlKey) {
event.preventDefault();
event.stopPropagation();
window.open(authorHref, "_blank", "noopener,noreferrer");
return;
}
event.preventDefault();
event.stopPropagation();
router.push(authorHref);
},
[authorHref, router]
);

const handleAuthorAuxClick = useCallback(
(event: MouseEvent<HTMLButtonElement>) => {
if (!authorHref || event.button !== 1) {
return;
}
event.preventDefault();
event.stopPropagation();
window.open(authorHref, "_blank", "noopener,noreferrer");
},
[authorHref]
);

let authorSection: ReactNode;
if (!wave) {
authorSection = (
Expand All @@ -188,17 +217,24 @@ export default function WaveItem({
);
} else if (authorHref) {
authorSection = (
<Link
href={authorHref}
prefetch={false}
className={`${authorWrapperClass} tw-no-underline`}
<button
type="button"
data-wave-item-interactive="true"
onClick={handleAuthorClick}
onAuxClick={handleAuthorAuxClick}
className={`${authorWrapperClass} tw-cursor-pointer tw-no-underline tw-bg-transparent tw-border-none tw-p-0 tw-text-left`}
aria-label={
author?.handle
? `View @${author.handle}`
: "View author profile"
}
>
<div className="tw-h-6 tw-w-6 tw-flex-shrink-0">{authorAvatar}</div>
<span className={linkedAuthorNameClass}>
{author?.handle ?? userPlaceholder}
</span>
{authorLevelBadge}
</Link>
</button>
);
} else {
authorSection = (
Expand All @@ -219,6 +255,11 @@ export default function WaveItem({
}
const target = event.target as HTMLElement | null;
if (shouldSkipNavigation(target, event.currentTarget)) {
event.preventDefault();
event.stopPropagation();
return;
}
if (event.button === 1 || event.metaKey || event.ctrlKey) {
return;
}
if (!event.defaultPrevented) {
Expand Down Expand Up @@ -277,19 +318,9 @@ export default function WaveItem({
<div className="tw-absolute tw-inset-x-0 tw-bottom-0 tw-flex tw-items-end tw-justify-between tw-gap-3">
<div className="tw-flex tw-min-w-0 tw-items-end tw-px-3 tw-pb-3">
<div className="tw-min-w-0">
{waveHref ? (
<Link
href={waveHref}
prefetch={false}
className="tw-no-underline tw-text-lg tracking-tight tw-font-semibold tw-text-white desktop-hover:hover:tw-text-iron-400 tw-transition tw-duration-300 tw-ease-out tw-line-clamp-1"
>
{wave?.name ?? titlePlaceholder}
</Link>
) : (
<span className="tw-text-lg tw-font-semibold tw-text-white">
{wave?.name ?? titlePlaceholder}
</span>
)}
<span className="tw-text-lg tw-font-semibold tw-text-white desktop-hover:group-hover:tw-text-iron-400 tw-transition tw-duration-300 tw-ease-out tw-line-clamp-1">
{wave?.name ?? titlePlaceholder}
</span>
</div>
</div>
<div className="tw-hidden sm:tw-block" />
Expand Down
50 changes: 47 additions & 3 deletions components/waves/list/WaveItemDropped.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,43 @@
import Link from "next/link";
"use client";

import { MouseEvent, useCallback } from "react";
import { useRouter } from "next/navigation";
Comment thread
ragnep marked this conversation as resolved.
import { ApiWave } from "@/generated/models/ApiWave";
import { numberWithCommas } from "@/helpers/Helpers";
import { getScaledImageUri, ImageScale } from "@/helpers/image.helpers";

export default function WaveItemDropped({ wave }: { readonly wave: ApiWave }) {
const contributors = wave.contributors_overview ?? [];
const router = useRouter();

const handleContributorClick = useCallback(
(href: string) =>
(event: MouseEvent<HTMLButtonElement>) => {
if (event.metaKey || event.ctrlKey) {
event.preventDefault();
event.stopPropagation();
window.open(href, "_blank", "noopener,noreferrer");
return;
}
event.preventDefault();
event.stopPropagation();
router.push(href);
},
[router]
);

const handleContributorAuxClick = useCallback(
(href: string) =>
(event: MouseEvent<HTMLButtonElement>) => {
if (event.button !== 1) {
return;
}
event.preventDefault();
event.stopPropagation();
window.open(href, "_blank", "noopener,noreferrer");
},
[]
);

return (
<div className="tw-flex tw-items-center tw-gap-x-2 tw-min-w-0">
Expand Down Expand Up @@ -45,9 +78,20 @@ export default function WaveItemDropped({ wave }: { readonly wave: ApiWave }) {
return (
<div key={baseKey} className="tw-block tw-group/item">
{contributorHref ? (
<Link href={contributorHref} prefetch={false}>
<button
type="button"
data-wave-item-interactive="true"
onClick={handleContributorClick(contributorHref)}
onAuxClick={handleContributorAuxClick(contributorHref)}
className="tw-cursor-pointer tw-bg-transparent tw-border-none tw-p-0 tw-m-0 tw-inline-flex"
aria-label={
c.contributor_identity
? `View @${c.contributor_identity}`
: "View contributor profile"
}
>
{avatar}
</Link>
</button>
) : (
avatar
)}
Expand Down
6 changes: 5 additions & 1 deletion scripts/dev-with-fallback.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,11 @@ async function findAvailablePort() {
async function run() {
try {
const port = await findAvailablePort();
const env = { ...process.env, PORT: String(port) };
const env = {
...process.env,
__NEXT_EXPERIMENTAL_MCP_SERVER: "true",
PORT: String(port),
};
console.log(`Starting Next.js dev server on port ${port}...`);

const child = spawn(
Expand Down