Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

introduce monoview fetchers #1591

Draft
wants to merge 12 commits into
base: add-guild-page
Choose a base branch
from
200 changes: 16 additions & 184 deletions src/app/(dashboard)/[guildUrlName]/[pageUrlName]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,189 +1,21 @@
"use client";

import { RequirementDisplayComponent } from "@/components/requirements/RequirementDisplayComponent";
import { rewardCards } from "@/components/rewards/rewardCards";
import { Badge } from "@/components/ui/Badge";
import { Button, buttonVariants } from "@/components/ui/Button";
import { Card } from "@/components/ui/Card";
import { Skeleton } from "@/components/ui/Skeleton";
import { useUser } from "@/hooks/useUser";
import { cn } from "@/lib/cssUtils";
import { fetchGuildApiData } from "@/lib/fetchGuildApi";
import type { GuildReward, GuildRewardType } from "@/lib/schemas/guildReward";
import type { Role } from "@/lib/schemas/role";
import { Check, ImageSquare, LockSimple } from "@phosphor-icons/react/dist/ssr";
import { useSuspenseQuery } from "@tanstack/react-query";
import { useSetAtom } from "jotai";
import { Suspense } from "react";
import { joinModalAtom } from "../atoms";
import { useGuild } from "../hooks/useGuild";
import { useSuspenseRoles } from "../hooks/useSuspenseRoles";

const GuildPage = () => {
const { data: roles } = useSuspenseRoles();

return (
<div className="my-4 space-y-4">
{roles.map((role) => (
<Suspense
fallback={<Skeleton className="h-40 w-full rounded-xl" />}
key={role.id}
>
<RoleCard role={role} />
</Suspense>
))}
</div>
import { getQueryClient } from "@/lib/getQueryClient";
import { pageMonoviewOptions } from "@/lib/options";
import type { DynamicRoute } from "@/lib/types";
import { Roles } from "../components/Roles";

const GuildPage = async ({
params,
}: DynamicRoute<{ guildUrlName: string; pageUrlName: string }>) => {
const { guildUrlName, pageUrlName } = await params;
const queryClient = getQueryClient();
await queryClient.prefetchQuery(
pageMonoviewOptions({
guildIdLike: guildUrlName,
pageIdLike: pageUrlName,
}),
);
};

const RoleCard = ({ role }: { role: Role }) => (
<Card className="flex flex-col md:flex-row" key={role.id}>
<div className="@container flex flex-col border-r p-5 md:w-1/2">
<div className="mb-2 flex items-center gap-3">
{role.imageUrl ? (
<img
className="size-14 rounded-full border"
src={role.imageUrl}
alt="role avatar"
/>
) : (
<div className="flex size-14 items-center justify-center rounded-full bg-image text-white">
<ImageSquare weight="duotone" className="size-6" />
</div>
)}
<h3 className="font-extrabold text-xl">{role.name}</h3>
</div>
<p className="mb-4 text-foreground-dimmed leading-relaxed">
{role.description}
</p>

<Suspense fallback={<p>Loading rewards...</p>}>
<RoleRewards roleId={role.id} roleRewards={role.rewards} />
</Suspense>
</div>

<div className="bg-card-secondary md:w-1/2">
<div className="flex items-center justify-between p-5">
<span className="font-bold text-foreground-secondary text-xs">
REQUIREMENTS
</span>

<AccessIndicator roleId={role.id} className="hidden sm:flex" />
</div>

{/* TODO group rules by access groups */}
<div className="grid px-5 pb-5">
{role.accessGroups[0].rules?.map((rule) => (
<RequirementDisplayComponent
key={rule.accessRuleId}
requirement={rule}
/>
))}
</div>

<AccessIndicator roleId={role.id} className="sm:hidden" />
</div>
</Card>
);

const RoleRewards = ({
roleId,
roleRewards,
}: { roleId: string; roleRewards: Role["rewards"] }) => {
const { data: guild } = useGuild();
const { data: rewards } = useSuspenseQuery<GuildReward[]>({
queryKey: ["reward", "search", guild.id],
queryFn: () =>
fetchGuildApiData<{ items: GuildReward[] }>(
`reward/search?customQuery=@guildId:{${guild.id}}`,
).then((data) => data.items), // TODO: we shouldn't do this, we should just get back an array on this endpoint in my opinion
});

return roleRewards?.length > 0 && rewards?.length > 0 ? (
<div className="mt-auto grid @[26rem]:grid-cols-2 gap-2">
{roleRewards.map((roleReward) => {
const guildReward = rewards.find((gr) => gr.id === roleReward.rewardId);
if (!guildReward) return null;

const hasRewardCard = (
rewardType: GuildRewardType,
): rewardType is keyof typeof rewardCards => rewardType in rewardCards;

const RewardCard = hasRewardCard(guildReward.type)
? rewardCards[guildReward.type]
: null;

if (!RewardCard) return null;

return (
<RewardCard
key={roleReward.rewardId}
roleId={roleId}
reward={{
guildReward,
roleReward,
}}
/>
);
})}
</div>
) : null;
};

// TODO: handle state during join & error/no access states too
const ACCESS_INDICATOR_CLASS =
"rounded-b-2xl rounded-t-none min-w-full justify-between sm:rounded-b-xl sm:rounded-t-xl sm:min-w-max";
const AccessIndicator = ({
roleId,
className,
}: { roleId: Role["id"]; className?: string }) => {
const { data: guild } = useGuild();

const { data: user } = useUser();
const isGuildMember = user?.guilds?.find((g) => g.guildId === guild.id);
const isRoleMember = !!user?.guilds
?.flatMap((g) => g.roles)
?.find((r) => r?.roleId === roleId);

const onJoinModalOpenChange = useSetAtom(joinModalAtom);

if (!isGuildMember)
return (
<Button
size="sm"
leftIcon={<LockSimple weight="bold" />}
className={cn(ACCESS_INDICATOR_CLASS, className)}
onClick={() => onJoinModalOpenChange(true)}
>
Join guild to collect rewards
</Button>
);

if (!isRoleMember)
return (
<Button
size="sm"
leftIcon={<LockSimple weight="bold" />}
className={cn(ACCESS_INDICATOR_CLASS, className)}
onClick={() => onJoinModalOpenChange(true)}
>
Check access to collect rewards
</Button>
);

return (
<Badge
className={buttonVariants({
size: "sm",
colorScheme: "success",
variant: "subtle",
className: [ACCESS_INDICATOR_CLASS, className, "pointer-events-none"],
})}
>
<Check weight="bold" />
<span>You have access</span>
</Badge>
);
return <Roles />;
};

export default GuildPage;
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { ScrollArea, ScrollBar } from "@/components/ui/ScrollArea";
import { Skeleton } from "@/components/ui/Skeleton";
import { cn } from "@/lib/cssUtils";
import { useGuild } from "../hooks/useGuild";
import { useSuspensePages } from "../hooks/useSuspensePages";
import { useSuspensePages } from "../hooks/usePages";
import { PageNavLink } from "./RoleGroupNavLink";

export const GuildTabs = () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import { type ReactNode, useEffect } from "react";
import { toast } from "sonner";
import { joinModalAtom } from "../atoms";
import { useGuild } from "../hooks/useGuild";
import { useSuspenseRoles } from "../hooks/useSuspenseRoles";
import { useSuspenseRoles } from "../hooks/useRoles";

const JOIN_MODAL_SEARCH_PARAM = "join";

Expand Down
Loading
Loading