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
425 changes: 351 additions & 74 deletions components/brain/my-stream/MyStreamWaveDesktopTabs.tsx

Large diffs are not rendered by default.

150 changes: 150 additions & 0 deletions components/brain/my-stream/tabs/MyStreamWaveCurationTabMenu.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
"use client";

import { useState } from "react";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { EllipsisVerticalIcon } from "@heroicons/react/24/outline";
import { CompactMenu, type CompactMenuItem } from "@/components/compact-menu";
import { useAuth } from "@/components/auth/Auth";
import CommonConfirmationModal from "@/components/utils/modal/CommonConfirmationModal";
import type { ApiWave } from "@/generated/models/ApiWave";
import type { ApiWaveCuration } from "@/generated/models/ApiWaveCuration";
import type { DropCurationMembership } from "@/hooks/drops/useDropCurations";
import { invalidateProfileWaveQueries } from "@/hooks/useProfileWave";
import { getWaveCurationsQueryKey } from "@/hooks/waves/useWaveCurations";
import { useProfileWaveMutation } from "@/hooks/useProfileWaveMutation";
import { commonApiDelete } from "@/services/api/common-api";
import MyStreamWaveCurationCreateDialog from "./MyStreamWaveCurationCreateDialog";

interface MyStreamWaveCurationTabMenuProps {
readonly wave: ApiWave;
readonly curation: ApiWaveCuration;
readonly onDeleted?: (() => void) | undefined;
readonly canSetAsProfileCuration?: boolean | undefined;
readonly isSetAsProfileCurationPending?: boolean | undefined;
}

const getErrorMessage = (error: unknown): string =>
error instanceof Error ? error.message : "Failed to delete curation.";

export default function MyStreamWaveCurationTabMenu({
wave,
curation,
onDeleted,
canSetAsProfileCuration = false,
isSetAsProfileCurationPending = false,
}: MyStreamWaveCurationTabMenuProps) {
const queryClient = useQueryClient();
const { connectedProfile, requestAuth, setToast } = useAuth();
const { updateProfileWave, isPending: isProfileWavePending } =
useProfileWaveMutation(connectedProfile);
const [isEditOpen, setIsEditOpen] = useState(false);
const [isDeleteOpen, setIsDeleteOpen] = useState(false);
const isSettingProfileCuration =
isSetAsProfileCurationPending || isProfileWavePending;

const deleteMutation = useMutation({
mutationFn: async () => {
const auth = await requestAuth();
if (!auth.success) {
throw new Error("Authentication was cancelled.");
}

await commonApiDelete({
endpoint: `waves/${wave.id}/curations/${curation.id}`,
});
},
onSuccess: async () => {
queryClient.setQueryData<ApiWaveCuration[]>(
getWaveCurationsQueryKey(wave.id),
(current) => current?.filter((item) => item.id !== curation.id)
);
queryClient.setQueriesData<DropCurationMembership[]>(
{ queryKey: ["drop-curations"] },
(current) => current?.filter((item) => item.id !== curation.id)
);
await queryClient.invalidateQueries({
queryKey: getWaveCurationsQueryKey(wave.id),
});
await queryClient.invalidateQueries({
queryKey: ["drop-curations"],
});
Comment thread
ragnep marked this conversation as resolved.
await invalidateProfileWaveQueries(queryClient, [
connectedProfile,
wave.author,
]);
setToast({
type: "success",
message: "Curation deleted.",
});
setIsDeleteOpen(false);
onDeleted?.();
},
onError: (error) => {
setToast({
type: "error",
message: getErrorMessage(error),
});
},
});

const menuItems: CompactMenuItem[] = [
{
id: "edit",
label: "Edit curation",
onSelect: () => setIsEditOpen(true),
},
...(canSetAsProfileCuration
? [
{
id: "set-profile-curation",
label: "Set as profile curation",
onSelect: () => {
void updateProfileWave(wave.id, curation.id);
},
disabled: isSettingProfileCuration,
},
]
: []),
{
id: "delete",
label: "Delete curation",
onSelect: () => setIsDeleteOpen(true),
className: "tw-text-red desktop-hover:hover:tw-text-red",
},
];

return (
<>
<CompactMenu
triggerClassName="tw-inline-flex tw-h-8 tw-w-4 tw-flex-shrink-0 tw-items-center tw-justify-center tw-border-0 tw-bg-transparent tw-text-iron-400 tw-transition hover:tw-text-iron-300 disabled:tw-cursor-not-allowed disabled:tw-opacity-40"
trigger={
<EllipsisVerticalIcon className="tw-mt-0.5 tw-block tw-size-4 tw-flex-shrink-0" />
}
aria-label="Curation options"
items={menuItems}
menuWidthClassName="tw-w-52"
disabled={deleteMutation.isPending || isSettingProfileCuration}
/>

{isEditOpen && (
<MyStreamWaveCurationCreateDialog
wave={wave}
isOpen={isEditOpen}
onClose={() => setIsEditOpen(false)}
onSaved={() => undefined}
curation={curation}
/>
)}

<CommonConfirmationModal
isOpen={isDeleteOpen}
onClose={() => setIsDeleteOpen(false)}
onConfirm={() => deleteMutation.mutate()}
title="Delete curation"
message={`Delete "${curation.name}" from this wave?`}
confirmText="Delete"
isConfirming={deleteMutation.isPending}
/>
</>
);
}
88 changes: 64 additions & 24 deletions components/common/TabToggle.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@ import React from "react";
interface TabOption {
readonly key: string;
readonly label: string;
readonly leadingIcon?: React.ReactNode | undefined;
readonly hasIndicator?: boolean | undefined;
readonly panelId: string;
readonly action?: React.ReactNode | undefined;
}

