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

feat: rewards #1579

Merged
merged 25 commits into from
Dec 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
e7c876e
feat(RewardCard): change layout with container queries
BrickheadJohnny Dec 6, 2024
6ca5366
chore: update schemas
BrickheadJohnny Dec 6, 2024
6fda0da
feat: custom reward cards for permissions and points
BrickheadJohnny Dec 6, 2024
fd62fb6
feat: fetch real rewards data and add a simple reward leaderboard
BrickheadJohnny Dec 6, 2024
f9bd2b9
Merge branch 'add-guild-page' into rewards
BrickheadJohnny Dec 6, 2024
1476c42
fix: remove points name for now
BrickheadJohnny Dec 6, 2024
8ff689c
Merge branch 'add-guild-page' into rewards
BrickheadJohnny Dec 9, 2024
227c405
feat: separate join and leave guild components
BrickheadJohnny Dec 10, 2024
4c7f21c
feat: useGuild hook
BrickheadJohnny Dec 10, 2024
81d3c38
fix: add missing use client directives
BrickheadJohnny Dec 10, 2024
da7220f
feat: auth rework (#1581)
BrickheadJohnny Dec 10, 2024
4ed0ffc
feat: third party auth (#1582)
BrickheadJohnny Dec 10, 2024
679da40
Merge branch 'add-guild-page' into rewards
BrickheadJohnny Dec 11, 2024
e01371f
fix: invalidate queries when signing in/out
BrickheadJohnny Dec 11, 2024
bbe0179
feat: join modal
BrickheadJohnny Dec 11, 2024
c4292ab
feat: DiscordRewardCard
BrickheadJohnny Dec 11, 2024
0cbbfdc
cleanup(RoleCard): remove unnecessary TODO comments
BrickheadJohnny Dec 12, 2024
3686ffe
feat: better identity-related types
BrickheadJohnny Dec 12, 2024
f34335b
fix(DiscordRewardCard): disabled state
BrickheadJohnny Dec 12, 2024
ac22552
fix(ConnectResultToast): show toast only after platform connection
BrickheadJohnny Dec 12, 2024
ee5cf51
fix(PointsRewardCard): don't use pathname for navigation
BrickheadJohnny Dec 12, 2024
f3a4c6c
fix(rewardCards): add proper types
BrickheadJohnny Dec 12, 2024
8c4d465
feat(leaderboard): display points' name
BrickheadJohnny Dec 12, 2024
a5c591b
feat(leaderboard): display user addresses & "Your position" section
BrickheadJohnny Dec 12, 2024
a84095d
feat(DiscordRewardCard): more detailed tooltip message
BrickheadJohnny Dec 12, 2024
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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,5 @@ bun.lockb
/playwright/.cache/
/playwright/.auth/
/playwright/results

certificates
7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,11 @@ Open source interface for Guild.xyz -- a tool for platformless membership manage
### Running the interface locally

1. `bun i`
2. `bun run dev`
3. If you don't have the secret environment variables, copy the `.env.example` as `.env.local`.
2. Append `127.0.0.1 local.openguild.xyz` to `/etc/hosts`
3. If you don't have the secret environment variables, copy the `.env.example` as `.env.local`
4. Run `bun dev`, create certificate if prompted
5. Open `https://local.openguild.xyz:3000` and dismiss the unsecure site warning

Open [http://localhost:3000](http://localhost:3000) in your browser to see the result.

### Getting secret environment variables (for core team members):

Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"private": true,
"scripts": {
"prepare": "husky",
"dev": "next dev --turbo",
"dev": "next dev --turbo --experimental-https",
"build": "next build",
"start": "next start",
"type-check": "tsc --pretty --noEmit --incremental false",
Expand All @@ -31,6 +31,7 @@
"@radix-ui/react-toggle-group": "^1.1.0",
"@radix-ui/react-tooltip": "^1.1.4",
"@t3-oss/env-nextjs": "^0.11.1",
"@tailwindcss/container-queries": "^0.1.1",
"@tailwindcss/typography": "^0.5.15",
"@tanstack/react-query": "^5.62.2",
"@tanstack/react-query-devtools": "^5.62.2",
Expand All @@ -40,7 +41,6 @@
"event-source-plus": "^0.1.8",
"foxact": "^0.2.43",
"jotai": "^2.10.3",
"jwt-decode": "^4.0.0",
"mini-svg-data-uri": "^1.4.4",
"next": "15.0.3",
"next-themes": "^0.4.4",
Expand Down
52 changes: 0 additions & 52 deletions src/actions/auth.ts

This file was deleted.

158 changes: 93 additions & 65 deletions src/app/(dashboard)/[guildUrlName]/[pageUrlName]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
"use client";

import { RequirementDisplayComponent } from "@/components/requirements/RequirementDisplayComponent";
import { rewardCards } from "@/components/rewards/rewardCards";
import { Button } from "@/components/ui/Button";
import { Card } from "@/components/ui/Card";
import { ScrollArea } from "@/components/ui/ScrollArea";
import { Skeleton } from "@/components/ui/Skeleton";
import { rewardBatchOptions, roleBatchOptions } from "@/lib/options";
import type { Schemas } from "@guildxyz/types";
import { Lock } from "@phosphor-icons/react/dist/ssr";
import { fetchGuildApiData } from "@/lib/fetchGuildApi";
import { roleBatchOptions } from "@/lib/options";
import type { GuildReward, GuildRewardType } from "@/lib/schemas/guildReward";
import type { Role } from "@/lib/schemas/role";
import { ImageSquare, Lock } from "@phosphor-icons/react/dist/ssr";
import { useSuspenseQuery } from "@tanstack/react-query";
import { useParams } from "next/navigation";
import { Suspense } from "react";
import { useGuild } from "../hooks/useGuild";

const GuildPage = () => {
const { pageUrlName, guildUrlName } = useParams<{
Expand Down Expand Up @@ -38,73 +41,98 @@ const GuildPage = () => {
);
};

const RoleCard = ({ role }: { role: Schemas["Role"] }) => {
const { data: rewards } = useSuspenseQuery(
rewardBatchOptions({ roleId: role.id }),
);

return (
<Card className="flex flex-col md:flex-row" key={role.id}>
<div className="border-r p-6 md:w-1/2">
<div className="flex items-center gap-3">
{role.imageUrl && (
<img
className="size-14 rounded-full border"
src={role.imageUrl} // TODO: fallback image
alt="role avatar"
/>
)}
<h3 className="font-bold text-xl tracking-tight">{role.name}</h3>
</div>
<p className="mt-4 text-foreground-dimmed leading-relaxed">
{role.description}
</p>
{!!rewards.length && (
<ScrollArea className="mt-8 h-64 rounded-lg border pr-3">
<div className="flex flex-col gap-4">
{rewards.map((reward) => (
<Reward reward={reward} key={reward.id} />
))}
</div>
</ScrollArea>
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">
<ImageSquare weight="duotone" className="size-6" />
</div>
)}
<h3 className="font-extrabold text-xl">{role.name}</h3>
</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>
<Button size="sm">
<Lock />
Join Guild to collect rewards
</Button>
</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>

{/* TODO group rules by access groups */}
<div className="grid px-5 pb-5">
{role.accessGroups[0].rules?.map((rule) => (
<RequirementDisplayComponent
key={rule.accessRuleId}
// @ts-expect-error: incomplete type
requirement={rule}
/>
))}
</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>
<Button size="sm">
<Lock />
Join Guild to collect rewards
</Button>
</div>
</Card>
);
};

const Reward = ({ reward }: { reward: Schemas["Reward"] }) => {
return (
<div className="border-b p-4 last:border-b-0">
<div className="mb-2 font-medium">{reward.name}</div>
<div className="text-foreground-dimmed text-sm">{reward.description}</div>
<pre className="mt-3 text-foreground-secondary text-xs">
<code>{JSON.stringify(reward.permissions, null, 2)}</code>
</pre>
{/* 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>
</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;
};

export default GuildPage;
29 changes: 29 additions & 0 deletions src/app/(dashboard)/[guildUrlName]/components/ActionButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
"use client";

import { Button } from "@/components/ui/Button";
import { userOptions } from "@/lib/options";
import { useQuery } from "@tanstack/react-query";
import { useGuild } from "../hooks/useGuild";
import { JoinGuild } from "./JoinGuild";
import { LeaveGuild } from "./LeaveGuild";

export const ActionButton = () => {
const user = useQuery(userOptions());
const guild = useGuild();

if (!guild.data) {
throw new Error("Failed to fetch guild");
}

const isJoined = !!user.data?.guilds?.some(
({ guildId }) => guildId === guild.data.id,
);

return isJoined ? <LeaveGuild /> : <JoinGuild />;
};

export const ActionButtonSkeleton = () => (
<Button isLoading loadingText="Loading">
Join guild
</Button>
);
11 changes: 5 additions & 6 deletions src/app/(dashboard)/[guildUrlName]/components/GuildTabs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,15 @@ import { Card } from "@/components/ui/Card";
import { ScrollArea, ScrollBar } from "@/components/ui/ScrollArea";
import { Skeleton } from "@/components/ui/Skeleton";
import { cn } from "@/lib/cssUtils";
import { guildOptions, pageBatchOptions } from "@/lib/options";
import { pageBatchOptions } from "@/lib/options";
import { useSuspenseQuery } from "@tanstack/react-query";
import { useParams } from "next/navigation";
import { useGuild } from "../hooks/useGuild";
import { useGuildUrlName } from "../hooks/useGuildUrlName";
import { PageNavLink } from "./RoleGroupNavLink";

export const GuildTabs = () => {
const { guildUrlName } = useParams<{ guildUrlName: string }>();
const { data: guild } = useSuspenseQuery(
guildOptions({ guildIdLike: guildUrlName }),
);
const guildUrlName = useGuildUrlName();
const { data: guild } = useGuild();
const { data: pages } = useSuspenseQuery(
pageBatchOptions({ guildIdLike: guildUrlName }),
);
Expand Down
Loading
Loading