From b0147bcccf886cb31ca1face32157a6424e53329 Mon Sep 17 00:00:00 2001 From: Miguel Campos Date: Mon, 15 Jan 2024 00:33:50 -0800 Subject: [PATCH] wip: ui placeholders for tokens, referrals, and friends list --- .eslintignore | 1 + .eslintrc.json | 13 +- .vscode/settings.json | 1 + libs/icons/valentines/src/index.ts | 10 + libs/icons/valentines/src/lib/calendar.tsx | 73 +++ libs/icons/valentines/src/lib/diamond.tsx | 2 - libs/icons/valentines/src/lib/hand.tsx | 75 +++ libs/icons/valentines/src/lib/heart.tsx | 58 ++ libs/icons/valentines/src/lib/heartbreak.tsx | 159 ++++++ libs/icons/valentines/src/lib/hearts.tsx | 123 ++++ libs/icons/valentines/src/lib/letter.tsx | 142 +++++ libs/icons/valentines/src/lib/mail.tsx | 2 - libs/icons/valentines/src/lib/mailbox.tsx | 165 ++++++ libs/icons/valentines/src/lib/wings.tsx | 133 +++++ libs/icons/valentines/src/lib/world.tsx | 245 ++++++++ libs/prisma/src/schemas/main.prisma | 3 +- .../lib/server/routers/usage/contributions.ts | 8 +- .../ui/game-grid/src/lib/mixed-grid-items.tsx | 14 +- libs/ui/inputs/src/index.ts | 1 + libs/ui/inputs/src/lib/clipboard-text.tsx | 53 ++ libs/ui/pages/account/src/index.ts | 4 + .../src/lib/__hooks__/use-time-until.ts | 16 + .../account/src/lib/__mocks__/friends.ts | 52 ++ .../pages/account/src/lib/__mocks__/index.ts | 2 + .../account/src/lib/__mocks__/referrals.ts | 47 ++ .../src/lib/bullet-points/bullet-points.tsx | 21 + .../account/src/lib/bullet-points/index.ts | 2 + .../src/lib/bullet-points/title-text.tsx | 20 + .../collapsible-section.tsx | 121 ++++ .../src/lib/collapsible-section/index.ts | 1 + libs/ui/pages/account/src/lib/const.ts | 16 + .../friends-panel/friends-panel.stories.tsx | 59 ++ .../src/lib/friends-panel/friends-panel.tsx | 63 +++ .../friends-table/friends-table.stories.tsx | 53 ++ .../friends-table/friends-table.tsx | 248 +++++++++ .../lib/friends-panel/friends-table/index.ts | 1 + .../account/src/lib/friends-panel/index.ts | 1 + .../sections/add-friends-section.stories.tsx | 33 ++ .../sections/add-friends-section.tsx | 131 +++++ .../src/lib/friends-panel/sections/index.ts | 1 + .../sections/send-gifts-section.stories.tsx | 55 ++ .../sections/send-gifts-section.tsx | 119 ++++ .../lib/modals/add-friend-modal.stories.tsx | 22 + .../src/lib/modals/add-friend-modal.tsx | 64 +++ .../lib/modals/claim-gift-modal.stories.tsx | 21 + .../src/lib/modals/claim-gift-modal.tsx | 67 +++ libs/ui/pages/account/src/lib/modals/index.ts | 1 + .../modals/remove-friend-modal.stories.tsx | 21 + .../src/lib/modals/remove-friend-modal.tsx | 64 +++ .../lib/modals/send-gift-modal.stories.tsx | 21 + .../src/lib/modals/send-gift-modal.tsx | 64 +++ .../account/src/lib/panel-footer/index.ts | 1 + .../src/lib/panel-footer/panel-footer.tsx | 43 ++ .../account/src/lib/panel-header/index.ts | 1 + .../src/lib/panel-header/panel-header.tsx | 32 ++ .../lib/referral-info/how-referrals-work.tsx | 27 + .../account/src/lib/referral-info/index.ts | 1 + .../src/lib/referral-info/referral-info.tsx | 73 +++ .../account/src/lib/referrals-panel/index.ts | 1 + .../referrals-panel/referral-table/index.ts | 1 + .../referrals-table.stories.tsx | 40 ++ .../referral-table/referrals-table.tsx | 104 ++++ .../referrals-panel.stories.tsx | 49 ++ .../lib/referrals-panel/referrals-panel.tsx | 65 +++ .../src/lib/referrals-panel/sections/index.ts | 1 + .../sections/referred-accounts-section.tsx | 62 +++ .../sections/referred-plays-section.tsx | 132 +++++ .../sections/share-your-link-section.tsx | 62 +++ .../account/src/lib/rewards-timer/index.ts | 1 + .../src/lib/rewards-timer/rewards-timer.tsx | 27 + .../account/src/lib/tokens-panel/index.ts | 1 + .../sections/daily-reward-section.stories.tsx | 46 ++ .../sections/daily-reward-section.tsx | 213 +++++++ .../sections/gift-box-section.stories.tsx | 39 ++ .../sections/gift-box-section.tsx | 126 +++++ .../src/lib/tokens-panel/sections/index.ts | 4 + .../invite-friends-section.stories.tsx | 35 ++ .../sections/invite-friends-section.tsx | 79 +++ .../sections/play-games-section.stories.tsx | 46 ++ .../sections/play-games-section.tsx | 159 ++++++ .../lib/tokens-panel/tokens-panel.stories.tsx | 66 +++ .../src/lib/tokens-panel/tokens-panel.tsx | 93 ++++ .../account/src/lib/tokens-panel/types.ts | 5 + libs/ui/pages/account/src/lib/types.ts | 17 + .../src/lib/modals/share-game/share-game.tsx | 22 +- .../lib/modals/share-game/social-buttons.tsx | 49 -- libs/ui/pills/src/index.ts | 1 - .../pills/src/lib/progress-pill.stories.tsx | 45 -- libs/ui/pills/src/lib/progress-pill.tsx | 65 --- libs/ui/social-media/.babelrc | 12 + libs/ui/social-media/.eslintrc.json | 18 + libs/ui/social-media/README.md | 7 + libs/ui/social-media/jest.config.ts | 11 + libs/ui/social-media/project.json | 30 + libs/ui/social-media/src/index.ts | 2 + libs/ui/social-media/src/lib/intents.ts | 12 + .../social-media/src/lib/social-buttons.tsx | 30 + libs/ui/social-media/tsconfig.json | 20 + libs/ui/social-media/tsconfig.lib.json | 23 + libs/ui/social-media/tsconfig.spec.json | 20 + libs/util/auth/src/lib/prisma-adapter.ts | 3 + libs/util/numbers/src/lib/numbers.ts | 8 +- libs/util/time/src/lib/time.spec.ts | 60 +- libs/util/time/src/lib/time.ts | 83 +-- libs/util/types/src/index.ts | 18 +- libs/util/types/src/lib/game-info.ts | 1 + libs/util/types/src/lib/index.ts | 16 - libs/util/urls/src/index.ts | 2 + package-lock.json | 523 +++++++++--------- package.json | 1 - tsconfig.base.json | 1 + types/@mui.d.ts | 1 + 112 files changed, 4885 insertions(+), 543 deletions(-) create mode 100644 libs/icons/valentines/src/lib/calendar.tsx create mode 100644 libs/icons/valentines/src/lib/hand.tsx create mode 100644 libs/icons/valentines/src/lib/heart.tsx create mode 100644 libs/icons/valentines/src/lib/heartbreak.tsx create mode 100644 libs/icons/valentines/src/lib/hearts.tsx create mode 100644 libs/icons/valentines/src/lib/letter.tsx create mode 100644 libs/icons/valentines/src/lib/mailbox.tsx create mode 100644 libs/icons/valentines/src/lib/wings.tsx create mode 100644 libs/icons/valentines/src/lib/world.tsx create mode 100644 libs/ui/inputs/src/lib/clipboard-text.tsx create mode 100644 libs/ui/pages/account/src/lib/__hooks__/use-time-until.ts create mode 100644 libs/ui/pages/account/src/lib/__mocks__/friends.ts create mode 100644 libs/ui/pages/account/src/lib/__mocks__/index.ts create mode 100644 libs/ui/pages/account/src/lib/__mocks__/referrals.ts create mode 100644 libs/ui/pages/account/src/lib/bullet-points/bullet-points.tsx create mode 100644 libs/ui/pages/account/src/lib/bullet-points/index.ts create mode 100644 libs/ui/pages/account/src/lib/bullet-points/title-text.tsx create mode 100644 libs/ui/pages/account/src/lib/collapsible-section/collapsible-section.tsx create mode 100644 libs/ui/pages/account/src/lib/collapsible-section/index.ts create mode 100644 libs/ui/pages/account/src/lib/const.ts create mode 100644 libs/ui/pages/account/src/lib/friends-panel/friends-panel.stories.tsx create mode 100644 libs/ui/pages/account/src/lib/friends-panel/friends-panel.tsx create mode 100644 libs/ui/pages/account/src/lib/friends-panel/friends-table/friends-table.stories.tsx create mode 100644 libs/ui/pages/account/src/lib/friends-panel/friends-table/friends-table.tsx create mode 100644 libs/ui/pages/account/src/lib/friends-panel/friends-table/index.ts create mode 100644 libs/ui/pages/account/src/lib/friends-panel/index.ts create mode 100644 libs/ui/pages/account/src/lib/friends-panel/sections/add-friends-section.stories.tsx create mode 100644 libs/ui/pages/account/src/lib/friends-panel/sections/add-friends-section.tsx create mode 100644 libs/ui/pages/account/src/lib/friends-panel/sections/index.ts create mode 100644 libs/ui/pages/account/src/lib/friends-panel/sections/send-gifts-section.stories.tsx create mode 100644 libs/ui/pages/account/src/lib/friends-panel/sections/send-gifts-section.tsx create mode 100644 libs/ui/pages/account/src/lib/modals/add-friend-modal.stories.tsx create mode 100644 libs/ui/pages/account/src/lib/modals/add-friend-modal.tsx create mode 100644 libs/ui/pages/account/src/lib/modals/claim-gift-modal.stories.tsx create mode 100644 libs/ui/pages/account/src/lib/modals/claim-gift-modal.tsx create mode 100644 libs/ui/pages/account/src/lib/modals/index.ts create mode 100644 libs/ui/pages/account/src/lib/modals/remove-friend-modal.stories.tsx create mode 100644 libs/ui/pages/account/src/lib/modals/remove-friend-modal.tsx create mode 100644 libs/ui/pages/account/src/lib/modals/send-gift-modal.stories.tsx create mode 100644 libs/ui/pages/account/src/lib/modals/send-gift-modal.tsx create mode 100644 libs/ui/pages/account/src/lib/panel-footer/index.ts create mode 100644 libs/ui/pages/account/src/lib/panel-footer/panel-footer.tsx create mode 100644 libs/ui/pages/account/src/lib/panel-header/index.ts create mode 100644 libs/ui/pages/account/src/lib/panel-header/panel-header.tsx create mode 100644 libs/ui/pages/account/src/lib/referral-info/how-referrals-work.tsx create mode 100644 libs/ui/pages/account/src/lib/referral-info/index.ts create mode 100644 libs/ui/pages/account/src/lib/referral-info/referral-info.tsx create mode 100644 libs/ui/pages/account/src/lib/referrals-panel/index.ts create mode 100644 libs/ui/pages/account/src/lib/referrals-panel/referral-table/index.ts create mode 100644 libs/ui/pages/account/src/lib/referrals-panel/referral-table/referrals-table.stories.tsx create mode 100644 libs/ui/pages/account/src/lib/referrals-panel/referral-table/referrals-table.tsx create mode 100644 libs/ui/pages/account/src/lib/referrals-panel/referrals-panel.stories.tsx create mode 100644 libs/ui/pages/account/src/lib/referrals-panel/referrals-panel.tsx create mode 100644 libs/ui/pages/account/src/lib/referrals-panel/sections/index.ts create mode 100644 libs/ui/pages/account/src/lib/referrals-panel/sections/referred-accounts-section.tsx create mode 100644 libs/ui/pages/account/src/lib/referrals-panel/sections/referred-plays-section.tsx create mode 100644 libs/ui/pages/account/src/lib/referrals-panel/sections/share-your-link-section.tsx create mode 100644 libs/ui/pages/account/src/lib/rewards-timer/index.ts create mode 100644 libs/ui/pages/account/src/lib/rewards-timer/rewards-timer.tsx create mode 100644 libs/ui/pages/account/src/lib/tokens-panel/index.ts create mode 100644 libs/ui/pages/account/src/lib/tokens-panel/sections/daily-reward-section.stories.tsx create mode 100644 libs/ui/pages/account/src/lib/tokens-panel/sections/daily-reward-section.tsx create mode 100644 libs/ui/pages/account/src/lib/tokens-panel/sections/gift-box-section.stories.tsx create mode 100644 libs/ui/pages/account/src/lib/tokens-panel/sections/gift-box-section.tsx create mode 100644 libs/ui/pages/account/src/lib/tokens-panel/sections/index.ts create mode 100644 libs/ui/pages/account/src/lib/tokens-panel/sections/invite-friends-section.stories.tsx create mode 100644 libs/ui/pages/account/src/lib/tokens-panel/sections/invite-friends-section.tsx create mode 100644 libs/ui/pages/account/src/lib/tokens-panel/sections/play-games-section.stories.tsx create mode 100644 libs/ui/pages/account/src/lib/tokens-panel/sections/play-games-section.tsx create mode 100644 libs/ui/pages/account/src/lib/tokens-panel/tokens-panel.stories.tsx create mode 100644 libs/ui/pages/account/src/lib/tokens-panel/tokens-panel.tsx create mode 100644 libs/ui/pages/account/src/lib/tokens-panel/types.ts delete mode 100644 libs/ui/pages/game/src/lib/modals/share-game/social-buttons.tsx delete mode 100644 libs/ui/pills/src/lib/progress-pill.stories.tsx delete mode 100644 libs/ui/pills/src/lib/progress-pill.tsx create mode 100644 libs/ui/social-media/.babelrc create mode 100644 libs/ui/social-media/.eslintrc.json create mode 100644 libs/ui/social-media/README.md create mode 100644 libs/ui/social-media/jest.config.ts create mode 100644 libs/ui/social-media/project.json create mode 100644 libs/ui/social-media/src/index.ts create mode 100644 libs/ui/social-media/src/lib/intents.ts create mode 100644 libs/ui/social-media/src/lib/social-buttons.tsx create mode 100644 libs/ui/social-media/tsconfig.json create mode 100644 libs/ui/social-media/tsconfig.lib.json create mode 100644 libs/ui/social-media/tsconfig.spec.json create mode 100644 libs/util/types/src/lib/game-info.ts delete mode 100644 libs/util/types/src/lib/index.ts diff --git a/.eslintignore b/.eslintignore index 3c3629e64..4cbee9a25 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1 +1,2 @@ node_modules +package.json \ No newline at end of file diff --git a/.eslintrc.json b/.eslintrc.json index a6e2ccff0..80ed9909f 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,6 +1,13 @@ { "root": true, - "ignorePatterns": ["**/*"], + "ignorePatterns": [ + "**/*", + "/package.json", + "package-lock.json", + "node_modules", + "dist", + "coverage" + ], "plugins": ["@nx", "simple-import-sort"], "overrides": [ { @@ -38,6 +45,10 @@ "files": ["*.js", "*.jsx"], "extends": ["plugin:@nx/javascript"], "rules": {} + }, + { + "files": ["*.json"], + "rules": {} } ] } diff --git a/.vscode/settings.json b/.vscode/settings.json index 4b9fb97f8..a5667c059 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -27,6 +27,7 @@ "fullstory", "gamemonetize", "Gamu", + "Giftcard", "hackathon", "iframed", "itchio", diff --git a/libs/icons/valentines/src/index.ts b/libs/icons/valentines/src/index.ts index 4bf5fdf4a..81bdddaa9 100644 --- a/libs/icons/valentines/src/index.ts +++ b/libs/icons/valentines/src/index.ts @@ -1,7 +1,17 @@ // https://www.svgrepo.com/collection/valentines-day-21/ +// https://www.svgrepo.com/collection/valentines-day-20/ export * from './lib/ticket'; export * from './lib/mail'; export * from './lib/diamond'; export * from './lib/gift'; export * from './lib/chat'; +export * from './lib/letter'; +export * from './lib/calendar'; +export * from './lib/heart'; +export * from './lib/hearts'; +export * from './lib/mailbox'; +export * from './lib/heartbreak'; +export * from './lib/world'; +export * from './lib/hand'; +export * from './lib/wings'; diff --git a/libs/icons/valentines/src/lib/calendar.tsx b/libs/icons/valentines/src/lib/calendar.tsx new file mode 100644 index 000000000..ca3a5cec7 --- /dev/null +++ b/libs/icons/valentines/src/lib/calendar.tsx @@ -0,0 +1,73 @@ +import { createSvgIcon } from '@mui/material'; + +const name = 'ValentinesCalendar'; + +export const ValentinesCalendar = createSvgIcon( + + + + + + + + + + + + + + + + + , + name +); diff --git a/libs/icons/valentines/src/lib/diamond.tsx b/libs/icons/valentines/src/lib/diamond.tsx index aeb7aa946..8485ea38c 100644 --- a/libs/icons/valentines/src/lib/diamond.tsx +++ b/libs/icons/valentines/src/lib/diamond.tsx @@ -4,8 +4,6 @@ const name = 'ValentinesDiamond'; export const ValentinesDiamond = createSvgIcon( + + + + + + + + + + , + name +); diff --git a/libs/icons/valentines/src/lib/heart.tsx b/libs/icons/valentines/src/lib/heart.tsx new file mode 100644 index 000000000..2dc2b998e --- /dev/null +++ b/libs/icons/valentines/src/lib/heart.tsx @@ -0,0 +1,58 @@ +import { createSvgIcon } from '@mui/material'; + +const name = 'ValentinesHeart'; + +export const ValentinesHeart = createSvgIcon( + + + + + + + + + , + name +); diff --git a/libs/icons/valentines/src/lib/heartbreak.tsx b/libs/icons/valentines/src/lib/heartbreak.tsx new file mode 100644 index 000000000..e1f426efd --- /dev/null +++ b/libs/icons/valentines/src/lib/heartbreak.tsx @@ -0,0 +1,159 @@ +import { createSvgIcon } from '@mui/material'; + +const name = 'ValentinesHeartbreak'; + +export const ValentinesHeartbreak = createSvgIcon( + + + + + + + + + + + + + + + + + + + + + + + + + + , + name +); diff --git a/libs/icons/valentines/src/lib/hearts.tsx b/libs/icons/valentines/src/lib/hearts.tsx new file mode 100644 index 000000000..fec75163b --- /dev/null +++ b/libs/icons/valentines/src/lib/hearts.tsx @@ -0,0 +1,123 @@ +import { createSvgIcon } from '@mui/material'; + +const name = 'ValentinesHearts'; + +export const ValentinesHearts = createSvgIcon( + + + + + + + + + + + + + + + + + + , + name +); diff --git a/libs/icons/valentines/src/lib/letter.tsx b/libs/icons/valentines/src/lib/letter.tsx new file mode 100644 index 000000000..877dc2c2b --- /dev/null +++ b/libs/icons/valentines/src/lib/letter.tsx @@ -0,0 +1,142 @@ +import { createSvgIcon } from '@mui/material'; + +const name = 'ValentinesLetter'; + +export const ValentinesLetter = createSvgIcon( + + + + + + + + + + + + + + + + + + + + + + , + name +); diff --git a/libs/icons/valentines/src/lib/mail.tsx b/libs/icons/valentines/src/lib/mail.tsx index 11a7ccdac..966c61346 100644 --- a/libs/icons/valentines/src/lib/mail.tsx +++ b/libs/icons/valentines/src/lib/mail.tsx @@ -4,8 +4,6 @@ const name = 'ValentinesMail'; export const ValentinesMail = createSvgIcon( + + + + + + + + + + + + + + + + + + + + + + + + + + + + , + name +); diff --git a/libs/icons/valentines/src/lib/wings.tsx b/libs/icons/valentines/src/lib/wings.tsx new file mode 100644 index 000000000..4901c3c52 --- /dev/null +++ b/libs/icons/valentines/src/lib/wings.tsx @@ -0,0 +1,133 @@ +import { createSvgIcon } from '@mui/material'; + +const name = 'ValentinesWings'; + +export const ValentinesWings = createSvgIcon( + + + + + + + + + + + + + + + + + + + , + name +); diff --git a/libs/icons/valentines/src/lib/world.tsx b/libs/icons/valentines/src/lib/world.tsx new file mode 100644 index 000000000..8b3656f6e --- /dev/null +++ b/libs/icons/valentines/src/lib/world.tsx @@ -0,0 +1,245 @@ +import { createSvgIcon } from '@mui/material'; + +const name = 'ValentinesWorld'; + +export const ValentinesWorld = createSvgIcon( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + , + name +); diff --git a/libs/prisma/src/schemas/main.prisma b/libs/prisma/src/schemas/main.prisma index 72eda7fed..b761d315c 100644 --- a/libs/prisma/src/schemas/main.prisma +++ b/libs/prisma/src/schemas/main.prisma @@ -113,7 +113,8 @@ model Email { model Profile { id String @id @default(cuid()) // basic settings - username String @unique + // TODO: handle unique usernames, return an error if the username is taken. + username String? @unique @db.VarChar(63) bio String? // publisher settings isPublisher Boolean @default(false) diff --git a/libs/trpc/charity/src/lib/server/routers/usage/contributions.ts b/libs/trpc/charity/src/lib/server/routers/usage/contributions.ts index 4cc5fc069..55b546b81 100644 --- a/libs/trpc/charity/src/lib/server/routers/usage/contributions.ts +++ b/libs/trpc/charity/src/lib/server/routers/usage/contributions.ts @@ -1,7 +1,4 @@ -import { - gameMonetizeGames, - games, -} from '@worksheets/data-access/charity-games'; +import { games } from '@worksheets/data-access/charity-games'; import { basicWebsiteStatisticsSchema } from '@worksheets/util/types'; import { publicProcedure } from '../../procedures'; @@ -10,7 +7,6 @@ export default publicProcedure .output(basicWebsiteStatisticsSchema) .query(async ({ ctx: { db } }) => { // donated games exclude game monetize games - const countGameMonetizeGames = gameMonetizeGames.length; const allGames = games.length; // game plays is the sum of all game plays @@ -26,7 +22,7 @@ export default publicProcedure }, 0); return { - donatedGames: allGames - countGameMonetizeGames, + donatedGames: allGames, totalGamePlays: totalGamePlays, uniqueGames: gamesPlayed.length, uniquePlayers: 500, diff --git a/libs/ui/game-grid/src/lib/mixed-grid-items.tsx b/libs/ui/game-grid/src/lib/mixed-grid-items.tsx index c7e8f7e9f..ab23adafd 100644 --- a/libs/ui/game-grid/src/lib/mixed-grid-items.tsx +++ b/libs/ui/game-grid/src/lib/mixed-grid-items.tsx @@ -8,8 +8,6 @@ import { CategoryPillProps, ImagePill, ImagePillProps, - ProgressPill, - ProgressPillProps, TextPill, TextPillProps, } from '@worksheets/ui/pills'; @@ -21,7 +19,6 @@ export type MixedGridItem = | (GameIconProps & { type: 'game'; span?: number }) | (CategoryPillProps & { type: 'category' }) | (TextPillProps & { type: 'text'; width?: BoxProps['gridColumn'] }) - | (ProgressPillProps & { type: 'progress' }) | (ImagePillProps & { type: 'image' }) | (ButtonPillProps & { type: 'button'; width?: BoxProps['gridColumn'] }) | (AdvertisementPillProps & { @@ -67,16 +64,7 @@ export const MixedGridItems: FC = ({ items, size }) => { )} - {item.type === 'progress' && ( - - - - )} + {item.type === 'image' && ( void; +}> = ({ text, label, helperText, onCopy }) => { + const [copied, setCopied] = useState(false); + + const handleCopyToClipboard = () => { + navigator.clipboard.writeText(text); + setCopied(true); + onCopy && onCopy(); + }; + + return ( + + Copied to clipboard! + + ) : ( + helperText + ) + } + onChange={(e) => { + // Ignore input + }} + InputProps={{ + endAdornment: ( + + + + + + ), + }} + /> + ); +}; diff --git a/libs/ui/pages/account/src/index.ts b/libs/ui/pages/account/src/index.ts index 82a0ded30..3fda23978 100644 --- a/libs/ui/pages/account/src/index.ts +++ b/libs/ui/pages/account/src/index.ts @@ -1,4 +1,8 @@ export * from './lib/account-screen'; export * from './lib/profile-panel'; export * from './lib/submissions-panel'; +export * from './lib/tokens-panel'; +export * from './lib/referrals-panel'; +export * from './lib/friends-panel'; export * from './lib/types'; +export * from './lib/modals'; diff --git a/libs/ui/pages/account/src/lib/__hooks__/use-time-until.ts b/libs/ui/pages/account/src/lib/__hooks__/use-time-until.ts new file mode 100644 index 000000000..bdc2d85d5 --- /dev/null +++ b/libs/ui/pages/account/src/lib/__hooks__/use-time-until.ts @@ -0,0 +1,16 @@ +import { useInterval } from '@worksheets/ui-core'; +import { + millisecondsToDuration, + timeUntil as timeUntilUtil, +} from '@worksheets/util/time'; +import { useState } from 'react'; + +export const useTimeUntil = (timestamp: number) => { + const [timeUntil, setTimeUntil] = useState('??:??:??'); + + useInterval(() => { + setTimeUntil(millisecondsToDuration(Math.max(0, timeUntilUtil(timestamp)))); + }, 1000); + + return timeUntil; +}; diff --git a/libs/ui/pages/account/src/lib/__mocks__/friends.ts b/libs/ui/pages/account/src/lib/__mocks__/friends.ts new file mode 100644 index 000000000..b3c85baf0 --- /dev/null +++ b/libs/ui/pages/account/src/lib/__mocks__/friends.ts @@ -0,0 +1,52 @@ +import { + daysAgo, + hoursAgo, + minutesAgo, + monthsAgo, + weeksAgo, +} from '@worksheets/util/time'; + +import { Friend } from '../types'; + +export const mockFriends: Friend[] = [ + { + id: '1', + username: 'mighty-mouse', + lastSeen: minutesAgo(1).getTime(), + gamesPlayed: 128, + isFavorite: true, + hasSentGiftToday: false, + }, + { + id: '2', + username: 'joey-jo-jo-junior-shabadoo-iii-esquire', + lastSeen: hoursAgo(21).getTime(), + gamesPlayed: 128, + isFavorite: true, + hasSentGiftToday: true, + }, + { + id: '3', + username: 'test-user', + lastSeen: daysAgo(5).getTime(), + gamesPlayed: 0, + isFavorite: false, + hasSentGiftToday: false, + }, + { + id: '4', + username: 'kyle-the-bard', + lastSeen: weeksAgo(2).getTime(), + gamesPlayed: 1615, + isFavorite: false, + hasSentGiftToday: true, + }, + { + id: '5', + username: 'shrek', + lastSeen: monthsAgo(1).getTime(), + gamesPlayed: 61346, + isFavorite: false, + hasSentGiftToday: false, + }, +]; diff --git a/libs/ui/pages/account/src/lib/__mocks__/index.ts b/libs/ui/pages/account/src/lib/__mocks__/index.ts new file mode 100644 index 000000000..806eb29e4 --- /dev/null +++ b/libs/ui/pages/account/src/lib/__mocks__/index.ts @@ -0,0 +1,2 @@ +export * from './referrals'; +export * from './friends'; diff --git a/libs/ui/pages/account/src/lib/__mocks__/referrals.ts b/libs/ui/pages/account/src/lib/__mocks__/referrals.ts new file mode 100644 index 000000000..23d809a70 --- /dev/null +++ b/libs/ui/pages/account/src/lib/__mocks__/referrals.ts @@ -0,0 +1,47 @@ +import { + daysAgo, + hoursAgo, + minutesAgo, + monthsAgo, + weeksAgo, +} from '@worksheets/util/time'; + +import { Referral } from '../types'; + +export const mockReferrals: Referral[] = [ + { + id: '1', + username: 'test', + createdAt: minutesAgo(23145).getTime(), + gamesRemaining: 10, + tokensEarned: 234, + }, + { + id: '2', + username: 'high-score-girl', + createdAt: hoursAgo(31567).getTime(), + gamesRemaining: 3, + tokensEarned: 5155, + }, + { + id: '3', + username: 'sailor-moon', + gamesRemaining: 4, + createdAt: daysAgo(53).getTime(), + tokensEarned: 235, + }, + { + id: '4', + username: 'nice-guy-123', + createdAt: weeksAgo(2).getTime(), + gamesRemaining: 2, + tokensEarned: 1, + }, + { + id: '5', + username: 'super-mario', + createdAt: monthsAgo(3).getTime(), + gamesRemaining: 0, + tokensEarned: 23, + }, +]; diff --git a/libs/ui/pages/account/src/lib/bullet-points/bullet-points.tsx b/libs/ui/pages/account/src/lib/bullet-points/bullet-points.tsx new file mode 100644 index 000000000..5f91b8cc5 --- /dev/null +++ b/libs/ui/pages/account/src/lib/bullet-points/bullet-points.tsx @@ -0,0 +1,21 @@ +import { Box } from '@mui/material'; +import { ListItem, UnorderedList } from '@worksheets/ui-core'; + +import { TitleText } from './title-text'; + +export const BulletPoints: React.FC<{ + icon: React.ReactNode; + title: string; + points: React.ReactNode[]; +}> = ({ title, icon, points }) => ( + + + + {points.map((point, index) => ( + + {point} + + ))} + + +); diff --git a/libs/ui/pages/account/src/lib/bullet-points/index.ts b/libs/ui/pages/account/src/lib/bullet-points/index.ts new file mode 100644 index 000000000..16d05ef85 --- /dev/null +++ b/libs/ui/pages/account/src/lib/bullet-points/index.ts @@ -0,0 +1,2 @@ +export * from './bullet-points'; +export * from './title-text'; diff --git a/libs/ui/pages/account/src/lib/bullet-points/title-text.tsx b/libs/ui/pages/account/src/lib/bullet-points/title-text.tsx new file mode 100644 index 000000000..8df1cd775 --- /dev/null +++ b/libs/ui/pages/account/src/lib/bullet-points/title-text.tsx @@ -0,0 +1,20 @@ +import { Typography } from '@mui/material'; +import { ReactNode } from 'react'; + +export type TitleTextProps = { + icon: ReactNode; + title: string; +}; + +export const TitleText: React.FC = ({ icon, title }) => ( + + {icon} + {title} + +); diff --git a/libs/ui/pages/account/src/lib/collapsible-section/collapsible-section.tsx b/libs/ui/pages/account/src/lib/collapsible-section/collapsible-section.tsx new file mode 100644 index 000000000..808561448 --- /dev/null +++ b/libs/ui/pages/account/src/lib/collapsible-section/collapsible-section.tsx @@ -0,0 +1,121 @@ +import { + ChevronRight, + ExpandLess, + ExpandMore, + SvgIconComponent, +} from '@mui/icons-material'; +import { + Box, + ButtonBase, + Collapse, + SvgIconOwnProps, + Typography, + useMediaQuery, + useTheme, +} from '@mui/material'; +import { FC, ReactNode, useState } from 'react'; + +export const CollapsibleSection: FC<{ + text: string; + description: string; + children: ReactNode; + status: ReactNode; + Icon: SvgIconComponent; + open?: boolean; +}> = ({ + text, + description, + status, + children, + Icon, + open: defaultOpen = false, +}) => { + const theme = useTheme(); + const isMobile = useMediaQuery(theme.breakpoints.down('sm')); + const [open, setOpen] = useState(defaultOpen); // TODO: default to false after testing + + return ( + `4px solid ${theme.palette.divider}`, + borderRadius: (theme) => theme.shape.borderRadius, + overflow: 'hidden', + }} + > + setOpen((o) => !o)} + sx={{ + width: '100%', + display: 'flex', + flexDirection: { xs: 'column', sm: 'row' }, + justifyContent: 'space-between', + alignItems: { xs: 'flex-start', sm: 'center' }, + p: 2, + gap: 1, + '&:hover': { + backgroundColor: (theme) => theme.palette.action.hover, + }, + }} + > + + + + {text} + + {description} + + + + + {status} + + + + + `4px solid ${theme.palette.divider}`, + }} + > + {children} + + + + ); +}; + +const RewardPanelAction: FC<{ open: boolean }> = ({ open }) => { + const theme = useTheme(); + const isMobile = useMediaQuery(theme.breakpoints.down('sm')); + + const actionSx: SvgIconOwnProps['sx'] = { + fontSize: { xs: '2rem', sm: '3rem' }, + }; + + if (isMobile) { + if (open) return ; + return ; + } else { + if (open) return ; + return ; + } +}; diff --git a/libs/ui/pages/account/src/lib/collapsible-section/index.ts b/libs/ui/pages/account/src/lib/collapsible-section/index.ts new file mode 100644 index 000000000..002c961ed --- /dev/null +++ b/libs/ui/pages/account/src/lib/collapsible-section/index.ts @@ -0,0 +1 @@ +export * from './collapsible-section'; diff --git a/libs/ui/pages/account/src/lib/const.ts b/libs/ui/pages/account/src/lib/const.ts new file mode 100644 index 000000000..8960aeff5 --- /dev/null +++ b/libs/ui/pages/account/src/lib/const.ts @@ -0,0 +1,16 @@ +// game play +export const MAX_TOKENS_PER_GAME = 5; +export const MAX_TOKENS_PER_DAY = 100; +// daily rewards +export const BASE_DAILY_REWARD = 20; +export const MOMENTUM_MULTIPLIER = 0.5; +export const MAX_MOMENTUM = 10; +// referrals +export const TOKENS_PER_REFERRAL_PLAY = 1; +export const MAX_TOKENS_FROM_REFERRAL_PLAYS = 100; +export const TOKENS_PER_REFERRAL_ACCOUNT = 100; +export const TOKENS_PER_REFERRAL_PURCHASE = 100; +// gift boxes +export const GIFT_BOX_DROP_RATE = 2.5; +export const MAX_TOKENS_IN_GIFT_BOX = 20; +export const MAX_DAILY_GIFT_BOX_SHARES = 5; diff --git a/libs/ui/pages/account/src/lib/friends-panel/friends-panel.stories.tsx b/libs/ui/pages/account/src/lib/friends-panel/friends-panel.stories.tsx new file mode 100644 index 000000000..6b18c2cc1 --- /dev/null +++ b/libs/ui/pages/account/src/lib/friends-panel/friends-panel.stories.tsx @@ -0,0 +1,59 @@ +import Paper from '@mui/material/Paper'; +import { action } from '@storybook/addon-actions'; +import type { Meta } from '@storybook/react'; + +import { mockFriends } from '../__mocks__'; +import { FriendsPanel } from './friends-panel'; + +type Story = Meta; + +const Default: Story = { + component: FriendsPanel, + args: { + onRemove: action('onRemove'), + onFavorite: action('onFavorite'), + }, + decorators: [ + (Story) => ( +
+ + + +
+ ), + ], +}; +export default Default; + +export const Empty: Story = { + args: { + friends: [], + refreshTimestamp: Date.now() + 1000 * 60 * 60 * 3, + friendCode: 'ABC123', + giftsRemaining: 5, + }, +}; + +export const WithFriends: Story = { + args: { + friends: mockFriends, + refreshTimestamp: Date.now() + 1000 * 60 * 60 * 12, + friendCode: 'ABC123', + giftsRemaining: 5, + }, +}; + +export const SentGifts: Story = { + args: { + friends: mockFriends, + refreshTimestamp: Date.now() + 1000 * 60 * 60 * 12, + friendCode: 'ABC123', + giftsRemaining: 0, + }, +}; diff --git a/libs/ui/pages/account/src/lib/friends-panel/friends-panel.tsx b/libs/ui/pages/account/src/lib/friends-panel/friends-panel.tsx new file mode 100644 index 000000000..5c3c33e48 --- /dev/null +++ b/libs/ui/pages/account/src/lib/friends-panel/friends-panel.tsx @@ -0,0 +1,63 @@ +import { InfoOutlined } from '@mui/icons-material'; +import { Box, Divider, Link, Typography } from '@mui/material'; +import { ValentinesHeart } from '@worksheets/icons/valentines'; + +import { PanelFooter } from '../panel-footer'; +import { PanelHeader } from '../panel-header'; +import { Friend } from '../types'; +import { SendGiftsSection } from './sections'; +import { AddFriendsSection } from './sections/add-friends-section'; + +export const FriendsPanel: React.FC<{ + friends: Friend[]; + refreshTimestamp: number; + giftsRemaining: number; + friendCode: string; + onRemove: (friend: Friend) => void; + onFavorite: (friend: Friend) => void; +}> = (props) => { + return ( + + } + /> + + + + + + + + + + + + + Earn tokens when people play games with our{' '} + Referral Program. + + + + + ); +}; diff --git a/libs/ui/pages/account/src/lib/friends-panel/friends-table/friends-table.stories.tsx b/libs/ui/pages/account/src/lib/friends-panel/friends-table/friends-table.stories.tsx new file mode 100644 index 000000000..0a497cbcc --- /dev/null +++ b/libs/ui/pages/account/src/lib/friends-panel/friends-table/friends-table.stories.tsx @@ -0,0 +1,53 @@ +import Paper from '@mui/material/Paper'; +import { action } from '@storybook/addon-actions'; +import type { Meta } from '@storybook/react'; + +import { mockFriends } from '../../__mocks__'; +import { FriendsTable } from './friends-table'; + +type Story = Meta; + +const Default: Story = { + component: FriendsTable, + args: { + onRemove: action('onRemove'), + onFavorite: action('onFavorite'), + }, + decorators: [ + (Story) => ( +
+ + + +
+ ), + ], +}; +export default Default; + +export const Empty: Story = { + args: { + friends: [], + canSendGifts: true, + }, +}; + +export const WithPeople: Story = { + args: { + friends: mockFriends, + canSendGifts: true, + }, +}; + +export const SentGifts: Story = { + args: { + friends: mockFriends, + canSendGifts: false, + }, +}; diff --git a/libs/ui/pages/account/src/lib/friends-panel/friends-table/friends-table.tsx b/libs/ui/pages/account/src/lib/friends-panel/friends-table/friends-table.tsx new file mode 100644 index 000000000..7104242b7 --- /dev/null +++ b/libs/ui/pages/account/src/lib/friends-panel/friends-table/friends-table.tsx @@ -0,0 +1,248 @@ +import { + AccessTime, + Cancel, + Favorite, + FavoriteBorderOutlined, +} from '@mui/icons-material'; +import { + Box, + Button, + IconButton, + Link, + styled, + Tooltip, + Typography, + useMediaQuery, + useTheme, +} from '@mui/material'; +import Table from '@mui/material/Table'; +import TableBody from '@mui/material/TableBody'; +import TableCell from '@mui/material/TableCell'; +import TableContainer from '@mui/material/TableContainer'; +import TableHead from '@mui/material/TableHead'; +import TableRow from '@mui/material/TableRow'; +import { ValentinesGift } from '@worksheets/icons/valentines'; +import { WebGamepad } from '@worksheets/icons/web'; +import { shorthandNumber } from '@worksheets/util/numbers'; +import { printRelativeDate } from '@worksheets/util/time'; +import * as React from 'react'; + +import { MAX_DAILY_GIFT_BOX_SHARES } from '../../const'; +import { Friend } from '../../types'; + +const StyledBox = styled(Box)(({ theme }) => ({ + border: `1px solid ${theme.palette.divider}`, + borderRadius: theme.shape.borderRadius, + overflow: 'hidden', +})); + +export type FriendsTableProps = { + friends: Friend[]; + canSendGifts: boolean; + onRemove: (friend: Friend) => void; + onFavorite: (friend: Friend) => void; +}; + +export const FriendsTable: React.FC = ({ + friends, + canSendGifts, + onRemove, + onFavorite, +}) => { + const theme = useTheme(); + const isMobile = useMediaQuery(theme.breakpoints.down('sm')); + + if (friends.length === 0) { + return ; + } + + return ( + + + + + + + + + + + + + + + + + + + + + + + Username + {/* Total Games */} + + + + + + {/* Last Seen */} + + + + + + {/* Gift Sent */} + + + + + {friends.map((friend) => ( + + + onFavorite(friend)}> + {friend.isFavorite ? ( + + ) : ( + + )} + + + + + + + onRemove(friend)} + > + + + + + + + {friend.username ?? friend.id} + + + {shorthandNumber(friend.gamesPlayed)} + + + + {printRelativeDate({ + stamp: friend.lastSeen, + includeTime: false, + })} + + + + + + + + + + + ))} + +
+
+ ); +}; + +const EmptyFriendsPlaceholder = () => ( + `1px solid ${theme.palette.divider}`, + padding: 3, + }} + > + + + Add Friends + + + + Share up to {MAX_DAILY_GIFT_BOX_SHARES} gift boxes with your + friends every day. + + + VIP Members can share 3x gift boxes. + + + + Learn More + + +); diff --git a/libs/ui/pages/account/src/lib/friends-panel/friends-table/index.ts b/libs/ui/pages/account/src/lib/friends-panel/friends-table/index.ts new file mode 100644 index 000000000..e6b21b19c --- /dev/null +++ b/libs/ui/pages/account/src/lib/friends-panel/friends-table/index.ts @@ -0,0 +1 @@ +export * from './friends-table'; diff --git a/libs/ui/pages/account/src/lib/friends-panel/index.ts b/libs/ui/pages/account/src/lib/friends-panel/index.ts new file mode 100644 index 000000000..4bb2e0790 --- /dev/null +++ b/libs/ui/pages/account/src/lib/friends-panel/index.ts @@ -0,0 +1 @@ +export * from './friends-panel'; diff --git a/libs/ui/pages/account/src/lib/friends-panel/sections/add-friends-section.stories.tsx b/libs/ui/pages/account/src/lib/friends-panel/sections/add-friends-section.stories.tsx new file mode 100644 index 000000000..9ff1859d5 --- /dev/null +++ b/libs/ui/pages/account/src/lib/friends-panel/sections/add-friends-section.stories.tsx @@ -0,0 +1,33 @@ +import { Paper } from '@mui/material'; +import type { Meta } from '@storybook/react'; + +import { AddFriendsSection } from './add-friends-section'; + +type Story = Meta; + +const Default: Story = { + component: AddFriendsSection, + args: { + friendCode: 'ABC123', + }, + decorators: [ + (Story) => ( +
+ + + +
+ ), + ], +}; +export default Default; + +export const Primary: Story = { + args: {}, +}; diff --git a/libs/ui/pages/account/src/lib/friends-panel/sections/add-friends-section.tsx b/libs/ui/pages/account/src/lib/friends-panel/sections/add-friends-section.tsx new file mode 100644 index 000000000..66d3a9ed3 --- /dev/null +++ b/libs/ui/pages/account/src/lib/friends-panel/sections/add-friends-section.tsx @@ -0,0 +1,131 @@ +import { + Add, + Diversity1Outlined, + FavoriteBorder, + InfoOutlined, +} from '@mui/icons-material'; +import { + Box, + Button, + Divider, + Link, + TextField, + Typography, +} from '@mui/material'; +import { ValentinesHearts } from '@worksheets/icons/valentines'; +import { ClipboardText } from '@worksheets/ui/inputs'; +import { SocialButtons } from '@worksheets/ui/social-media'; +import { useRouter } from 'next/router'; + +import { BulletPoints } from '../../bullet-points'; +import { CollapsibleSection } from '../../collapsible-section'; +import { PanelFooter } from '../../panel-footer'; + +export const AddFriendsSection: React.FC<{ friendCode: string }> = (props) => { + const { query } = useRouter(); + const { open } = query; + const isOpen = open === 'add'; + + return ( + } + open={isOpen} + > + + + + Share your Friend Code + + + + + + + + Your friend code is unique to you and can be shared with anyone. + + + + + + Add Friends + + + Don't be shy! Ask your friends for their friend code and earn tokens + together. + + + + + + } + points={[ + `Ask your friends for their friend code and enter it above.`, + `If your friend has an account, you'll get a confirmation screen.`, + <> + If your friend doesn't have an account, get a{' '} + Referral Link to send them. + You'll both get tokens! + , + ]} + /> + + + + ); +}; diff --git a/libs/ui/pages/account/src/lib/friends-panel/sections/index.ts b/libs/ui/pages/account/src/lib/friends-panel/sections/index.ts new file mode 100644 index 000000000..287a54698 --- /dev/null +++ b/libs/ui/pages/account/src/lib/friends-panel/sections/index.ts @@ -0,0 +1 @@ +export * from './send-gifts-section'; diff --git a/libs/ui/pages/account/src/lib/friends-panel/sections/send-gifts-section.stories.tsx b/libs/ui/pages/account/src/lib/friends-panel/sections/send-gifts-section.stories.tsx new file mode 100644 index 000000000..940d92507 --- /dev/null +++ b/libs/ui/pages/account/src/lib/friends-panel/sections/send-gifts-section.stories.tsx @@ -0,0 +1,55 @@ +import { Paper } from '@mui/material'; +import { action } from '@storybook/addon-actions'; +import type { Meta } from '@storybook/react'; +import { hoursFromNow } from '@worksheets/util/time'; + +import { mockFriends } from '../../__mocks__'; +import { SendGiftsSection } from './send-gifts-section'; + +type Story = Meta; + +const Default: Story = { + component: SendGiftsSection, + args: { + refreshTimestamp: hoursFromNow(10).getTime(), + onRemove: action('onRemove'), + onFavorite: action('onFavorite'), + }, + decorators: [ + (Story) => ( +
+ + + +
+ ), + ], +}; +export default Default; + +export const Empty: Story = { + args: { + friends: [], + giftsRemaining: 5, + }, +}; + +export const Primary: Story = { + args: { + friends: mockFriends, + giftsRemaining: 3, + }, +}; + +export const NoMoreGifts: Story = { + args: { + friends: mockFriends, + giftsRemaining: 0, + }, +}; diff --git a/libs/ui/pages/account/src/lib/friends-panel/sections/send-gifts-section.tsx b/libs/ui/pages/account/src/lib/friends-panel/sections/send-gifts-section.tsx new file mode 100644 index 000000000..8a6cde453 --- /dev/null +++ b/libs/ui/pages/account/src/lib/friends-panel/sections/send-gifts-section.tsx @@ -0,0 +1,119 @@ +import { CheckCircleOutline, InfoOutlined } from '@mui/icons-material'; +import { Box, Divider, Link, Typography } from '@mui/material'; +import { + ValentinesGift, + ValentinesMailbox, +} from '@worksheets/icons/valentines'; +import { useRouter } from 'next/router'; +import React from 'react'; + +import { useTimeUntil } from '../../__hooks__/use-time-until'; +import { BulletPoints } from '../../bullet-points'; +import { CollapsibleSection } from '../../collapsible-section'; +import { MAX_DAILY_GIFT_BOX_SHARES } from '../../const'; +import { PanelFooter } from '../../panel-footer'; +import { RewardsTimer } from '../../rewards-timer'; +import { Friend } from '../../types'; +import { FriendsTable } from '../friends-table'; + +export const SendGiftsSection: React.FC<{ + giftsRemaining: number; + friends: Friend[]; + refreshTimestamp: number; + onRemove: (friend: Friend) => void; + onFavorite: (friend: Friend) => void; +}> = (props) => { + const { query } = useRouter(); + const { open } = query; + + const isOpen = open === 'send'; + + const timeRemaining = useTimeUntil(props.refreshTimestamp); + + const addFriendsLink = '/account/friends?open=add'; + + const canSendGifts = props.giftsRemaining > 0; + + return ( + + ) : ( + + ) + } + > + + + Share The Love + + + + {props.giftsRemaining} Gifts Available + + + + + + + + + + + } + points={[ + <> + Add friends by sharing your{' '} + Friend Code. + , + `You can send a gift to ${MAX_DAILY_GIFT_BOX_SHARES} friends per day.`, + `You earn a gift box for every friend you send a gift to.`, + <> + Open gift boxes on the{' '} + Tokens Page. + , + `Your favorite friends are listed at the top of the table.`, + ]} + /> + + + + + ); +}; diff --git a/libs/ui/pages/account/src/lib/modals/add-friend-modal.stories.tsx b/libs/ui/pages/account/src/lib/modals/add-friend-modal.stories.tsx new file mode 100644 index 000000000..8a44d8921 --- /dev/null +++ b/libs/ui/pages/account/src/lib/modals/add-friend-modal.stories.tsx @@ -0,0 +1,22 @@ +import { action } from '@storybook/addon-actions'; +import type { Meta } from '@storybook/react'; + +import { AddFriendModal } from './add-friend-modal'; + +type Story = Meta; + +const Default: Story = { + component: AddFriendModal, + args: { + onClose: action('onClose'), + onAdd: action('onAdd'), + }, +}; +export default Default; + +export const Primary: Story = { + args: { + open: true, + friendUsername: 'my-friend', + }, +}; diff --git a/libs/ui/pages/account/src/lib/modals/add-friend-modal.tsx b/libs/ui/pages/account/src/lib/modals/add-friend-modal.tsx new file mode 100644 index 000000000..334add79c --- /dev/null +++ b/libs/ui/pages/account/src/lib/modals/add-friend-modal.tsx @@ -0,0 +1,64 @@ +import { Add, Cancel } from '@mui/icons-material'; +import { Box, Button, IconButton, Typography } from '@mui/material'; +import { ValentinesHeart } from '@worksheets/icons/valentines'; +import { BaseModal, ModalWrapper } from '@worksheets/ui-core'; + +export const AddFriendModal: React.FC< + ModalWrapper<{ friendUsername: string; onAdd: () => void }> +> = ({ open, onClose, onAdd, friendUsername }) => { + const handleClose = () => onClose && onClose({}, 'escapeKeyDown'); + const handleAdd = () => { + onAdd(); + handleClose(); + }; + return ( + + + + + + + Add Friend + + + + We found them! +
+ Would you like to add {friendUsername} to your friends list? +
+ + + + +
+
+ ); +}; diff --git a/libs/ui/pages/account/src/lib/modals/claim-gift-modal.stories.tsx b/libs/ui/pages/account/src/lib/modals/claim-gift-modal.stories.tsx new file mode 100644 index 000000000..ffc9d5786 --- /dev/null +++ b/libs/ui/pages/account/src/lib/modals/claim-gift-modal.stories.tsx @@ -0,0 +1,21 @@ +import { action } from '@storybook/addon-actions'; +import type { Meta } from '@storybook/react'; + +import { ClaimGiftModal } from './claim-gift-modal'; + +type Story = Meta; + +const Default: Story = { + component: ClaimGiftModal, + args: { + onClose: action('onClose'), + }, +}; +export default Default; + +export const Primary: Story = { + args: { + open: true, + amount: 637, + }, +}; diff --git a/libs/ui/pages/account/src/lib/modals/claim-gift-modal.tsx b/libs/ui/pages/account/src/lib/modals/claim-gift-modal.tsx new file mode 100644 index 000000000..0a725df0f --- /dev/null +++ b/libs/ui/pages/account/src/lib/modals/claim-gift-modal.tsx @@ -0,0 +1,67 @@ +import { Cancel } from '@mui/icons-material'; +import Add from '@mui/icons-material/Add'; +import { Box, Button, IconButton, Typography } from '@mui/material'; +import { ValentinesGift } from '@worksheets/icons/valentines'; +import { BaseModal, ModalWrapper } from '@worksheets/ui-core'; + +export const ClaimGiftModal: React.FC> = ({ + open, + onClose, + amount, +}) => { + const handleClose = () => onClose && onClose({}, 'escapeKeyDown'); + return ( + + + + + + + Gift Box + + + + Congratulations! +
+ You've earned{' '} + + {amount} + {' '} + tokens. +
+ + + + +
+
+ ); +}; diff --git a/libs/ui/pages/account/src/lib/modals/index.ts b/libs/ui/pages/account/src/lib/modals/index.ts new file mode 100644 index 000000000..4affd7c29 --- /dev/null +++ b/libs/ui/pages/account/src/lib/modals/index.ts @@ -0,0 +1 @@ +export * from './claim-gift-modal'; diff --git a/libs/ui/pages/account/src/lib/modals/remove-friend-modal.stories.tsx b/libs/ui/pages/account/src/lib/modals/remove-friend-modal.stories.tsx new file mode 100644 index 000000000..424722e01 --- /dev/null +++ b/libs/ui/pages/account/src/lib/modals/remove-friend-modal.stories.tsx @@ -0,0 +1,21 @@ +import { action } from '@storybook/addon-actions'; +import type { Meta } from '@storybook/react'; + +import { RemoveFriendModal } from './remove-friend-modal'; + +type Story = Meta; + +const Default: Story = { + component: RemoveFriendModal, + args: { + onClose: action('onClose'), + onRemove: action('onRemove'), + }, +}; +export default Default; + +export const Primary: Story = { + args: { + open: true, + }, +}; diff --git a/libs/ui/pages/account/src/lib/modals/remove-friend-modal.tsx b/libs/ui/pages/account/src/lib/modals/remove-friend-modal.tsx new file mode 100644 index 000000000..1c4d06b4e --- /dev/null +++ b/libs/ui/pages/account/src/lib/modals/remove-friend-modal.tsx @@ -0,0 +1,64 @@ +import { Cancel, HeartBroken } from '@mui/icons-material'; +import { Box, Button, IconButton, Typography } from '@mui/material'; +import { ValentinesHeartbreak } from '@worksheets/icons/valentines'; +import { BaseModal, ModalWrapper } from '@worksheets/ui-core'; + +export const RemoveFriendModal: React.FC< + ModalWrapper<{ onRemove: () => void }> +> = ({ open, onClose, onRemove }) => { + const handleClose = () => onClose && onClose({}, 'escapeKeyDown'); + const handleRemove = () => { + onRemove(); + handleClose(); + }; + return ( + + + + + + + Remove Friend + + + + Splitting up isn't easy. +
+ Are you sure you want to remove this friend? +
+ + + + +
+
+ ); +}; diff --git a/libs/ui/pages/account/src/lib/modals/send-gift-modal.stories.tsx b/libs/ui/pages/account/src/lib/modals/send-gift-modal.stories.tsx new file mode 100644 index 000000000..246ac9a9a --- /dev/null +++ b/libs/ui/pages/account/src/lib/modals/send-gift-modal.stories.tsx @@ -0,0 +1,21 @@ +import { action } from '@storybook/addon-actions'; +import type { Meta } from '@storybook/react'; + +import { SendGiftModal } from './send-gift-modal'; + +type Story = Meta; + +const Default: Story = { + component: SendGiftModal, + args: { + onClose: action('onClose'), + onRemove: action('onRemove'), + }, +}; +export default Default; + +export const Primary: Story = { + args: { + open: true, + }, +}; diff --git a/libs/ui/pages/account/src/lib/modals/send-gift-modal.tsx b/libs/ui/pages/account/src/lib/modals/send-gift-modal.tsx new file mode 100644 index 000000000..b17beb642 --- /dev/null +++ b/libs/ui/pages/account/src/lib/modals/send-gift-modal.tsx @@ -0,0 +1,64 @@ +import { Cancel, MailOutline } from '@mui/icons-material'; +import { Box, Button, IconButton, Typography } from '@mui/material'; +import { ValentinesMailbox } from '@worksheets/icons/valentines'; +import { BaseModal, ModalWrapper } from '@worksheets/ui-core'; + +export const SendGiftModal: React.FC< + ModalWrapper<{ onRemove: () => void }> +> = ({ open, onClose, onRemove }) => { + const handleClose = () => onClose && onClose({}, 'escapeKeyDown'); + const handleRemove = () => { + onRemove(); + handleClose(); + }; + return ( + + + + + + + Send Gift + + + + You get a gift box for every friend you send a gift to. +
+ We'll let them know you sent it. +
+ + + + +
+
+ ); +}; diff --git a/libs/ui/pages/account/src/lib/panel-footer/index.ts b/libs/ui/pages/account/src/lib/panel-footer/index.ts new file mode 100644 index 000000000..7d81997ef --- /dev/null +++ b/libs/ui/pages/account/src/lib/panel-footer/index.ts @@ -0,0 +1 @@ +export * from './panel-footer'; diff --git a/libs/ui/pages/account/src/lib/panel-footer/panel-footer.tsx b/libs/ui/pages/account/src/lib/panel-footer/panel-footer.tsx new file mode 100644 index 000000000..afaa47c5a --- /dev/null +++ b/libs/ui/pages/account/src/lib/panel-footer/panel-footer.tsx @@ -0,0 +1,43 @@ +import { ArrowRightAlt } from '@mui/icons-material'; +import { Box, Button, Link, Typography } from '@mui/material'; +import React from 'react'; + +export const PanelFooter: React.FC<{ + learn: { + text: string; + href: string; + }; + action?: { + text: string; + href: string; + }; +}> = ({ learn, action }) => ( + + + Learn more about {learn.text} in the{' '} + + Help Center + + + {action && ( + + )} + +); diff --git a/libs/ui/pages/account/src/lib/panel-header/index.ts b/libs/ui/pages/account/src/lib/panel-header/index.ts new file mode 100644 index 000000000..a07286330 --- /dev/null +++ b/libs/ui/pages/account/src/lib/panel-header/index.ts @@ -0,0 +1 @@ +export * from './panel-header'; diff --git a/libs/ui/pages/account/src/lib/panel-header/panel-header.tsx b/libs/ui/pages/account/src/lib/panel-header/panel-header.tsx new file mode 100644 index 000000000..aa892d86a --- /dev/null +++ b/libs/ui/pages/account/src/lib/panel-header/panel-header.tsx @@ -0,0 +1,32 @@ +import { Box, Typography, useMediaQuery, useTheme } from '@mui/material'; + +export const PanelHeader: React.FC<{ + primary: string; + secondary: string; + icon: React.ReactNode; +}> = (props) => { + const theme = useTheme(); + const isMobile = useMediaQuery(theme.breakpoints.down('sm')); + return ( + + {props.primary} + + {props.icon} + {props.secondary} + + + ); +}; diff --git a/libs/ui/pages/account/src/lib/referral-info/how-referrals-work.tsx b/libs/ui/pages/account/src/lib/referral-info/how-referrals-work.tsx new file mode 100644 index 000000000..00d477d80 --- /dev/null +++ b/libs/ui/pages/account/src/lib/referral-info/how-referrals-work.tsx @@ -0,0 +1,27 @@ +import { Box, Link, Typography } from '@mui/material'; +import { ListItem, UnorderedList } from '@worksheets/ui-core'; + +import { + TOKENS_PER_REFERRAL_PLAY, + TOKENS_PER_FRIEND_PLAY_DAILY_LIMIT, + TOKENS_PER_REFERRAL_ACCOUNT, +} from '../const'; + +export const HowReferralsWork = () => ( + + + How It Works + + + + Earn {TOKENS_PER_REFERRAL_PLAY} tokens every time someone{' '} + plays a game with your referral link. (Up{' '} + {TOKENS_PER_FRIEND_PLAY_DAILY_LIMIT} tokens per day.) + + + Earn {TOKENS_PER_REFERRAL_ACCOUNT} tokens when someone makes an account + using your referral link. + + + +); diff --git a/libs/ui/pages/account/src/lib/referral-info/index.ts b/libs/ui/pages/account/src/lib/referral-info/index.ts new file mode 100644 index 000000000..130d8c057 --- /dev/null +++ b/libs/ui/pages/account/src/lib/referral-info/index.ts @@ -0,0 +1 @@ +export * from './referral-info'; diff --git a/libs/ui/pages/account/src/lib/referral-info/referral-info.tsx b/libs/ui/pages/account/src/lib/referral-info/referral-info.tsx new file mode 100644 index 000000000..8961fc7a6 --- /dev/null +++ b/libs/ui/pages/account/src/lib/referral-info/referral-info.tsx @@ -0,0 +1,73 @@ +import { Diversity1Outlined } from '@mui/icons-material'; +import { Box, Typography } from '@mui/material'; +import { ValentinesTicket } from '@worksheets/icons/valentines'; +import { ClipboardText } from '@worksheets/ui/inputs'; +import { SocialButtons } from '@worksheets/ui/social-media'; +import { shorthandNumber } from '@worksheets/util/numbers'; + +export const ReferralInfo: React.FC<{ + referrals: number; + link: string; + tokens: number; +}> = ({ referrals, link, tokens }) => { + return ( + + + + Share your link + + + + + + + + + + {shorthandNumber(referrals)} Accounts Referred + + + + + + {shorthandNumber(tokens)} Tokens Earned + + + + + ); +}; diff --git a/libs/ui/pages/account/src/lib/referrals-panel/index.ts b/libs/ui/pages/account/src/lib/referrals-panel/index.ts new file mode 100644 index 000000000..d611dc1ea --- /dev/null +++ b/libs/ui/pages/account/src/lib/referrals-panel/index.ts @@ -0,0 +1 @@ +export * from './referrals-panel'; diff --git a/libs/ui/pages/account/src/lib/referrals-panel/referral-table/index.ts b/libs/ui/pages/account/src/lib/referrals-panel/referral-table/index.ts new file mode 100644 index 000000000..bee75be4f --- /dev/null +++ b/libs/ui/pages/account/src/lib/referrals-panel/referral-table/index.ts @@ -0,0 +1 @@ +export * from './referrals-table'; diff --git a/libs/ui/pages/account/src/lib/referrals-panel/referral-table/referrals-table.stories.tsx b/libs/ui/pages/account/src/lib/referrals-panel/referral-table/referrals-table.stories.tsx new file mode 100644 index 000000000..f672b5791 --- /dev/null +++ b/libs/ui/pages/account/src/lib/referrals-panel/referral-table/referrals-table.stories.tsx @@ -0,0 +1,40 @@ +import Paper from '@mui/material/Paper'; +import type { Meta } from '@storybook/react'; + +import { mockReferrals } from '../../__mocks__'; +import { ReferralsTable } from './referrals-table'; + +type Story = Meta; + +const Default: Story = { + component: ReferralsTable, + args: {}, + decorators: [ + (Story) => ( +
+ + + +
+ ), + ], +}; +export default Default; + +export const Empty: Story = { + args: { + referrals: [], + }, +}; + +export const WithPeople: Story = { + args: { + referrals: mockReferrals, + }, +}; diff --git a/libs/ui/pages/account/src/lib/referrals-panel/referral-table/referrals-table.tsx b/libs/ui/pages/account/src/lib/referrals-panel/referral-table/referrals-table.tsx new file mode 100644 index 000000000..ce58f3a27 --- /dev/null +++ b/libs/ui/pages/account/src/lib/referrals-panel/referral-table/referrals-table.tsx @@ -0,0 +1,104 @@ +import { Box, Link, styled, Typography } from '@mui/material'; +import Table from '@mui/material/Table'; +import TableBody from '@mui/material/TableBody'; +import TableCell from '@mui/material/TableCell'; +import TableContainer from '@mui/material/TableContainer'; +import TableHead from '@mui/material/TableHead'; +import TableRow from '@mui/material/TableRow'; +import { ValentinesLetter } from '@worksheets/icons/valentines'; +import { printShortDate } from '@worksheets/util/time'; +import * as React from 'react'; + +import { Referral } from '../../types'; + +export type ReferralsTableProps = { + referrals: Referral[]; +}; + +const StyledBox = styled(Box)(({ theme }) => ({ + border: `1px solid ${theme.palette.divider}`, + borderRadius: theme.shape.borderRadius, + overflow: 'hidden', +})); + +export const ReferralsTable: React.FC = ({ + referrals, +}) => { + if (referrals.length === 0) { + return ; + } + return ( + + + + + Username + {/* Created On */} + Created On + + + + {referrals.map((referral) => ( + + + {referral.username ?? referral.id} + + + + {printShortDate(referral.createdAt)} + + + ))} + +
+
+ ); +}; + +const EmptyReferralsPlaceholder = () => ( + `1px solid ${theme.palette.divider}`, + padding: 3, + }} + > + + + Invite your friends + + + + Earn tokens when someone plays games with your link. + + + When they sign up, you'll both get a bonus! + + + + Learn More + + +); diff --git a/libs/ui/pages/account/src/lib/referrals-panel/referrals-panel.stories.tsx b/libs/ui/pages/account/src/lib/referrals-panel/referrals-panel.stories.tsx new file mode 100644 index 000000000..eafaeb24f --- /dev/null +++ b/libs/ui/pages/account/src/lib/referrals-panel/referrals-panel.stories.tsx @@ -0,0 +1,49 @@ +import Paper from '@mui/material/Paper'; +import type { Meta } from '@storybook/react'; + +import { mockReferrals } from '../__mocks__'; +import { ReferralsPanel } from './referrals-panel'; + +type Story = Meta; + +const Default: Story = { + component: ReferralsPanel, + args: {}, + decorators: [ + (Story) => ( +
+ + + +
+ ), + ], +}; + +export default Default; + +export const Empty: Story = { + args: { + referrals: [], + link: 'https://www.worksheets.dev', + tokens: 0, + refreshTimestamp: Date.now() + 1000 * 60 * 60 * 23, + gamesPlayed: 0, + }, +}; + +export const WithReferrals: Story = { + args: { + referrals: mockReferrals, + link: 'https://www.worksheets.dev', + tokens: 123561, + refreshTimestamp: Date.now() + 1000 * 60 * 60 * 23, + gamesPlayed: 33, + }, +}; diff --git a/libs/ui/pages/account/src/lib/referrals-panel/referrals-panel.tsx b/libs/ui/pages/account/src/lib/referrals-panel/referrals-panel.tsx new file mode 100644 index 000000000..c9a2e5f86 --- /dev/null +++ b/libs/ui/pages/account/src/lib/referrals-panel/referrals-panel.tsx @@ -0,0 +1,65 @@ +import { FavoriteBorder } from '@mui/icons-material'; +import { Box, Divider, Typography } from '@mui/material'; +import { ValentinesWorld } from '@worksheets/icons/valentines'; + +import { PanelFooter } from '../panel-footer'; +import { PanelHeader } from '../panel-header'; +import { Referral } from '../types'; +import { ReferredAccountsSection } from './sections'; +import { ReferredPlaysSection } from './sections/referred-plays-section'; +import { ShareYourLinkSection } from './sections/share-your-link-section'; + +export const ReferralsPanel: React.FC<{ + referrals: Referral[]; + link: string; + tokens: number; + refreshTimestamp: number; + gamesPlayed: number; +}> = ({ referrals, link, tokens, refreshTimestamp, gamesPlayed }) => { + return ( + + } + /> + + + + + + + + + + + + + + Help us grow by sharing your referral link with friends and family. + + + + + + ); +}; diff --git a/libs/ui/pages/account/src/lib/referrals-panel/sections/index.ts b/libs/ui/pages/account/src/lib/referrals-panel/sections/index.ts new file mode 100644 index 000000000..c44f06448 --- /dev/null +++ b/libs/ui/pages/account/src/lib/referrals-panel/sections/index.ts @@ -0,0 +1 @@ +export * from './referred-accounts-section'; diff --git a/libs/ui/pages/account/src/lib/referrals-panel/sections/referred-accounts-section.tsx b/libs/ui/pages/account/src/lib/referrals-panel/sections/referred-accounts-section.tsx new file mode 100644 index 000000000..fc0c54698 --- /dev/null +++ b/libs/ui/pages/account/src/lib/referrals-panel/sections/referred-accounts-section.tsx @@ -0,0 +1,62 @@ +import { AccountCircleOutlined, InfoOutlined } from '@mui/icons-material'; +import { Box, Link, Typography } from '@mui/material'; +import { ValentinesWings } from '@worksheets/icons/valentines'; +import React from 'react'; + +import { BulletPoints } from '../../bullet-points'; +import { CollapsibleSection } from '../../collapsible-section'; +import { TOKENS_PER_REFERRAL_ACCOUNT } from '../../const'; +import { PanelFooter } from '../../panel-footer'; +import { Referral } from '../../types'; +import { ReferralsTable } from '../referral-table'; + +export const ReferredAccountsSection: React.FC<{ + referrals: Referral[]; +}> = ({ referrals }) => { + return ( + } + Icon={ValentinesWings} + > + + My Referrals + + + + } + title={'How It Works'} + points={[ + `Earn ${TOKENS_PER_REFERRAL_ACCOUNT} tokens, when someone makes an account using your referral link.`, + <> + Earn {TOKENS_PER_REFERRAL_ACCOUNT} tokens when someone spends + money at the Charity Store using your + referral link. + , + `Users cannot remove their referral link once they've created an account.`, + `Users can add a referral link to their account if they don't have one.`, + ]} + /> + + + + + ); +}; diff --git a/libs/ui/pages/account/src/lib/referrals-panel/sections/referred-plays-section.tsx b/libs/ui/pages/account/src/lib/referrals-panel/sections/referred-plays-section.tsx new file mode 100644 index 000000000..f74df8126 --- /dev/null +++ b/libs/ui/pages/account/src/lib/referrals-panel/sections/referred-plays-section.tsx @@ -0,0 +1,132 @@ +import { + CheckCircleOutline, + InfoOutlined, + PendingOutlined, +} from '@mui/icons-material'; +import { + Box, + LinearProgress, + Link, + Typography, + useMediaQuery, + useTheme, +} from '@mui/material'; +import { ValentinesDiamond } from '@worksheets/icons/valentines'; +import { calculatePercentage, toPercentage } from '@worksheets/util/numbers'; +import React from 'react'; + +import { useTimeUntil } from '../../__hooks__/use-time-until'; +import { BulletPoints } from '../../bullet-points'; +import { CollapsibleSection } from '../../collapsible-section'; +import { MAX_TOKENS_FROM_REFERRAL_PLAYS } from '../../const'; +import { RewardsTimer } from '../../rewards-timer'; + +export const ReferredPlaysSection: React.FC<{ + tokensEarned: number; + refreshTimestamp: number; +}> = ({ tokensEarned, refreshTimestamp }) => { + const theme = useTheme(); + const isMobile = useMediaQuery(theme.breakpoints.down('sm')); + const timeRemaining = useTimeUntil(refreshTimestamp); + const isMax = tokensEarned >= MAX_TOKENS_FROM_REFERRAL_PLAYS; + return ( + + ) : ( + + ) + } + Icon={ValentinesDiamond} + > + + + + {tokensEarned} / {MAX_TOKENS_FROM_REFERRAL_PLAYS} Tokens Earned + + + {isMax ? ( + + ) : ( + + )} + + + {toPercentage(tokensEarned, MAX_TOKENS_FROM_REFERRAL_PLAYS)} + + + + theme.shape.borderRadius, + }} + /> + + + {isMax + ? `You have received all your tokens today!` + : `${MAX_TOKENS_FROM_REFERRAL_PLAYS - tokensEarned} more + tokens until you reach your daily limit.`} + + + + + } + title={'How It Works'} + points={[ + `Users don't need to make an account for you to get tokens.`, + `Earn 1 token for every game someone plays with your referral link.`, + `You can earn a maximum of ${MAX_TOKENS_FROM_REFERRAL_PLAYS} tokens per day.`, + <> + VIP Members earn x2 tokens when + someone plays a game, and have no daily limit. + , + ]} + /> + + + ); +}; diff --git a/libs/ui/pages/account/src/lib/referrals-panel/sections/share-your-link-section.tsx b/libs/ui/pages/account/src/lib/referrals-panel/sections/share-your-link-section.tsx new file mode 100644 index 000000000..8cdbb7537 --- /dev/null +++ b/libs/ui/pages/account/src/lib/referrals-panel/sections/share-your-link-section.tsx @@ -0,0 +1,62 @@ +import { InfoOutlined, LinkOutlined } from '@mui/icons-material'; +import { Box, Divider, Link } from '@mui/material'; +import { ValentinesWorld } from '@worksheets/icons/valentines'; +import React from 'react'; + +import { BulletPoints } from '../../bullet-points'; +import { CollapsibleSection } from '../../collapsible-section'; +import { TOKENS_PER_REFERRAL_ACCOUNT } from '../../const'; +import { PanelFooter } from '../../panel-footer'; +import { ReferralInfo } from '../../referral-info'; + +export const ShareYourLinkSection: React.FC<{ + referralLink: string; + numReferrals: number; + tokensEarned: number; +}> = ({ referralLink, numReferrals, tokensEarned }) => { + return ( + } + Icon={ValentinesWorld} + > + + + + + } + title="How It Works" + points={[ + `When a user clicks your referral link, they will be redirected to the website.`, + `If a user does not make an account, you can still earn tokens as long as they don't clear their cache.`, + `If they make an account, you will earn ${TOKENS_PER_REFERRAL_ACCOUNT} tokens.`, + <> + VIP Members get a significant boost + to their referral earnings. + , + ]} + /> + + + + + ); +}; diff --git a/libs/ui/pages/account/src/lib/rewards-timer/index.ts b/libs/ui/pages/account/src/lib/rewards-timer/index.ts new file mode 100644 index 000000000..6a3c95d3f --- /dev/null +++ b/libs/ui/pages/account/src/lib/rewards-timer/index.ts @@ -0,0 +1 @@ +export * from './rewards-timer'; diff --git a/libs/ui/pages/account/src/lib/rewards-timer/rewards-timer.tsx b/libs/ui/pages/account/src/lib/rewards-timer/rewards-timer.tsx new file mode 100644 index 000000000..78025d775 --- /dev/null +++ b/libs/ui/pages/account/src/lib/rewards-timer/rewards-timer.tsx @@ -0,0 +1,27 @@ +import { Box, Typography } from '@mui/material'; +import { WebHourglass } from '@worksheets/icons/web'; +import { FC } from 'react'; + +export const RewardsTimer: FC<{ timeRemaining: string }> = ({ + timeRemaining, +}) => { + return ( + + + Rewards refresh every 24 hours. + + + + {timeRemaining} + + + ); +}; diff --git a/libs/ui/pages/account/src/lib/tokens-panel/index.ts b/libs/ui/pages/account/src/lib/tokens-panel/index.ts new file mode 100644 index 000000000..f1843c7ee --- /dev/null +++ b/libs/ui/pages/account/src/lib/tokens-panel/index.ts @@ -0,0 +1 @@ +export * from './tokens-panel'; diff --git a/libs/ui/pages/account/src/lib/tokens-panel/sections/daily-reward-section.stories.tsx b/libs/ui/pages/account/src/lib/tokens-panel/sections/daily-reward-section.stories.tsx new file mode 100644 index 000000000..d7ab7364d --- /dev/null +++ b/libs/ui/pages/account/src/lib/tokens-panel/sections/daily-reward-section.stories.tsx @@ -0,0 +1,46 @@ +import { Paper } from '@mui/material'; +import { action } from '@storybook/addon-actions'; +import type { Meta } from '@storybook/react'; + +import { DailyRewardSection } from './daily-reward-section'; + +type Story = Meta; + +const Default: Story = { + component: DailyRewardSection, + args: { + onClaim: action('onClaim'), + }, + decorators: [ + (Story) => ( +
+ + + +
+ ), + ], +}; +export default Default; + +export const Unclaimed: Story = { + args: { + timeRemaining: '00:01:43', + claimed: false, + momentum: 2.5, + }, +}; + +export const Claimed: Story = { + args: { + timeRemaining: '00:01:43', + claimed: true, + momentum: 3.5, + }, +}; diff --git a/libs/ui/pages/account/src/lib/tokens-panel/sections/daily-reward-section.tsx b/libs/ui/pages/account/src/lib/tokens-panel/sections/daily-reward-section.tsx new file mode 100644 index 000000000..8475d1172 --- /dev/null +++ b/libs/ui/pages/account/src/lib/tokens-panel/sections/daily-reward-section.tsx @@ -0,0 +1,213 @@ +import { + AccessTime, + AccessTimeOutlined, + CardGiftcardOutlined, + CheckCircleOutline, + InfoOutlined, + LocalFireDepartment, +} from '@mui/icons-material'; +import { Box, Button, Link, Typography } from '@mui/material'; +import { ValentinesMail, ValentinesTicket } from '@worksheets/icons/valentines'; +import React from 'react'; + +import { BulletPoints, TitleText } from '../../bullet-points'; +import { CollapsibleSection } from '../../collapsible-section'; +import { + BASE_DAILY_REWARD, + MAX_MOMENTUM, + MOMENTUM_MULTIPLIER, +} from '../../const'; +import { PanelFooter } from '../../panel-footer'; + +export const DailyRewardSection: React.FC<{ + claimed: boolean; + momentum: number; + timeRemaining: string; + onClaim: () => void; +}> = ({ claimed, momentum, timeRemaining, onClaim }) => { + const isComplete = claimed; + + return ( + + ) : ( + + ) + } + > + + + {isComplete ? 'Come Back Later' : 'Daily Reward Available'}! + + + {isComplete ? ( + + + + Next Daily Reward in {timeRemaining} + + + + ) : ( + + + + Your reward expires in {timeRemaining} + + + + )} + + + + + {isComplete ? ( + 'You have claimed your daily reward' + ) : ( + + Gain{' '} + {' '} + momentum every time you claim the daily reward + + )} + + + + + } + title="Momentum" + /> + + + + } + points={[ + 'Rewards are available every 24 hours and reset at 12:00 AM UTC.', + `Gain ${MOMENTUM_MULTIPLIER}x momentum every time you claim a daily reward up to a maximum of ${MAX_MOMENTUM}x.`, + `Each daily reward is worth ${BASE_DAILY_REWARD} tokens multiplied by your current momentum ${momentum}.`, + `If you miss a claim, you won't be able to claim the reward and you will lose momentum.`, + <> + VIP members earn x2 the daily reward + and x2 the momentum. + , + ]} + /> + + + + + ); +}; + +const TokenPrizeMessage: React.FC<{ momentum: number }> = ({ momentum }) => ( + + Get{' '} + {' '} + {BASE_DAILY_REWARD} x + + {momentum} = + {' '} + {BASE_DAILY_REWARD * momentum} tokens for today's reward. + +); diff --git a/libs/ui/pages/account/src/lib/tokens-panel/sections/gift-box-section.stories.tsx b/libs/ui/pages/account/src/lib/tokens-panel/sections/gift-box-section.stories.tsx new file mode 100644 index 000000000..a8e49436b --- /dev/null +++ b/libs/ui/pages/account/src/lib/tokens-panel/sections/gift-box-section.stories.tsx @@ -0,0 +1,39 @@ +import { Paper } from '@mui/material'; +import type { Meta } from '@storybook/react'; + +import { GiftBoxSection } from './gift-box-section'; + +type Story = Meta; + +const Default: Story = { + component: GiftBoxSection, + args: {}, + decorators: [ + (Story) => ( +
+ + + +
+ ), + ], +}; +export default Default; + +export const Empty: Story = { + args: { + amount: 0, + }, +}; + +export const Available: Story = { + args: { + amount: 12, + }, +}; diff --git a/libs/ui/pages/account/src/lib/tokens-panel/sections/gift-box-section.tsx b/libs/ui/pages/account/src/lib/tokens-panel/sections/gift-box-section.tsx new file mode 100644 index 000000000..d9cd4a171 --- /dev/null +++ b/libs/ui/pages/account/src/lib/tokens-panel/sections/gift-box-section.tsx @@ -0,0 +1,126 @@ +import { + CardGiftcardOutlined, + CheckCircleOutline, + InfoOutlined, + NewReleasesOutlined, + RemoveCircleOutline, +} from '@mui/icons-material'; +import { Box, Button, Link, Typography } from '@mui/material'; +import { ValentinesGift } from '@worksheets/icons/valentines'; + +import { BulletPoints } from '../../bullet-points'; +import { CollapsibleSection } from '../../collapsible-section'; +import { + GIFT_BOX_DROP_RATE, + MAX_DAILY_GIFT_BOX_SHARES, + MAX_TOKENS_IN_GIFT_BOX, +} from '../../const'; +import { PanelFooter } from '../../panel-footer'; + +export const GiftBoxSection: React.FC<{ + amount: number; + onClaim: () => void; +}> = ({ amount, onClaim }) => { + const isComplete = amount === 0; + + return ( + + ) : ( + + ) + } + > + + + {isComplete ? 'No' : amount} Gift Boxes Available + + + + {isComplete + ? 'You have opened all of your gift boxes so far.' + : `You will earn 1 to ${MAX_TOKENS_IN_GIFT_BOX} tokens for each gift box you open.`} + + + + + + {GIFT_BOX_DROP_RATE}% chance to win a gift box while playing games. + + + } + points={[ + `Each gift box contains a random amount of tokens between 1 and ${MAX_TOKENS_IN_GIFT_BOX} tokens.`, + `Each game has a ${GIFT_BOX_DROP_RATE}% chance to drop a bonus gift box. Play games to earn gift boxes.`, + `Send up to ${MAX_DAILY_GIFT_BOX_SHARES} gift boxes every day and Get a free gift box every time you send a gift box to a friend.`, + <> + {' '} + Buy more gift boxes from the{' '} + Charity Store. + , + <> + {' '} + VIP members get x2 tokens per gift + box, x2 drop rates while playing games, and can send x2 gift + boxes. + , + ]} + /> + + + + ); +}; diff --git a/libs/ui/pages/account/src/lib/tokens-panel/sections/index.ts b/libs/ui/pages/account/src/lib/tokens-panel/sections/index.ts new file mode 100644 index 000000000..7fe3a3e2b --- /dev/null +++ b/libs/ui/pages/account/src/lib/tokens-panel/sections/index.ts @@ -0,0 +1,4 @@ +export * from './daily-reward-section'; +export * from './gift-box-section'; +export * from './invite-friends-section'; +export * from './play-games-section'; diff --git a/libs/ui/pages/account/src/lib/tokens-panel/sections/invite-friends-section.stories.tsx b/libs/ui/pages/account/src/lib/tokens-panel/sections/invite-friends-section.stories.tsx new file mode 100644 index 000000000..39ccfba34 --- /dev/null +++ b/libs/ui/pages/account/src/lib/tokens-panel/sections/invite-friends-section.stories.tsx @@ -0,0 +1,35 @@ +import { Paper } from '@mui/material'; +import type { Meta } from '@storybook/react'; + +import { InviteFriendsSection } from './invite-friends-section'; + +type Story = Meta; + +const Default: Story = { + component: InviteFriendsSection, + args: {}, + decorators: [ + (Story) => ( +
+ + + +
+ ), + ], +}; +export default Default; + +export const Primary: Story = { + args: { + referrals: 30, + tokens: 513, + link: 'https://charity.games/referral/137647813', + }, +}; diff --git a/libs/ui/pages/account/src/lib/tokens-panel/sections/invite-friends-section.tsx b/libs/ui/pages/account/src/lib/tokens-panel/sections/invite-friends-section.tsx new file mode 100644 index 000000000..f5bbd1641 --- /dev/null +++ b/libs/ui/pages/account/src/lib/tokens-panel/sections/invite-friends-section.tsx @@ -0,0 +1,79 @@ +import { AccountCircleOutlined, InfoOutlined } from '@mui/icons-material'; +import { Box, Link } from '@mui/material'; +import { ValentinesChat } from '@worksheets/icons/valentines'; +import React from 'react'; + +import { BulletPoints } from '../../bullet-points'; +import { CollapsibleSection } from '../../collapsible-section'; +import { + MAX_TOKENS_FROM_REFERRAL_PLAYS, + TOKENS_PER_REFERRAL_ACCOUNT, + TOKENS_PER_REFERRAL_PLAY, + TOKENS_PER_REFERRAL_PURCHASE, +} from '../../const'; +import { PanelFooter } from '../../panel-footer'; +import { ReferralInfo } from '../../referral-info'; +import { ReferralProgress } from '../types'; + +export const InviteFriendsSection: React.FC = ({ + link, + tokens, + referrals, +}) => ( + } + > + + + + } + title="How It Works" + points={[ + <> + Earn {TOKENS_PER_REFERRAL_PLAY} token when someone{' '} + plays a game using your referral link. + , + <> + Your referrals can earn you up to {MAX_TOKENS_FROM_REFERRAL_PLAYS}{' '} + tokens every day when they play games with + your link. + , + <> + Earn {TOKENS_PER_REFERRAL_ACCOUNT} tokens when someone makes an + account using your referral link. + , + <> + Earn {TOKENS_PER_REFERRAL_PURCHASE} tokens when someone spends money + at the Charity Store using your referral + link. + , + <> + VIP Members earn 2x tokens from + referrals and have no daily limit. + , + ]} + /> + + + + +); diff --git a/libs/ui/pages/account/src/lib/tokens-panel/sections/play-games-section.stories.tsx b/libs/ui/pages/account/src/lib/tokens-panel/sections/play-games-section.stories.tsx new file mode 100644 index 000000000..97677355f --- /dev/null +++ b/libs/ui/pages/account/src/lib/tokens-panel/sections/play-games-section.stories.tsx @@ -0,0 +1,46 @@ +import { Paper } from '@mui/material'; +import type { Meta } from '@storybook/react'; + +import { MAX_TOKENS_PER_DAY } from '../../const'; +import { PlayGamesSection } from './play-games-section'; + +type Story = Meta; + +const Default: Story = { + component: PlayGamesSection, + args: {}, + decorators: [ + (Story) => ( +
+ + + +
+ ), + ], +}; +export default Default; + +export const NoProgress: Story = { + args: { + tokens: 0, + }, +}; + +export const PartialProgress: Story = { + args: { + tokens: 123, + }, +}; + +export const CompleteProgress: Story = { + args: { + tokens: MAX_TOKENS_PER_DAY, + }, +}; diff --git a/libs/ui/pages/account/src/lib/tokens-panel/sections/play-games-section.tsx b/libs/ui/pages/account/src/lib/tokens-panel/sections/play-games-section.tsx new file mode 100644 index 000000000..8cf9425fd --- /dev/null +++ b/libs/ui/pages/account/src/lib/tokens-panel/sections/play-games-section.tsx @@ -0,0 +1,159 @@ +import { + CheckCircleOutline, + InfoOutlined, + NewReleasesOutlined, + PendingOutlined, + Star, +} from '@mui/icons-material'; +import { + Box, + LinearProgress, + Link, + Typography, + useMediaQuery, + useTheme, +} from '@mui/material'; +import { WebGamepad } from '@worksheets/icons/web'; +import { calculatePercentage, toPercentage } from '@worksheets/util/numbers'; + +import { BulletPoints } from '../../bullet-points'; +import { CollapsibleSection } from '../../collapsible-section'; +import { MAX_TOKENS_PER_DAY, MAX_TOKENS_PER_GAME } from '../../const'; +import { PanelFooter } from '../../panel-footer'; + +export const PlayGamesSection: React.FC<{ tokens: number }> = ({ tokens }) => { + const theme = useTheme(); + const isMobile = useMediaQuery(theme.breakpoints.down('sm')); + + const isComplete = tokens >= MAX_TOKENS_PER_DAY; + const isFresh = tokens === 0; + + const percentProgress: string = toPercentage(tokens, MAX_TOKENS_PER_DAY); + + return ( + + ) : isComplete ? ( + + ) : ( + + ) + } + > + + + + {tokens} / {MAX_TOKENS_PER_DAY} Tokens Earned + + + {isFresh ? ( + + ) : isComplete ? ( + + ) : ( + + )} + + + {percentProgress} + + + + theme.shape.borderRadius, + }} + /> + + + {isComplete + ? `You have received all your tokens today. Play again tomorrow!` + : `Play more games to earn ${MAX_TOKENS_PER_DAY - tokens} + tokens.`} + + } + points={[ + <> + Select a game from the Arcade, bonus + games earn x2 tokens. + , + `Press the play game button and you'll immediately earn ${MAX_TOKENS_PER_GAME} tokens.`, + `There is no time limit or score requirement to earn tokens.`, + <> + VIP Members earn twice as many + tokens per game and have no daily limit. + , + ]} + /> + } + points={[ + Sudoku, + Solitaire 2048, + Emoji War, + Hyper Wheel, + ]} + /> + + + + + ); +}; diff --git a/libs/ui/pages/account/src/lib/tokens-panel/tokens-panel.stories.tsx b/libs/ui/pages/account/src/lib/tokens-panel/tokens-panel.stories.tsx new file mode 100644 index 000000000..eaf0eb3aa --- /dev/null +++ b/libs/ui/pages/account/src/lib/tokens-panel/tokens-panel.stories.tsx @@ -0,0 +1,66 @@ +import { Paper } from '@mui/material'; +import { action } from '@storybook/addon-actions'; +import type { Meta } from '@storybook/react'; + +import { MAX_TOKENS_PER_DAY } from '../const'; +import { TokensPanel } from './tokens-panel'; + +type Story = Meta; + +const Default: Story = { + component: TokensPanel, + args: { + onClaimDailyGift: action('onClaimDailyGift'), + onClaimGiftBox: action('onClaimGiftBox'), + }, + decorators: [ + (Story) => ( +
+ + + +
+ ), + ], +}; +export default Default; + +export const Empty: Story = { + args: { + tokens: 0, + // 5 minutes from now, in milliseconds + refreshTimestamp: Date.now() + 1000 * 60 * 300, + gameProgressTokens: 0, + giftBoxes: 0, + dailyGiftMomentum: 1, + referralProgress: { + referrals: 0, + tokens: 0, + link: 'https://www.charity.games/referral/137647813', + }, + claimedDailyGift: false, + }, +}; + +export const Primary: Story = { + args: { + tokens: 100, + // 5 minutes from now, in milliseconds + refreshTimestamp: Date.now() + 1000 * 60 * 5, + gameProgressTokens: MAX_TOKENS_PER_DAY - 23, + giftBoxes: 3, + dailyGiftMomentum: 2.5, + referralProgress: { + referrals: 30, + tokens: 513, + link: 'https://charity.games/referral/137647813', + }, + claimedDailyGift: false, + }, +}; diff --git a/libs/ui/pages/account/src/lib/tokens-panel/tokens-panel.tsx b/libs/ui/pages/account/src/lib/tokens-panel/tokens-panel.tsx new file mode 100644 index 000000000..bb02a1e28 --- /dev/null +++ b/libs/ui/pages/account/src/lib/tokens-panel/tokens-panel.tsx @@ -0,0 +1,93 @@ +import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined'; +import { Box, Divider, Link, Typography } from '@mui/material'; +import { ValentinesTicket } from '@worksheets/icons/valentines'; +import { FC } from 'react'; + +import { useTimeUntil } from '../__hooks__/use-time-until'; +import { PanelHeader } from '../panel-header'; +import { RewardsTimer } from '../rewards-timer'; +import { + DailyRewardSection, + GiftBoxSection, + InviteFriendsSection, + PlayGamesSection, +} from './sections'; +import { ReferralProgress } from './types'; + +export const TokensPanel: FC<{ + tokens: number; + refreshTimestamp: number; + gameProgressTokens: number; + referralProgress: ReferralProgress; + giftBoxes: number; + claimedDailyGift: boolean; + dailyGiftMomentum: number; + onClaimDailyGift: () => void; + onClaimGiftBox: () => void; +}> = ({ + tokens, + refreshTimestamp, + gameProgressTokens, + giftBoxes, + referralProgress, + dailyGiftMomentum, + claimedDailyGift, + onClaimGiftBox, + onClaimDailyGift, +}) => { + const timeRemaining = useTimeUntil(refreshTimestamp); + + return ( + + } + /> + + + + + + + + + + + + + + + + + + Redeem tokens for real world rewards at the{' '} + prize wall or at the{' '} + auction house. + + + + Learn more about tokens and rewards in the{' '} + Help Center. + + + ); +}; diff --git a/libs/ui/pages/account/src/lib/tokens-panel/types.ts b/libs/ui/pages/account/src/lib/tokens-panel/types.ts new file mode 100644 index 000000000..79253d6aa --- /dev/null +++ b/libs/ui/pages/account/src/lib/tokens-panel/types.ts @@ -0,0 +1,5 @@ +export type ReferralProgress = { + referrals: number; + tokens: number; + link: string; +}; diff --git a/libs/ui/pages/account/src/lib/types.ts b/libs/ui/pages/account/src/lib/types.ts index 752e671da..5f2e4b4ec 100644 --- a/libs/ui/pages/account/src/lib/types.ts +++ b/libs/ui/pages/account/src/lib/types.ts @@ -11,3 +11,20 @@ export const basicGameSubmissionSchema = z.object({ }); export type BasicGameSubmission = z.infer; + +export type Referral = { + id: string; + username?: string; + createdAt: number; + gamesRemaining: number; + tokensEarned: number; +}; + +export type Friend = { + id: string; + username?: string; + lastSeen: number; + gamesPlayed: number; + isFavorite: boolean; + hasSentGiftToday: boolean; +}; diff --git a/libs/ui/pages/game/src/lib/modals/share-game/share-game.tsx b/libs/ui/pages/game/src/lib/modals/share-game/share-game.tsx index 778b9d5f5..9cf2626f0 100644 --- a/libs/ui/pages/game/src/lib/modals/share-game/share-game.tsx +++ b/libs/ui/pages/game/src/lib/modals/share-game/share-game.tsx @@ -3,12 +3,12 @@ import Box from '@mui/material/Box'; import Button, { ButtonProps } from '@mui/material/Button'; import IconButton from '@mui/material/IconButton'; import Typography from '@mui/material/Typography'; +import { shareGame, SocialButtons } from '@worksheets/ui/social-media'; import { BaseModal, ModalWrapper } from '@worksheets/ui-core'; import { SerializableGameSchema } from '@worksheets/util/types'; import { FC } from 'react'; import { ClipboardText } from './clipboard-text'; -import { SocialButtons } from './social-buttons'; export const ShareGameModal: FC< ModalWrapper<{ @@ -42,7 +42,7 @@ export const ShareGameModal: FC< Share this game - + @@ -53,6 +53,24 @@ export const ShareGameModal: FC< ); }; +const SocialButtonsWrapper: FC<{ title: string; url: string }> = ({ + title, + url, +}) => { + const encodedProps = { + title: encodeURIComponent(title), + url: encodeURIComponent(url), + }; + + return ( + + ); +}; + const CustomButton: FC> = (props) => { return (