Skip to content

Commit

Permalink
Rewards
Browse files Browse the repository at this point in the history
  • Loading branch information
csansoon committed Sep 26, 2024
1 parent 947f40e commit 439f5bb
Show file tree
Hide file tree
Showing 19 changed files with 2,948 additions and 4 deletions.
27 changes: 27 additions & 0 deletions apps/web/src/actions/rewards/claimRewardAction.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
'use server'

import { ClaimedReward, RewardType } from '@latitude-data/core/browser'
import { claimReward } from '@latitude-data/core/services/claimedRewards/claim'
import { z } from 'zod'

import { authProcedure } from '../procedures'

export const claimRewardAction = authProcedure
.createServerAction()
.input(
z.object({
type: z.enum(Object.values(RewardType) as [string, ...string[]]),
reference: z.string(),
}),
)
.handler(async ({ input, ctx }) => {
const workspace = ctx.workspace
const user = ctx.user
const result = await claimReward({
workspace,
user,
type: input.type as RewardType,
reference: input.reference,
})
return result.unwrap() as ClaimedReward
})
12 changes: 12 additions & 0 deletions apps/web/src/actions/rewards/fetchClaimedRewardsAction.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
'use server'

import { authProcedure } from '../procedures'
import { ClaimedRewardsRepository } from '@latitude-data/core/repositories'