interface TabToggleProps {
Expand All @@ -20,34 +22,72 @@ export const TabToggle: React.FC<TabToggleProps> = ({
onSelect,
fullWidth = false, // Default to false for backwards compatibility
}) => {
const hasActions = options.some(
(option) => option.action !== undefined && option.action !== null
);

const renderTabButton = (option: TabOption, style?: React.CSSProperties) => (
<button
key={option.key}
onClick={() => onSelect(option.key)}
role="tab"
aria-selected={activeKey === option.key}
aria-controls={option.panelId}
style={style}
className={`tw-relative tw-whitespace-nowrap tw-border-x-0 tw-border-b-2 tw-border-t-0 tw-border-solid tw-bg-transparent tw-py-3 tw-text-sm tw-font-medium tw-transition-all tw-duration-200 ${
fullWidth ? "tw-flex tw-flex-1 tw-justify-center tw-text-center" : ""
} ${
activeKey === option.key
? "tw-border-primary-300 tw-text-white"
: "tw-border-transparent tw-text-iron-500 desktop-hover:hover:tw-text-iron-200"
}`}
>
<span className="tw-inline-flex tw-h-5 tw-items-center tw-gap-1 tw-align-middle tw-leading-5">
{option.leadingIcon}
<span className="tw-leading-5">{option.label}</span>
</span>
{option.hasIndicator && (
<div className="tw-absolute -tw-right-1 tw-top-1 tw-h-2 tw-w-2 tw-rounded-full tw-bg-red"></div>
)}
</button>
);

if (!hasActions) {
return (
<div
className={`tw-flex tw-gap-x-1 ${fullWidth ? "tw-w-full" : "tw-w-auto"}`}
role="tablist"
>
{options.map((option) => renderTabButton(option))}
</div>
);
}

return (
<div
className={`tw-flex tw-gap-x-1 ${fullWidth ? "tw-w-full" : "tw-w-auto"}`}
role="tablist"
>
{options.map((option) => (
<button
key={option.key}
onClick={() => onSelect(option.key)}
role="tab"
aria-selected={activeKey === option.key}
aria-controls={option.panelId}
className={`tw-relative tw-whitespace-nowrap tw-border-x-0 tw-border-b-2 tw-border-t-0 tw-border-solid tw-bg-transparent tw-py-3 tw-text-sm tw-font-medium tw-transition-all tw-duration-200 ${
fullWidth
? "tw-flex tw-flex-1 tw-justify-center tw-text-center"
: ""
} ${
activeKey === option.key
? "tw-border-primary-300 tw-text-white"
: "tw-border-transparent tw-text-iron-500 desktop-hover:hover:tw-text-iron-200"
}`}
>
{option.label}
{option.hasIndicator && (
<div className="tw-absolute -tw-right-1 tw-top-1 tw-h-2 tw-w-2 tw-rounded-full tw-bg-red"></div>
)}
</button>
))}
<div className="tw-contents" role="tablist">
{options.map((option, index) =>
renderTabButton(option, { order: index * 2 })
)}
</div>
{options.map((option, index) => {
if (option.action === undefined || option.action === null) {
return null;
}

const actionOrder = index * 2 + 1;
return (
<div
key={`${option.key}:action`}
style={{ order: actionOrder }}
className="tw-border-x-0 tw-border-b-2 tw-border-t-0 tw-border-solid tw-border-transparent"
>
{option.action}
</div>
);
})}
</div>
);
Comment thread
ragnep marked this conversation as resolved.
};
Loading
Loading