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
61 changes: 61 additions & 0 deletions app/[user]/curations/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { createUserTabPage } from "@/app/[user]/_lib/userTabPageFactory";
import UserPageProfileWave from "@/components/user/waves/UserPageProfileWave";
import type { ApiIdentity } from "@/generated/models/ApiIdentity";
import {
USER_PAGE_TAB_IDS,
USER_PAGE_TAB_MAP,
} from "@/components/user/layout/userTabs.config";
import { redirect } from "next/navigation";

const getProfileTabDestination = ({
profile,
query,
}: {
readonly profile: ApiIdentity;
readonly query: Record<string, string | string[] | undefined>;
}): string => {
const searchParams = new URLSearchParams();

for (const [key, value] of Object.entries(query)) {
if (key === "curation" || value === undefined) {
continue;
}

if (Array.isArray(value)) {
for (const entry of value) {
searchParams.append(key, entry);
}
continue;
}

searchParams.append(key, value);
}

const canonicalUser = profile.handle ?? profile.primary_wallet ?? "";
const basePath = canonicalUser ? `/${encodeURIComponent(canonicalUser)}` : "/";
const queryString = searchParams.toString();

return queryString ? `${basePath}?${queryString}` : basePath;
};

function WaveTab({ profile }: { readonly profile: ApiIdentity }) {
return <UserPageProfileWave profile={profile} />;
}

const TAB_CONFIG = USER_PAGE_TAB_MAP[USER_PAGE_TAB_IDS.WAVES];

const { Page, generateMetadata } = createUserTabPage({
subroute: TAB_CONFIG.route,
metaLabel: TAB_CONFIG.metaLabel,
Tab: WaveTab,
getTabProps: async ({ profile, query }) => {
if (!profile.profile_wave_id) {
redirect(getProfileTabDestination({ profile, query }));
}

return {};
},
});

export default Page;
export { generateMetadata };
73 changes: 68 additions & 5 deletions app/[user]/waves/page.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,77 @@
import { notFound, redirect } from "next/navigation";
import { redirect } from "next/navigation";
import { getAppCommonHeaders } from "@/helpers/server.app.helpers";
import { getUserProfile } from "@/helpers/server.helpers";