export const fetchClaimedRewardsAction = authProcedure
.createServerAction()
.handler(async ({ ctx }) => {
const claimedRewardsScope = new ClaimedRewardsRepository(ctx.workspace.id)
const result = await claimedRewardsScope.findAllValidOptimistic()
return result.unwrap()
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
'use client'

import { useMemo } from 'react'

import {
ClaimedReward,
REWARD_VALUES,
RewardType,
} from '@latitude-data/core/browser'
import { Button, cn, Icon, Text } from '@latitude-data/web-ui'
import useRewards from '$/stores/rewards'

function RewardItem({
description,
type,
claimedRewards,
isLoading,
}: {
description: string
type: RewardType
claimedRewards: ClaimedReward[]
isLoading: boolean
}) {
const isClaimed = useMemo(() => {
if (isLoading || !claimedRewards) return false
return claimedRewards.some((r) => r.reward_type === type)
}, [isLoading, claimedRewards, type])

const runs = useMemo(() => `${REWARD_VALUES[type] / 1000}k`, [type])

return (
<Button variant='ghost' className='justify-start hover:bg-muted' fullWidth>
<div className='flex flex-row w-full items-center gap-4 justify-between'>
<div className='flex flex-row items-center gap-2'>
{isLoading ? (
<Icon
name='loader'
color='foregroundMuted'
className='animate-spin'
/>
) : (
<Icon
name='check'
color={isClaimed ? 'accent' : 'foregroundMuted'}
className={cn({ 'opacity-50': !isClaimed })}
/>
)}
<Text.H5M>{description}</Text.H5M>
</div>
<Text.H5M color='foregroundMuted'>+{runs} runs</Text.H5M>
</div>
</Button>
)
}

export function RewardsMenu() {
const { data: claimedRewards, isLoading } = useRewards()

return (
<div className='flex flex-col p-4 gap-2'>
<RewardItem
description='Give us a Github star'
type={RewardType.GithubStar}
claimedRewards={claimedRewards}
isLoading={isLoading}
/>
<RewardItem
description='Follow us on X or LinkedIn'
type={RewardType.Follow}
claimedRewards={claimedRewards}
isLoading={isLoading}
/>
<RewardItem
description='Post on X or LinkedIn'
type={RewardType.Post}
claimedRewards={claimedRewards}
isLoading={isLoading}
/>
<RewardItem
description='Invite a company member'
type={RewardType.Invite}
claimedRewards={claimedRewards}
isLoading={isLoading}
/>
<RewardItem
description='Refer Latitude to a friend'
type={RewardType.Referral}
claimedRewards={claimedRewards}
isLoading={isLoading}
/>
</div>
)
}
22 changes: 22 additions & 0 deletions apps/web/src/components/layouts/AppLayout/Header/Rewards/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { Button } from '@latitude-data/web-ui'
import Popover from 'node_modules/@latitude-data/web-ui/src/ds/atoms/Popover'

import { RewardsMenu } from './Menu'

export function RewardsButton() {
return (
<Popover.Root>
<Popover.Trigger asChild>
<Button size='small' >Rewards</Button>
</Popover.Trigger>
<Popover.Content
side='bottom'
sideOffset={8}
align='center'
className='bg-background rounded-md w-fill shadow-lg border border-border'
>
<RewardsMenu />
</Popover.Content>
</Popover.Root>
)
}
2 changes: 2 additions & 0 deletions apps/web/src/components/layouts/AppLayout/Header/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { Fragment } from 'react/jsx-runtime'

import AvatarDropdown from './AvatarDropdown'
import { UsageIndicator } from './UsageIndicator'
import { RewardsButton } from './Rewards'

function BreadcrumbSeparator() {
return (
Expand Down Expand Up @@ -112,6 +113,7 @@ export default function AppHeader({
<Breadcrumb showLogo breadcrumbs={breadcrumbs} />
<div className='flex flex-row items-center gap-x-6'>
<nav className='flex flex-row gap-x-4 items-center'>
<RewardsButton />
<UsageIndicator />
{navigationLinks.map((link, idx) => (
<NavLink key={idx} {...link} />
Expand Down
78 changes: 78 additions & 0 deletions apps/web/src/stores/rewards.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { useCallback } from 'react'

import { ClaimedReward } from '@latitude-data/core/browser'
import { useToast } from '@latitude-data/web-ui'
import { claimRewardAction } from '$/actions/rewards/claimRewardAction'
import { fetchClaimedRewardsAction } from '$/actions/rewards/fetchClaimedRewardsAction'
import useLatitudeAction from '$/hooks/useLatitudeAction'
import useSWR, { SWRConfiguration } from 'swr'

import useWorkspaceUsage from './workspaceUsage'

const EMPTY_ARRAY: ClaimedReward[] = []

export default function useRewards(opts?: SWRConfiguration) {
const { mutate: mutateUsage } = useWorkspaceUsage()

const { toast } = useToast()
const {
mutate,
data = EMPTY_ARRAY,
isLoading,
error: swrError,
} = useSWR<ClaimedReward[]>(
['workspaceClaimedRewards'],
useCallback(async () => {
const [data, error] = await fetchClaimedRewardsAction()
if (error) {
toast({
title: 'Error',
description: error.message,
variant: 'destructive',
})
}
if (!data) return EMPTY_ARRAY

return data
}, []),
opts,
)

const updateRewards = useCallback(
(newReward: ClaimedReward) => {
mutate(
(prevRewards) => {
if (!prevRewards) return [newReward]
return [...prevRewards, newReward]
},
{ revalidate: false },
)
},
[mutate],
)

const increaseMaxUsage = useCallback(
(increaseCount: number) => {
mutateUsage(
(prevUsage) => {
if (!prevUsage) return prevUsage
return {
...prevUsage,
max: prevUsage.max + increaseCount,
}
},
{ revalidate: false },
)
},
[mutateUsage],
)

const { execute: claimReward } = useLatitudeAction(claimRewardAction, {
onSuccess: ({ data: claimedReward }) => {
updateRewards(claimedReward)
increaseMaxUsage(claimedReward.value)
},
})

return { data, isLoading, error: swrError, claimReward }
}
32 changes: 32 additions & 0 deletions packages/core/drizzle/0060_lumpy_tenebrous.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
DO $$ BEGIN
CREATE TYPE "latitude"."reward_types" AS ENUM('github_star', 'follow', 'post', 'invite', 'referral');
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
CREATE TABLE IF NOT EXISTS "latitude"."claimed_rewards" (
"id" bigserial PRIMARY KEY NOT NULL,
"workspace_id" bigint NOT NULL,
"creator_id" text,
"reward_type" "latitude"."reward_types" NOT NULL,
"reference" text NOT NULL,
"value" bigint NOT NULL,
"accepted" boolean,
"created_at" timestamp DEFAULT now() NOT NULL,
"updated_at" timestamp DEFAULT now() NOT NULL
);
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "latitude"."claimed_rewards" ADD CONSTRAINT "claimed_rewards_workspace_id_workspaces_id_fk" FOREIGN KEY ("workspace_id") REFERENCES "latitude"."workspaces"("id") ON DELETE no action ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "latitude"."claimed_rewards" ADD CONSTRAINT "claimed_rewards_creator_id_users_id_fk" FOREIGN KEY ("creator_id") REFERENCES "latitude"."users"("id") ON DELETE set null ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
CREATE INDEX IF NOT EXISTS "claimed_rewards_workspace_id_idx" ON "latitude"."claimed_rewards" USING btree ("workspace_id");--> statement-breakpoint
CREATE UNIQUE INDEX IF NOT EXISTS "workspace_id_reward_type_unique" ON "latitude"."claimed_rewards" USING btree ("workspace_id","reward_type");
Loading

0 comments on commit 439f5bb

Please sign in to comment.