Skip to content

Commit

Permalink
Add dedicated menu for plant ambassadors (#251)
Browse files Browse the repository at this point in the history
* Update data (add plant ambassadors)

* Add dedicated menu for plant ambassadors

* Fix down arrow showing when not scrollable

* Improve up/down arrow visibility checking to handle edge-cases

* Improve text wrapping + image positioning

* Match website sorting in panel/mobile view

* Don't show ambassador menus if no ambassadors
  • Loading branch information
MattIPv4 authored Mar 10, 2025
1 parent 64bb6ab commit da5f0d7
Show file tree
Hide file tree
Showing 9 changed files with 111 additions and 46 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
"types": "tsc --noEmit"
},
"dependencies": {
"@alveusgg/data": "0.54.1",
"@alveusgg/data": "0.55.0",
"@headlessui/react": "^2.2.0",
"react": "^19.0.0",
"react-canvas-confetti": "^2.0.7",
Expand Down
10 changes: 5 additions & 5 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions src/components/AmbassadorButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,8 @@ export default function AmbassadorButton(props: AmbassadorButtonProps) {
/>

<div className="my-auto px-1 pt-2 pb-2">
<h2 className="text-sm">{ambassador.name}</h2>
<h3 className="text-xs text-alveus-green-200">
<h2 className="text-sm text-balance">{ambassador.name}</h2>
<h3 className="text-xs text-balance text-alveus-green-200">
{ambassador.species.name}
</h3>
</div>
Expand Down
13 changes: 10 additions & 3 deletions src/components/AmbassadorCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,16 @@ import partyHat from "../assets/party.svg";

const headingClass = "text-base text-alveus-green-400";

const offsetPosition = (position?: string) => {
const [x, y] = (position || "50% 50%").split(" ");
return `${x} min(calc(${y} + 1.5rem), 0%)`;
const offsetPosition = (position?: `${number}% ${number}%`) => {
const [x, y] = (position || "50% 50%").split(" ") as [
`${number}%`,
`${number}%`,
];

const yPct = Number(y.replace("%", ""));
if (yPct <= 50) return `${x} min(calc(${y} + 1.5rem), 0%)`;

return `${x} ${y}`;
};

const stringifyLifespan = (value: number | { min: number; max: number }) => {
Expand Down
13 changes: 13 additions & 0 deletions src/components/icons/IconPlant.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { BaseIcon, type IconProps } from "./BaseIcon";

// This SVG code is derived from FontAwesome (https://fontawesome.com/icons/seedling)
export default function IconPlant(props: IconProps) {
return (
<BaseIcon viewBox="-64 -64 640 640" {...props}>
<path
fill="currentColor"
d="M512 32c0 113.6-84.6 207.5-194.2 222c-7.1-53.4-30.6-101.6-65.3-139.3C290.8 46.3 364 0 448 0l32 0c17.7 0 32 14.3 32 32zM0 96C0 78.3 14.3 64 32 64l32 0c123.7 0 224 100.3 224 224l0 32 0 160c0 17.7-14.3 32-32 32s-32-14.3-32-32l0-160C100.3 320 0 219.7 0 96z"
/>
</BaseIcon>
);
}
16 changes: 8 additions & 8 deletions src/pages/overlay/components/Buttons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@ import Tooltip from "../../../components/Tooltip";

import { classes } from "../../../utils/classes";

type ButtonsOptions = Readonly<
{
key: string;
type: "primary" | "secondary";
icon: (props: { size: number; className?: string }) => JSX.Element;
title: string;
}[]
>;
export interface ButtonsOption {
key: string;
type: "primary" | "secondary";
icon: (props: { size: number; className?: string }) => JSX.Element;
title: string;
}

type ButtonsOptions = Readonly<ButtonsOption[]>;

interface ButtonsProps<T extends ButtonsOptions> {
options: T;
Expand Down
37 changes: 23 additions & 14 deletions src/pages/overlay/components/overlay/Ambassadors.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,19 +27,25 @@ const arrowPathClass =
"[&_path]:stroke-alveus-tan [&_path]:stroke-[0.25rem] [&_path]:[paint-order:stroke] [&_path]:transition-[stroke] [&_path]:group-hover:stroke-highlight [&_path]:group-hover:stroke-[0.375rem] [&_path]:group-focus:stroke-highlight [&_path]:group-focus:stroke-[0.375rem]";
const hiddenClass = "opacity-0 pointer-events-none";

export default function Ambassadors(props: OverlayOptionProps) {
type AmbassadorsProps = OverlayOptionProps & { plants?: boolean };

export default function Ambassadors(props: AmbassadorsProps) {
const {
context: { activeAmbassador, setActiveAmbassador },
className,
plants = false,
} = props;

const rawAmbassadors = useAmbassadors();
const ambassadors = useMemo(
() =>
typeSafeObjectEntries(rawAmbassadors ?? {}).sort(([, a], [, b]) =>
sortDate(a.arrival, b.arrival),
),
[rawAmbassadors],
typeSafeObjectEntries(rawAmbassadors ?? {})
.filter(
([, ambassador]) =>
(ambassador.species.class.name === "plantae") === plants,
)
.sort(([, a], [, b]) => sortDate(a.arrival, b.arrival)),
[rawAmbassadors, plants],
);

const upArrowRef = useRef<HTMLButtonElement>(null);
Expand Down Expand Up @@ -87,24 +93,27 @@ export default function Ambassadors(props: OverlayOptionProps) {
if (ambassadorList.current) {
if (ambassadorList.current.scrollTop === 0)
upArrowRef.current?.classList.add(...hiddenClass.split(" "));
else if (
else upArrowRef.current?.classList.remove(...hiddenClass.split(" "));

if (
ambassadorList.current.scrollTop +
ambassadorList.current.clientHeight ===
ambassadorList.current.clientHeight >=
ambassadorList.current.scrollHeight
)
downArrowRef.current?.classList.add(...hiddenClass.split(" "));
else {
upArrowRef.current?.classList.remove(...hiddenClass.split(" "));
downArrowRef.current?.classList.remove(...hiddenClass.split(" "));
}
else downArrowRef.current?.classList.remove(...hiddenClass.split(" "));
}
}, []);

// Check the arrow visibility on mount
// Sometimes browsers restore odd scroll positions
// Check the arrow visibility on mount, as browsers restore odd scroll positions
// Also, check it whenever the ambassador list changes as the list may change size
useEffect(() => {
handleArrowVisibility();
}, [handleArrowVisibility]);

// If the window is resized, check the arrow visibility again
window.addEventListener("resize", handleArrowVisibility);
return () => window.removeEventListener("resize", handleArrowVisibility);
}, [handleArrowVisibility, ambassadors]);

return (
<div
Expand Down
56 changes: 48 additions & 8 deletions src/pages/overlay/components/overlay/Overlay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,13 @@ import {
useMemo,
type SetStateAction,
type Dispatch,
type JSX,
} from "react";

import Welcome from "../../../../components/Welcome";
import IconWelcome from "../../../../components/icons/IconWelcome";
import IconAmbassadors from "../../../../components/icons/IconAmbassadors";
import IconPlant from "../../../../components/icons/IconPlant";
import IconSettings from "../../../../components/icons/IconSettings";

import { useAmbassadors } from "../../../../hooks/useAmbassadors";
Expand All @@ -25,19 +27,27 @@ import useSleeping from "../../hooks/useSleeping";
import AmbassadorsOverlay from "./Ambassadors";
import SettingsOverlay from "./Settings";

import Buttons from "../Buttons";
import Buttons, { type ButtonsOption } from "../Buttons";

// Show command-triggered popups for 10s
const commandTimeout = 10_000;

type OverlayOption = ButtonsOption & {
component: (props: OverlayOptionProps) => JSX.Element;
condition?: (props: {
ambassadors: ReturnType<typeof useAmbassadors>;
}) => boolean;
};

const overlayOptions = [
{
key: "welcome",
type: "primary",
icon: IconWelcome,
title: "Welcome to Alveus",
component: (props: OverlayOptionProps) => (
component: (props) => (
<Welcome
{...props}
className={classes("absolute top-0 left-0 mx-4 my-6", props.className)}
/>
),
Expand All @@ -46,8 +56,23 @@ const overlayOptions = [
key: "ambassadors",
type: "primary",
icon: IconAmbassadors,
title: "Explore our Ambassadors",
title: "Explore our Animal Ambassadors",
component: AmbassadorsOverlay,
condition: ({ ambassadors }) =>
Object.values(ambassadors ?? {}).some(
(a) => a.species.class.name !== "plantae",
),
},
{
key: "ambassadorPlants",
type: "primary",
icon: IconPlant,
title: "Explore our Plant Ambassadors",
component: (props) => <AmbassadorsOverlay {...props} plants />,
condition: ({ ambassadors }) =>
Object.values(ambassadors ?? {}).some(
(a) => a.species.class.name === "plantae",
),
},
{
key: "settings",
Expand All @@ -56,7 +81,7 @@ const overlayOptions = [
title: "Extension Settings",
component: SettingsOverlay,
},
] as const;
] as const satisfies OverlayOption[];

export const isValidOverlayKey = (key: string) =>
key === "" || overlayOptions.some((option) => option.key === key);
Expand Down Expand Up @@ -89,6 +114,14 @@ export default function Overlay() {
} = useSleeping();

const ambassadors = useAmbassadors();
const options = useMemo(
() =>
overlayOptions.filter(
(option) =>
!("condition" in option) || option.condition({ ambassadors }),
),
[ambassadors],
);

const [activeAmbassador, setActiveAmbassador] =
useState<ActiveAmbassadorState>({});
Expand All @@ -113,12 +146,19 @@ export default function Overlay() {
useCallback(
(command: string) => {
if (!settings.disableChatPopup.value) {
if (Object.keys(ambassadors ?? {}).includes(command))
const ambassador = ambassadors?.[command];
if (ambassador)
setActiveAmbassador({ key: command, isCommand: true });
else if (command !== "welcome") return;

// Show the card
setVisibleOption(command === "welcome" ? "welcome" : "ambassadors");
setVisibleOption(
ambassador
? ambassador.species.class.name === "plantae"
? "ambassadorPlants"
: "ambassadors"
: "welcome",
);

// Dismiss the overlay after a delay
if (timeoutRef.current) clearTimeout(timeoutRef.current);
Expand Down Expand Up @@ -206,12 +246,12 @@ export default function Overlay() {
)}
>
<Buttons
options={overlayOptions}
options={options}
onClick={setVisibleOption}
active={visibleOption}
/>
<div className="relative h-full w-full">
{overlayOptions.map((option) => (
{options.map((option) => (
<option.component
key={option.key}
context={context}
Expand Down
6 changes: 1 addition & 5 deletions src/pages/panel/components/Ambassadors.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,13 @@ import { useAmbassadors } from "../../../hooks/useAmbassadors";

import useChatCommand from "../../../hooks/useChatCommand";
import { typeSafeObjectEntries } from "../../../utils/helpers";
import { sortDate } from "../../../utils/dateManager";

import Overlay from "./Overlay";

export default function Ambassadors() {
const rawAmbassadors = useAmbassadors();
const ambassadors = useMemo(
() =>
typeSafeObjectEntries(rawAmbassadors ?? {}).sort(([, a], [, b]) =>
sortDate(a.arrival, b.arrival),
),
() => typeSafeObjectEntries(rawAmbassadors ?? {}),
[rawAmbassadors],
);

Expand Down

0 comments on commit da5f0d7

Please sign in to comment.