From 1e00d092033664f9c4cf3b09111d556338ee1800 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Sans=C3=B3n?= Date: Thu, 26 Sep 2024 10:54:52 +0200 Subject: [PATCH] Rewards --- .vscode/settings.json | 3 +- .../src/actions/rewards/claimRewardAction.ts | 27 + .../rewards/fetchClaimedRewardsAction.ts | 13 + .../rewards/fetchPendingRewardClaimsAction.ts | 17 + .../updateRewardClaimValidityAction.ts | 28 + .../backoffice/_components/BackofficeTabs.tsx | 37 + .../web/src/app/(admin)/backoffice/layout.tsx | 4 +- apps/web/src/app/(admin)/backoffice/page.tsx | 198 +- .../app/(admin)/backoffice/rewards/page.tsx | 197 ++ .../app/(admin)/backoffice/templates/page.tsx | 196 ++ .../Header/Rewards/Content/RewardItem.tsx | 57 + .../Content/RewardMenu/RewardConfigs.tsx | 112 + .../Content/RewardMenu/RewardMenuBase.tsx | 128 + .../Rewards/Content/RewardMenu/Step.tsx | 18 + .../Rewards/Content/RewardMenu/index.tsx | 42 + .../Header/Rewards/Content/index.tsx | 51 + .../AppLayout/Header/Rewards/index.tsx | 24 + .../AppLayout/Header/UsageIndicator/index.tsx | 10 +- .../layouts/AppLayout/Header/index.tsx | 2 + apps/web/src/services/routes.ts | 14 + apps/web/src/stores/pendingRewardClaims.ts | 48 + apps/web/src/stores/rewards.ts | 80 + packages/core/drizzle/0060_curved_brood.sql | 31 + packages/core/drizzle/meta/0060_snapshot.json | 2489 +++++++++++++++++ packages/core/drizzle/meta/_journal.json | 7 + packages/core/src/constants.ts | 16 + .../core/src/data-access/claimedRewards.ts | 30 + packages/core/src/data-access/index.ts | 1 + packages/core/src/events/handlers/index.ts | 12 + .../repositories/claimedRewardsRepository.ts | 81 + packages/core/src/repositories/index.ts | 1 + packages/core/src/schema/index.ts | 1 + .../core/src/schema/models/claimedRewards.ts | 38 + packages/core/src/schema/types.ts | 8 + .../core/src/services/claimedRewards/claim.ts | 69 + .../claimedRewards/claimNewUserReferrals.ts | 64 + .../core/src/services/claimedRewards/index.ts | 3 + .../src/services/claimedRewards/update.ts | 33 + .../core/src/services/users/createUser.ts | 3 + .../core/src/services/workspaces/usage.ts | 13 +- .../mailers/invitations/InvitationMailer.ts | 2 +- packages/web-ui/src/ds/atoms/Button/index.tsx | 39 +- .../src/ds/atoms/CircularProgress/index.tsx | 1 + packages/web-ui/src/ds/atoms/Icons/index.tsx | 10 +- packages/web-ui/tailwind.config.js | 8 +- 45 files changed, 4055 insertions(+), 211 deletions(-) create mode 100644 apps/web/src/actions/rewards/claimRewardAction.ts create mode 100644 apps/web/src/actions/rewards/fetchClaimedRewardsAction.ts create mode 100644 apps/web/src/actions/rewards/fetchPendingRewardClaimsAction.ts create mode 100644 apps/web/src/actions/rewards/updateRewardClaimValidityAction.ts create mode 100644 apps/web/src/app/(admin)/backoffice/_components/BackofficeTabs.tsx create mode 100644 apps/web/src/app/(admin)/backoffice/rewards/page.tsx create mode 100644 apps/web/src/app/(admin)/backoffice/templates/page.tsx create mode 100644 apps/web/src/components/layouts/AppLayout/Header/Rewards/Content/RewardItem.tsx create mode 100644 apps/web/src/components/layouts/AppLayout/Header/Rewards/Content/RewardMenu/RewardConfigs.tsx create mode 100644 apps/web/src/components/layouts/AppLayout/Header/Rewards/Content/RewardMenu/RewardMenuBase.tsx create mode 100644 apps/web/src/components/layouts/AppLayout/Header/Rewards/Content/RewardMenu/Step.tsx create mode 100644 apps/web/src/components/layouts/AppLayout/Header/Rewards/Content/RewardMenu/index.tsx create mode 100644 apps/web/src/components/layouts/AppLayout/Header/Rewards/Content/index.tsx create mode 100644 apps/web/src/components/layouts/AppLayout/Header/Rewards/index.tsx create mode 100644 apps/web/src/stores/pendingRewardClaims.ts create mode 100644 apps/web/src/stores/rewards.ts create mode 100644 packages/core/drizzle/0060_curved_brood.sql create mode 100644 packages/core/drizzle/meta/0060_snapshot.json create mode 100644 packages/core/src/data-access/claimedRewards.ts create mode 100644 packages/core/src/repositories/claimedRewardsRepository.ts create mode 100644 packages/core/src/schema/models/claimedRewards.ts create mode 100644 packages/core/src/services/claimedRewards/claim.ts create mode 100644 packages/core/src/services/claimedRewards/claimNewUserReferrals.ts create mode 100644 packages/core/src/services/claimedRewards/index.ts create mode 100644 packages/core/src/services/claimedRewards/update.ts diff --git a/.vscode/settings.json b/.vscode/settings.json index 44a73ec3a..a84c425cc 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -3,5 +3,6 @@ { "mode": "auto" } - ] + ], + "editor.formatOnSave": true } diff --git a/apps/web/src/actions/rewards/claimRewardAction.ts b/apps/web/src/actions/rewards/claimRewardAction.ts new file mode 100644 index 000000000..3401b8d71 --- /dev/null +++ b/apps/web/src/actions/rewards/claimRewardAction.ts @@ -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 | undefined + }) diff --git a/apps/web/src/actions/rewards/fetchClaimedRewardsAction.ts b/apps/web/src/actions/rewards/fetchClaimedRewardsAction.ts new file mode 100644 index 000000000..df5a73bac --- /dev/null +++ b/apps/web/src/actions/rewards/fetchClaimedRewardsAction.ts @@ -0,0 +1,13 @@ +'use server' + +import { ClaimedRewardsRepository } from '@latitude-data/core/repositories' + +import { authProcedure } from '../procedures' + +export const fetchClaimedRewardsAction = authProcedure + .createServerAction() + .handler(async ({ ctx }) => { + const claimedRewardsScope = new ClaimedRewardsRepository(ctx.workspace.id) + const result = await claimedRewardsScope.findAllValidOptimistic() + return result.unwrap() + }) diff --git a/apps/web/src/actions/rewards/fetchPendingRewardClaimsAction.ts b/apps/web/src/actions/rewards/fetchPendingRewardClaimsAction.ts new file mode 100644 index 000000000..5b349a383 --- /dev/null +++ b/apps/web/src/actions/rewards/fetchPendingRewardClaimsAction.ts @@ -0,0 +1,17 @@ +'use server' + +import { findAllRewardClaimsPendingToValidate } from '@latitude-data/core/data-access' +import { UnauthorizedError } from '@latitude-data/core/lib/errors' + +import { authProcedure } from '../procedures' + +export const fetchPendingRewardClaimsAction = authProcedure + .createServerAction() + .handler(async ({ ctx }) => { + if (!ctx.user.admin) { + throw new UnauthorizedError('You must be an admin to see pending claims') + } + + const result = await findAllRewardClaimsPendingToValidate() + return result.unwrap() + }) diff --git a/apps/web/src/actions/rewards/updateRewardClaimValidityAction.ts b/apps/web/src/actions/rewards/updateRewardClaimValidityAction.ts new file mode 100644 index 000000000..3a4e5a4e6 --- /dev/null +++ b/apps/web/src/actions/rewards/updateRewardClaimValidityAction.ts @@ -0,0 +1,28 @@ +'use server' + +import { ClaimedReward } from '@latitude-data/core/browser' +import { UnauthorizedError } from '@latitude-data/core/lib/errors' +import { updateRewardClaim } from '@latitude-data/core/services/claimedRewards/update' +import { z } from 'zod' + +import { authProcedure } from '../procedures' + +export const updateRewardClaimValidityAction = authProcedure + .createServerAction() + .input( + z.object({ + claimId: z.number(), + isValid: z.boolean().nullable(), + }), + ) + .handler(async ({ input, ctx }) => { + if (!ctx.user.admin) { + throw new UnauthorizedError('You must be an admin to see pending claims') + } + + const result = await updateRewardClaim({ + claimId: input.claimId, + isValid: input.isValid, + }) + return result.unwrap() as ClaimedReward | undefined + }) diff --git a/apps/web/src/app/(admin)/backoffice/_components/BackofficeTabs.tsx b/apps/web/src/app/(admin)/backoffice/_components/BackofficeTabs.tsx new file mode 100644 index 000000000..32a4e06f6 --- /dev/null +++ b/apps/web/src/app/(admin)/backoffice/_components/BackofficeTabs.tsx @@ -0,0 +1,37 @@ +'use client' + +import { ReactNode } from 'react' + +import { TabSelector } from '@latitude-data/web-ui' +import { useNavigate } from '$/hooks/useNavigate' +import { BackofficeRoutes, ROUTES } from '$/services/routes' +import { useSelectedLayoutSegment } from 'next/navigation' + +export function BackofficeTabs({ children }: { children: ReactNode }) { + const router = useNavigate() + const selected = useSelectedLayoutSegment() as BackofficeRoutes + return ( +
+
+ { + router.push(ROUTES.backoffice[value].root) + }} + /> +
+ {children} +
+ ) +} diff --git a/apps/web/src/app/(admin)/backoffice/layout.tsx b/apps/web/src/app/(admin)/backoffice/layout.tsx index d7d3a7169..769697556 100644 --- a/apps/web/src/app/(admin)/backoffice/layout.tsx +++ b/apps/web/src/app/(admin)/backoffice/layout.tsx @@ -6,6 +6,8 @@ import { getSession } from '$/services/auth/getSession' import { ROUTES } from '$/services/routes' import { redirect } from 'next/navigation' +import { BackofficeTabs } from './_components/BackofficeTabs' + export default async function AdminLayout({ children, }: { @@ -19,7 +21,7 @@ export default async function AdminLayout({ return ( - {children} + {children} ) } diff --git a/apps/web/src/app/(admin)/backoffice/page.tsx b/apps/web/src/app/(admin)/backoffice/page.tsx index 3f4ed0088..88688c070 100644 --- a/apps/web/src/app/(admin)/backoffice/page.tsx +++ b/apps/web/src/app/(admin)/backoffice/page.tsx @@ -1,196 +1,6 @@ -'use client' +import { BackofficeRoutes, ROUTES } from '$/services/routes' +import { redirect } from 'next/navigation' -import { FormEvent } from 'react' - -import { - EvaluationResultableType, - EvaluationTemplateWithCategory, -} from '@latitude-data/core/browser' -import { - Button, - FormField, - FormWrapper, - Icon, - Input, - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, - TableWithHeader, - TabSelector, - Text, - TextArea, -} from '@latitude-data/web-ui' -import useEvaluationTemplates from '$/stores/evaluationTemplates' - -import { useEvaluationConfiguration } from '../../(private)/evaluations/_components/CreateEvaluationModal' - -export default function AdminPage() { - const { data: evaluationTemplates } = useEvaluationTemplates() - - return ( -
- - -
- ) -} - -function EvaluationTemplatesTable({ - evaluationTemplates, -}: { - evaluationTemplates: EvaluationTemplateWithCategory[] -}) { - const { destroy } = useEvaluationTemplates() - if (!evaluationTemplates.length) return null - - return ( - - - - Name - Category - Description - Configuration - - - - - {evaluationTemplates.map((template) => ( - - - {template.name} - - - {template.category} - - -
-
- {template.description} -
-
-
- - - {JSON.stringify(template.configuration)} - - - - - -
- ))} -
- - } - /> - ) -} - -function NewEvaluationTemplate() { - const { create } = useEvaluationTemplates() - - const handleSubmit = (ev: FormEvent) => { - ev.preventDefault() // This line prevents the default form submission - - const formData = new FormData(ev.currentTarget) - const name = formData.get('name') as string - const description = formData.get('description') as string - const prompt = formData.get('prompt') as string - - create({ - name, - description, - prompt, - configuration, - }) - } - - const { - configuration, - handleTypeChange, - handleRangeFromChange, - handleRangeToChange, - } = useEvaluationConfiguration() - - return ( -
-
- - - - - -