export default async function WavesPage({
type UserRouteParams = { user: string };
type UserSearchParams = Record<string, string | string[] | undefined>;

const normalizeSearchParams = (
params?: UserSearchParams | URLSearchParams
): URLSearchParams => {
const normalizedParams = new URLSearchParams();

if (!params) {
return normalizedParams;
}

if (params instanceof URLSearchParams) {
for (const [key, value] of params.entries()) {
normalizedParams.append(key, value);
}
return normalizedParams;
}

for (const [key, value] of Object.entries(params)) {
if (value === undefined) {
continue;
}

if (Array.isArray(value)) {
for (const entry of value) {
normalizedParams.append(key, entry);
}
continue;
}

normalizedParams.append(key, value);
}

return normalizedParams;
};

export default async function LegacyWavesPage({
params,
searchParams,
}: {
readonly params?: Promise<{ user: string }> | undefined;
readonly params?: Promise<UserRouteParams> | undefined;
readonly searchParams?: Promise<UserSearchParams> | undefined;
}) {
const resolvedParams = params ? await params : undefined;
const resolvedSearchParams = searchParams ? await searchParams : undefined;
const user = resolvedParams?.user;

if (!user) {
notFound();
redirect("/");
}
redirect(`/${user}`);

const queryString = normalizeSearchParams(resolvedSearchParams).toString();
let basePath = `/${encodeURIComponent(user)}`;

try {
const profile = await getUserProfile({
user: user.toLowerCase(),
headers: await getAppCommonHeaders(),
});
const canonicalUser = profile.handle ?? profile.primary_wallet;

basePath = profile.profile_wave_id
? `/${encodeURIComponent(canonicalUser)}/curations`
: `/${encodeURIComponent(canonicalUser)}`;
Comment thread
ragnep marked this conversation as resolved.
} catch {
basePath = `/${encodeURIComponent(user)}`;
}

const destination = queryString ? `${basePath}?${queryString}` : basePath;

redirect(destination);
}
30 changes: 30 additions & 0 deletions components/brain/my-stream/curations/CurationEmptyState.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
"use client";

interface CurationEmptyStateProps {
readonly curationTitle: string;
readonly containerClassName?: string | undefined;
readonly description?: string | undefined;
}

const DEFAULT_CONTAINER_CLASS_NAME =
"tw-flex tw-min-h-[20rem] tw-items-center tw-justify-center tw-px-6";

const DEFAULT_DESCRIPTION =
"This tab will show the drops added to this curation.";

export default function CurationEmptyState({
curationTitle,
containerClassName = DEFAULT_CONTAINER_CLASS_NAME,
description = DEFAULT_DESCRIPTION,
}: CurationEmptyStateProps) {
return (
<div className={containerClassName}>
<div className="tw-max-w-md tw-rounded-2xl tw-border tw-border-dashed tw-border-iron-700 tw-bg-iron-950/70 tw-px-6 tw-py-8 tw-text-center">
<p className="tw-mb-2 tw-text-base tw-font-semibold tw-text-iron-100">
{curationTitle} is empty
</p>
<p className="tw-mb-0 tw-text-sm tw-text-iron-400">{description}</p>
</div>
</div>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@
import CircleLoader, {
CircleLoaderSize,
} from "@/components/distribution-plan-tool/common/CircleLoader";
import CurationEmptyState from "@/components/brain/my-stream/curations/CurationEmptyState";
import { Spinner } from "@/components/dotLoader/DotLoader";
import CommonIntersectionElement from "@/components/utils/CommonIntersectionElement";
import Drop, { DropLocation } from "@/components/waves/drops/Drop";
import type { ExtendedDrop } from "@/helpers/waves/drop.helpers";
import { useCurationManagementPermission } from "@/hooks/useCurationManagementPermission";
import { useDropCurationMembershipMutation } from "@/hooks/drops/useDropCurationMembershipMutation";
import { useDropCurations } from "@/hooks/drops/useDropCurations";
import useDeviceInfo from "@/hooks/useDeviceInfo";
import useIsTouchDevice from "@/hooks/useIsTouchDevice";
import { useWaveDrops } from "@/hooks/useWaveDrops";
Expand All @@ -21,7 +22,8 @@ interface MyStreamWaveCurationContentProps {
readonly wave: ApiWave;
readonly curationId: string;
readonly curationName?: string | null | undefined;
readonly onDropClick: (drop: ExtendedDrop) => void;
readonly onDropClick?: ((drop: ExtendedDrop) => void) | undefined;
readonly constrainToViewport?: boolean | undefined;
}

function MyStreamWaveCurationDropItem({
Expand All @@ -37,7 +39,7 @@ function MyStreamWaveCurationDropItem({
readonly nextDrop: ExtendedDrop | null;
readonly curationId: string;
readonly canManageActiveCuration: boolean;
readonly onDropClick: (drop: ExtendedDrop) => void;
readonly onDropClick?: ((drop: ExtendedDrop) => void) | undefined;
}) {
const { hasTouchScreen, isApp } = useDeviceInfo();
const isTouchDevice = useIsTouchDevice();
Expand Down Expand Up @@ -103,7 +105,8 @@ function MyStreamWaveCurationDropItem({
}}
disabled={isPending}
aria-label="Remove drop from this curation"
className="tw-absolute tw-right-7 tw-top-4 tw-z-20 tw-inline-flex tw-h-8 tw-w-8 tw-items-center tw-justify-center tw-rounded-lg tw-border tw-border-solid tw-border-rose-500/25 tw-bg-rose-500/10 tw-p-0 tw-text-rose-400 tw-shadow-[0_10px_30px_rgba(0,0,0,0.32)] tw-backdrop-blur-sm tw-transition-all tw-duration-200 tw-ease-out active:tw-bg-rose-500/15 disabled:tw-cursor-not-allowed disabled:tw-opacity-60 desktop-hover:tw-pointer-events-none desktop-hover:tw-w-auto desktop-hover:tw-translate-y-1 desktop-hover:tw-gap-1.5 desktop-hover:tw-border-iron-700/80 desktop-hover:tw-bg-iron-950/90 desktop-hover:tw-px-2.5 desktop-hover:tw-text-xs desktop-hover:tw-font-medium desktop-hover:tw-text-iron-200 desktop-hover:tw-opacity-0 desktop-hover:group-hover:tw-pointer-events-auto desktop-hover:group-hover:tw-translate-y-0 desktop-hover:group-hover:tw-opacity-100 desktop-hover:hover:tw-border-iron-500 desktop-hover:hover:tw-bg-iron-900 desktop-hover:hover:tw-text-white"
title="Remove from wave"
className="tw-absolute tw-right-7 tw-top-4 tw-z-20 tw-inline-flex tw-h-8 tw-w-8 tw-items-center tw-justify-center tw-rounded-lg tw-border tw-border-solid tw-border-white/10 tw-bg-black/50 tw-p-0 tw-text-iron-400 tw-shadow-[0_10px_30px_rgba(0,0,0,0.32)] tw-backdrop-blur-sm tw-transition-all tw-duration-200 tw-ease-out hover:tw-border-rose-500/30 hover:tw-bg-rose-500/20 hover:tw-text-rose-300 active:tw-bg-rose-500/15 disabled:tw-cursor-not-allowed disabled:tw-opacity-60 desktop-hover:tw-pointer-events-none desktop-hover:tw-w-auto desktop-hover:tw-translate-y-1 desktop-hover:tw-gap-1.5 desktop-hover:tw-px-2.5 desktop-hover:tw-text-xs desktop-hover:tw-font-medium desktop-hover:tw-opacity-0 desktop-hover:group-hover:tw-pointer-events-auto desktop-hover:group-hover:tw-translate-y-0 desktop-hover:group-hover:tw-opacity-100"
>
{isPending ? (
<>
Expand All @@ -129,6 +132,7 @@ export default function MyStreamWaveCurationContent({
curationId,
curationName,
onDropClick,
constrainToViewport = true,
}: MyStreamWaveCurationContentProps) {
const { leaderboardViewStyle } = useLayout();
const { drops, fetchNextPage, hasNextPage, isFetching, isFetchingNextPage } =
Expand All @@ -137,9 +141,9 @@ export default function MyStreamWaveCurationContent({
curationId,
});
const permissionProbeDropId = drops[0]?.id ?? "";
const { data: permissionProbeCurations = [] } = useDropCurations({
dropId: permissionProbeDropId,
enabled: Boolean(permissionProbeDropId),
const canManageActiveCuration = useCurationManagementPermission({
curationId,
probeDropId: permissionProbeDropId,
});

const isInitialLoading = isFetching && drops.length === 0;
Expand All @@ -156,9 +160,6 @@ export default function MyStreamWaveCurationContent({
);

const curationTitle = curationName?.trim() ?? "Curation";
const canManageActiveCuration =
permissionProbeCurations.find((curation) => curation.id === curationId)
?.authenticated_user_can_curate ?? false;

const renderedDrops = useMemo(
() =>
Expand Down Expand Up @@ -186,20 +187,18 @@ export default function MyStreamWaveCurationContent({
);
} else if (drops.length === 0) {
content = (
<div className="tw-flex tw-flex-1 tw-items-center tw-justify-center tw-px-6">
<div className="tw-max-w-md tw-rounded-2xl tw-border tw-border-dashed tw-border-iron-700 tw-bg-iron-950/70 tw-px-6 tw-py-8 tw-text-center">
<p className="tw-mb-2 tw-text-base tw-font-semibold tw-text-iron-100">
{curationTitle} is empty
</p>
<p className="tw-mb-0 tw-text-sm tw-text-iron-400">
This tab will show the drops added to this curation.
</p>
</div>
</div>
<CurationEmptyState
curationTitle={curationTitle}
containerClassName={
constrainToViewport
? "tw-flex tw-flex-1 tw-items-center tw-justify-center tw-px-6"
: undefined
}
/>
);
} else {
content = (
<div className="tw-flex tw-min-h-0 tw-flex-1 tw-flex-col tw-py-4 md:tw-px-4">
<div className="tw-flex tw-min-h-0 tw-flex-1 tw-flex-col">
{renderedDrops}
{(hasNextPage || isFetchingNextPage) && (
<div className="tw-py-4">
Expand All @@ -220,8 +219,12 @@ export default function MyStreamWaveCurationContent({

return (
<div
className="tw-flex tw-h-full tw-min-h-0 tw-w-full tw-min-w-0 tw-flex-grow tw-flex-col tw-overflow-y-auto tw-overflow-x-hidden tw-scrollbar-thin tw-scrollbar-track-iron-800 tw-scrollbar-thumb-iron-500 desktop-hover:hover:tw-scrollbar-thumb-iron-300"
style={leaderboardViewStyle}
className={
constrainToViewport
? "tw-flex tw-h-full tw-min-h-0 tw-w-full tw-min-w-0 tw-flex-grow tw-flex-col tw-overflow-y-auto tw-overflow-x-hidden tw-scrollbar-thin tw-scrollbar-track-iron-800 tw-scrollbar-thumb-iron-500 desktop-hover:hover:tw-scrollbar-thumb-iron-300"
: "tw-flex tw-min-h-0 tw-w-full tw-min-w-0 tw-flex-col"
}
style={constrainToViewport ? leaderboardViewStyle : undefined}
>
{content}
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ export default function MyStreamWaveCreateCurationAction({

return (
<>
<div className="tw-flex tw-flex-shrink-0 tw-items-center tw-gap-2 tw-pr-2 sm:tw-pr-4">
<div className="tw-flex tw-flex-shrink-0 tw-items-center tw-gap-2">
{showCreateFirstCurationCallout ? (
<button
type="button"
Expand Down
Loading
Loading