diff --git a/package.json b/package.json index 4b36ed1..eb50d51 100644 --- a/package.json +++ b/package.json @@ -22,14 +22,20 @@ "@polkadot/extension-dapp": "^0.47.3", "@polkadot/extension-inject": "^0.47.3", "@radix-ui/react-alert-dialog": "^1.0.5", + "@radix-ui/react-dialog": "^1.0.5", + "@radix-ui/react-toast": "^1.1.5", "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", "dotenv": "^16.4.5", + "embla-carousel": "^8.1.5", + "embla-carousel-react": "^8.1.5", "lucide-react": "^0.376.0", "next": "14.2.3", + "next-themes": "^0.3.0", "react": "^18.3.1", "react-dom": "^18.3.1", "react-hook-form": "^7.51.4", + "sonner": "^1.4.41", "tailwind-merge": "^2.3.0", "tailwindcss-animate": "^1.0.7", "zod": "^3.23.6" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cf6dd2a..3f27c93 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -26,6 +26,12 @@ dependencies: '@radix-ui/react-alert-dialog': specifier: ^1.0.5 version: 1.0.5(@types/react-dom@18.3.0)(@types/react@18.3.1)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-dialog': + specifier: ^1.0.5 + version: 1.0.5(@types/react-dom@18.3.0)(@types/react@18.3.1)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-toast': + specifier: ^1.1.5 + version: 1.1.5(@types/react-dom@18.3.0)(@types/react@18.3.1)(react-dom@18.3.1)(react@18.3.1) class-variance-authority: specifier: ^0.7.0 version: 0.7.0 @@ -35,12 +41,21 @@ dependencies: dotenv: specifier: ^16.4.5 version: 16.4.5 + embla-carousel: + specifier: ^8.1.5 + version: 8.1.5 + embla-carousel-react: + specifier: ^8.1.5 + version: 8.1.5(react@18.3.1) lucide-react: specifier: ^0.376.0 version: 0.376.0(react@18.3.1) next: specifier: 14.2.3 version: 14.2.3(react-dom@18.3.1)(react@18.3.1) + next-themes: + specifier: ^0.3.0 + version: 0.3.0(react-dom@18.3.1)(react@18.3.1) react: specifier: ^18.3.1 version: 18.3.1 @@ -50,6 +65,9 @@ dependencies: react-hook-form: specifier: ^7.51.4 version: 7.51.4(react@18.3.1) + sonner: + specifier: ^1.4.41 + version: 1.4.41(react-dom@18.3.1)(react@18.3.1) tailwind-merge: specifier: ^2.3.0 version: 2.3.0 @@ -877,6 +895,30 @@ packages: react-dom: 18.3.1(react@18.3.1) dev: false + /@radix-ui/react-collection@1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.1)(react-dom@18.3.1)(react@18.3.1): + resolution: {integrity: sha512-3SzW+0PW7yBBoQlT8wNcGtaxaD0XSu0uLUFgrtHY08Acx05TaHaOmVLR73c0j/cqpDy53KBMO7s0dx2wmOIDIA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@babel/runtime': 7.24.4 + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.3.1)(react@18.3.1) + '@radix-ui/react-context': 1.0.1(@types/react@18.3.1)(react@18.3.1) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.1)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-slot': 1.0.2(@types/react@18.3.1)(react@18.3.1) + '@types/react': 18.3.1 + '@types/react-dom': 18.3.0 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + dev: false + /@radix-ui/react-compose-refs@1.0.1(@types/react@18.3.1)(react@18.3.1): resolution: {integrity: sha512-fDSBgd44FKHa1FRMU59qBMPFcl2PZE+2nmqunj+BWFyYYjnhIDWL2ItDs3rrbJDQOtzt5nIebLCQc4QRfz6LJw==} peerDependencies: @@ -1095,6 +1137,38 @@ packages: react: 18.3.1 dev: false + /@radix-ui/react-toast@1.1.5(@types/react-dom@18.3.0)(@types/react@18.3.1)(react-dom@18.3.1)(react@18.3.1): + resolution: {integrity: sha512-fRLn227WHIBRSzuRzGJ8W+5YALxofH23y0MlPLddaIpLpCDqdE0NZlS2NRQDRiptfxDeeCjgFIpexB1/zkxDlw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@babel/runtime': 7.24.4 + '@radix-ui/primitive': 1.0.1 + '@radix-ui/react-collection': 1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.1)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.3.1)(react@18.3.1) + '@radix-ui/react-context': 1.0.1(@types/react@18.3.1)(react@18.3.1) + '@radix-ui/react-dismissable-layer': 1.0.5(@types/react-dom@18.3.0)(@types/react@18.3.1)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-portal': 1.0.4(@types/react-dom@18.3.0)(@types/react@18.3.1)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-presence': 1.0.1(@types/react-dom@18.3.0)(@types/react@18.3.1)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.1)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.3.1)(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.3.1)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.0.1(@types/react@18.3.1)(react@18.3.1) + '@radix-ui/react-visually-hidden': 1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.1)(react-dom@18.3.1)(react@18.3.1) + '@types/react': 18.3.1 + '@types/react-dom': 18.3.0 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + dev: false + /@radix-ui/react-use-callback-ref@1.0.1(@types/react@18.3.1)(react@18.3.1): resolution: {integrity: sha512-D94LjX4Sp0xJFVaoQOd3OO9k7tpBYNOXdVhkltUbGv2Qb9OXdrg/CpsjlZv7ia14Sylv398LswWBVVu5nqKzAQ==} peerDependencies: @@ -1153,6 +1227,27 @@ packages: react: 18.3.1 dev: false + /@radix-ui/react-visually-hidden@1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.1)(react-dom@18.3.1)(react@18.3.1): + resolution: {integrity: sha512-D4w41yN5YRKtu464TLnByKzMDG/JlMPHtfZgQAu9v6mNakUqGUI9vUrfQKz8NK41VMm/xbZbh76NUTVtIYqOMA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@babel/runtime': 7.24.4 + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.1)(react-dom@18.3.1)(react@18.3.1) + '@types/react': 18.3.1 + '@types/react-dom': 18.3.0 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + dev: false + /@rushstack/eslint-patch@1.10.2: resolution: {integrity: sha512-hw437iINopmQuxWPSUEvqE56NCPsiU8N4AYtfHmJFckclktzK9YQJieD3XkDCDH4OjL+C7zgPUh73R/nrcHrqw==} dev: true @@ -1767,6 +1862,28 @@ packages: /eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + /embla-carousel-react@8.1.5(react@18.3.1): + resolution: {integrity: sha512-xFmfxgJd7mpWDHQ4iyK1Qs+5BTTwu4bkn+mSROKiUH9nKpPHTeilQ+rpeQDCHRrAPeshD67aBk0/p6FxWxXsng==} + peerDependencies: + react: ^16.8.0 || ^17.0.1 || ^18.0.0 + dependencies: + embla-carousel: 8.1.5 + embla-carousel-reactive-utils: 8.1.5(embla-carousel@8.1.5) + react: 18.3.1 + dev: false + + /embla-carousel-reactive-utils@8.1.5(embla-carousel@8.1.5): + resolution: {integrity: sha512-76uZTrSaEGGta+qpiGkMFlLK0I7N04TdjZ2obrBhyggYIFDWlxk1CriIEmt2lisLNsa1IYXM85kr863JoCMSyg==} + peerDependencies: + embla-carousel: 8.1.5 + dependencies: + embla-carousel: 8.1.5 + dev: false + + /embla-carousel@8.1.5: + resolution: {integrity: sha512-R6xTf7cNdR2UTNM6/yUPZlJFRmZSogMiRjJ5vXHO65II5MoUlrVYUAP0fHQei/py82Vf15lj+WI+QdhnzBxA2g==} + dev: false + /emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} @@ -2913,6 +3030,16 @@ packages: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} dev: true + /next-themes@0.3.0(react-dom@18.3.1)(react@18.3.1): + resolution: {integrity: sha512-/QHIrsYpd6Kfk7xakK4svpDI5mmXP0gfvCoJdGpZQ2TOrQZmsW0QxjaiLn8wbIKjtm4BTSqLoix4lxYYOnLJ/w==} + peerDependencies: + react: ^16.8 || ^17 || ^18 + react-dom: ^16.8 || ^17 || ^18 + dependencies: + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + dev: false + /next@14.2.3(react-dom@18.3.1)(react@18.3.1): resolution: {integrity: sha512-dowFkFTR8v79NPJO4QsBUtxv0g9BrS/phluVpMAt2ku7H+cbcBJlopXjkWlwxrk/xGqMemr7JkGPGemPrLLX7A==} engines: {node: '>=18.17.0'} @@ -3582,6 +3709,16 @@ packages: dev: false optional: true + /sonner@1.4.41(react-dom@18.3.1)(react@18.3.1): + resolution: {integrity: sha512-uG511ggnnsw6gcn/X+YKkWPo5ep9il9wYi3QJxHsYe7yTZ4+cOd1wuodOUmOpFuXL+/RE3R04LczdNCDygTDgQ==} + peerDependencies: + react: ^18.0.0 + react-dom: ^18.0.0 + dependencies: + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + dev: false + /source-map-js@1.2.0: resolution: {integrity: sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==} engines: {node: '>=0.10.0'} diff --git a/public/images/profile/profile.png b/public/images/profile/profile.png new file mode 100644 index 0000000..24926b1 Binary files /dev/null and b/public/images/profile/profile.png differ diff --git a/src/app/(dashboard)/home/_components/game-mode.tsx b/src/app/(dashboard)/home/_components/game-mode.tsx new file mode 100644 index 0000000..1120f19 --- /dev/null +++ b/src/app/(dashboard)/home/_components/game-mode.tsx @@ -0,0 +1,174 @@ +import { Icons } from '@/components/icons'; +import { Button } from '@/components/ui/button'; +import Form, { useZodForm } from '@/components/ui/form'; +import { Input } from '@/components/ui/input'; +import useLiveCountdown from '@/hooks/use-live-countdown'; +import { cn } from '@/lib/utils'; +import { gameSchema } from '@/lib/validations/game'; +import Image from 'next/image'; +import { + Carousel, + CarouselMainContainer, + CarouselNext, + CarouselPrevious, + SliderMainItem, + CarouselThumbsContainer, + SliderThumbItem +} from '@/components/ui/extension-carousel'; +import { Dispatch, SetStateAction, useEffect, useTransition } from 'react'; + +interface GameProps { + setDisplay: Dispatch>; + close: () => void; +} + +export default function GameMode({ setDisplay, close }: GameProps) { + const [isPending, startTransition] = useTransition(); + + const { seconds } = useLiveCountdown(60); + + const form = useZodForm({ + schema: gameSchema + }); + + function onSubmit(data: any) { + startTransition(() => { + setDisplay('success'); + }); + } + + useEffect(() => { + if (seconds <= 0) { + setDisplay('fail'); + } + }); + + return ( +
+
+ +
+ points + 1500 +
+
+
+ {/* image */} +
+ {/* */} + + + {Array.from({ length: 5 }).map((_, index) => ( + + + + ))} + + + {Array.from({ length: 5 }).map((_, index) => ( + + + + ))} + + + +
+ {/* data */} +
+
+

Property 1

+ +

One bed luxury apartment,

+
+ + +
+ + + +
+
+

Key features

+

+ Private balcony. Communal roof terrace. Resident's concierge service. Close + proximity to green spaces. 999 year lease with peppercorn ground rent +

+
+
+
+ + +
+
+
+ {/* timer */} +
+
+ + {seconds > 0 && `${seconds}`} + +
+
+
+
+ ); +} + +interface DescriptionProps extends React.HTMLAttributes { + title: string; + description?: string; +} + +const DescriptionList = ({ className, title, description }: DescriptionProps) => { + return ( +
+
{title}:
+
{description}
+
+ ); +}; diff --git a/src/app/(dashboard)/home/_components/game.tsx b/src/app/(dashboard)/home/_components/game.tsx deleted file mode 100644 index 91a0e86..0000000 --- a/src/app/(dashboard)/home/_components/game.tsx +++ /dev/null @@ -1,26 +0,0 @@ -'use client'; - -import { - AlertDialog, - AlertDialogContent, - AlertDialogTrigger -} from '@/components/ui/alert-dialog'; -import { Button } from '@/components/ui/button'; -import { useState } from 'react'; - -export default function Play() { - const [isDialogOpen, setIsDialogOpen] = useState(false); - - return ( - - - - - -
Hello
-
-
- ); -} diff --git a/src/app/(dashboard)/home/_components/live-game-container.tsx b/src/app/(dashboard)/home/_components/live-game-container.tsx new file mode 100644 index 0000000..4c955f0 --- /dev/null +++ b/src/app/(dashboard)/home/_components/live-game-container.tsx @@ -0,0 +1,83 @@ +'use client'; + +import { ReactNode, useState } from 'react'; +import { Sheet, SheetClose, SheetContent, SheetTrigger } from '@/components/ui/sheet'; +import { Button } from '@/components/ui/button'; +import GameMode from './game-mode'; +import { GuessFail, GuessPass } from './success-failure'; +import { getApi } from '@/lib/polkadot'; +import { web3Enable, web3FromAddress } from '@polkadot/extension-dapp'; +interface IGamePlaySection { + [key: string]: ReactNode; +} + +type Player = { + Player: 1; +}; + +type Practice = { + Practice: 0; +}; + +type GameType = Player | Practice; + +export default function LiveGamePlay({ type }: { type: GameType }) { + const [openGameSheet, setOpenGameSheet] = useState(false); + const [display, setDisplay] = useState<'play' | 'success' | 'fail'>('play'); + + function closeGameSheet() { + setOpenGameSheet(false); + } + + const game: IGamePlaySection = { + play: , + success: , + fail: + }; + + const address = '5CSFhuBPkG1SDyHseSHh23Kg89oYVysjRmXH5ea3F3fzEyx5'; + const playGame = async (gameType: GameType) => { + try { + const api = await getApi(); + const extensions = await web3Enable('RealXDEal'); + const injected = await web3FromAddress(address); + const extrinsic = api.tx.gameModule.playGame(gameType); + const signer = injected.signer; + + const unsub = await extrinsic.signAndSend(address, { signer }, result => { + if (result.status.isInBlock) { + console.log(`Completed at block hash #${result.status.asInBlock.toString()}`); + } else if (result.status.isBroadcast) { + console.log('Broadcasting the guess...'); + } + }); + + console.log('Transaction sent:', unsub); + } catch (error) { + console.error('Failed to submit guess:', error); + } + }; + + return ( + + + + + { + e.preventDefault(); + }} + onEscapeKeyDown={e => { + e.preventDefault(); + }} + > +
+ {game[display]} +
+
+
+ ); +} diff --git a/src/app/(dashboard)/home/_components/success-failure.tsx b/src/app/(dashboard)/home/_components/success-failure.tsx new file mode 100644 index 0000000..95f5397 --- /dev/null +++ b/src/app/(dashboard)/home/_components/success-failure.tsx @@ -0,0 +1,58 @@ +import { Button } from '@/components/ui/button'; +import Image from 'next/image'; + +interface GameProps { + close: () => void; +} + +export function GuessPass({ close }: GameProps) { + return ( +
+
+ points + 1500 +
+ +
+
+ +
+ +
+

Great guess!!

+

+ You have won this property NFT and have gained +10 points{' '} +

+
+ +
+
+ ); +} +export function GuessFail({ close }: GameProps) { + return ( +
+
+ points + 1500 +
+ +
+
+ +
+

Sorry wrong guess

+

You have lost -10 points

+
+ +
+
+ ); +} diff --git a/src/app/(dashboard)/home/page.tsx b/src/app/(dashboard)/home/page.tsx index e698a08..3a91582 100644 --- a/src/app/(dashboard)/home/page.tsx +++ b/src/app/(dashboard)/home/page.tsx @@ -1,4 +1,4 @@ -'use client'; +// 'use client'; import { Card, CardWithoutHeading } from '@/components/cards/card'; import { Shell } from '@/components/shell'; import { TaskCard } from '../tasks/page'; @@ -9,6 +9,8 @@ import { LeadBoardCard } from '@/components/cards/leadboard-card'; import Link from 'next/link'; import { getApi } from '@/lib/polkadot'; import { web3Enable, web3FromAddress } from '@polkadot/extension-dapp'; +import LiveGamePlay from './_components/live-game-container'; +import { getAvailableNFTs } from '@/lib/queries'; type Player = { Player: 1; @@ -20,32 +22,33 @@ type Practice = { type GameType = Player | Practice; -export default function App() { - const address = '5CSFhuBPkG1SDyHseSHh23Kg89oYVysjRmXH5ea3F3fzEyx5'; - const playGame = async (gameType: GameType) => { - try { - const api = await getApi(); - const extensions = await web3Enable('RealXDEal'); - const injected = await web3FromAddress(address); - const extrinsic = api.tx.gameModule.playGame(gameType); - const signer = injected.signer; +export default async function App() { + // getAvailableNFTs(1); - const unsub = await extrinsic.signAndSend(address, { signer }, result => { - if (result.status.isInBlock) { - console.log(`Completed at block hash #${result.status.asInBlock.toString()}`); - } else if (result.status.isBroadcast) { - console.log('Broadcasting the guess...'); - } - }); - - console.log('Transaction sent:', unsub); - } catch (error) { - console.error('Failed to submit guess:', error); - } - }; + // console.log(process.env.NEXT_PUBLIC_RPC); return ( +
+
+ {/* profile */} +
+ + 1Ay00011DY... + +
+
+
+ Points + 1500 +
+
@@ -55,16 +58,17 @@ export default function App() {
- + */}
- {/* */} - - {/* */} + */}
diff --git a/src/app/globals.css b/src/app/globals.css index 534586e..d156d31 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -1,81 +1,79 @@ @tailwind base; - @tailwind components; - @tailwind utilities; +@tailwind components; +@tailwind utilities; - @layer base { - :root { - --background: 0 0% 100%; - --foreground: 222.2 84% 4.9%; +@layer base { + :root { + --background: 0 0% 100%; + --foreground: 222.2 84% 4.9%; - --card: 0 0% 100%; - --card-foreground: 222.2 84% 4.9%; + --card: 0 0% 100%; + --card-foreground: 222.2 84% 4.9%; - --popover: 0 0% 100%; - --popover-foreground: 222.2 84% 4.9%; + --popover: 0 0% 100%; + --popover-foreground: 222.2 84% 4.9%; - --primary: 222.2 47.4% 11.2%; - --primary-foreground: 210 40% 98%; + --primary: 222.2 47.4% 11.2%; + --primary-foreground: 210 40% 98%; - --secondary: 210 40% 96.1%; - --secondary-foreground: 222.2 47.4% 11.2%; + --secondary: 210 40% 96.1%; + --secondary-foreground: 222.2 47.4% 11.2%; - --muted: 210 40% 96.1%; - --muted-foreground: 215.4 16.3% 46.9%; + --muted: 210 40% 96.1%; + --muted-foreground: 215.4 16.3% 46.9%; - --accent: 210 40% 96.1%; - --accent-foreground: 222.2 47.4% 11.2%; + --accent: 210 40% 96.1%; + --accent-foreground: 222.2 47.4% 11.2%; - --destructive: 0 84.2% 60.2%; - --destructive-foreground: 210 40% 98%; + --destructive: 0 84.2% 60.2%; + --destructive-foreground: 210 40% 98%; - --border: 214.3 31.8% 91.4%; - --input: 214.3 31.8% 91.4%; - --ring: 222.2 84% 4.9%; + --border: 214.3 31.8% 91.4%; + --input: 214.3 31.8% 91.4%; + --ring: 222.2 84% 4.9%; - --radius: 0.5rem; - } + --radius: 0.5rem; + } - .dark { - --background: 222.2 84% 4.9%; - --foreground: 210 40% 98%; + .dark { + --background: 222.2 84% 4.9%; + --foreground: 210 40% 98%; - --card: 222.2 84% 4.9%; - --card-foreground: 210 40% 98%; + --card: 222.2 84% 4.9%; + --card-foreground: 210 40% 98%; - --popover: 222.2 84% 4.9%; - --popover-foreground: 210 40% 98%; + --popover: 222.2 84% 4.9%; + --popover-foreground: 210 40% 98%; - --primary: 210 40% 98%; - --primary-foreground: 222.2 47.4% 11.2%; + --primary: 210 40% 98%; + --primary-foreground: 222.2 47.4% 11.2%; - --secondary: 217.2 32.6% 17.5%; - --secondary-foreground: 210 40% 98%; + --secondary: 217.2 32.6% 17.5%; + --secondary-foreground: 210 40% 98%; - --muted: 217.2 32.6% 17.5%; - --muted-foreground: 215 20.2% 65.1%; + --muted: 217.2 32.6% 17.5%; + --muted-foreground: 215 20.2% 65.1%; - --accent: 217.2 32.6% 17.5%; - --accent-foreground: 210 40% 98%; + --accent: 217.2 32.6% 17.5%; + --accent-foreground: 210 40% 98%; - --destructive: 0 62.8% 30.6%; - --destructive-foreground: 210 40% 98%; + --destructive: 0 62.8% 30.6%; + --destructive-foreground: 210 40% 98%; - --border: 217.2 32.6% 17.5%; - --input: 217.2 32.6% 17.5%; - --ring: 212.7 26.8% 83.9%; - } + --border: 217.2 32.6% 17.5%; + --input: 217.2 32.6% 17.5%; + --ring: 212.7 26.8% 83.9%; } +} - @layer base { - body { - @apply bg-background text-foreground; - } +@layer base { + body { + @apply bg-background text-foreground; } - .game-play-bg { - background-image: url("/images/gameplay.png"); - background-size:cover; - background-position: center; - background-repeat: no-repeat; - background-size: cover; - } \ No newline at end of file + input[type='number']::-webkit-inner-spin-button, + input[type='number']::-webkit-outer-spin-button { + -webkit-appearance: none; + margin: 0; + } +} diff --git a/src/app/layout.tsx b/src/app/layout.tsx index bc44507..a394704 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -3,10 +3,12 @@ import { Unbounded } from 'next/font/google'; import { fontHeading } from '@/lib/fonts'; import './globals.css'; import { cn } from '@/lib/utils'; +import { Toaster } from '@/components/ui/toaster'; const unbounded = Unbounded({ style: 'normal', subsets: ['latin'], + weight: ['200', '300', '400', '500', '600', '700', '800', '900'], variable: '--font-unbounded' }); @@ -30,6 +32,7 @@ export default function RootLayout({ )} > {children} + ); diff --git a/src/components/cards/leadboard-card.tsx b/src/components/cards/leadboard-card.tsx index 91031bc..f3abf85 100644 --- a/src/components/cards/leadboard-card.tsx +++ b/src/components/cards/leadboard-card.tsx @@ -1,30 +1,30 @@ import { Icons } from '@/components/icons'; import { cn } from '@/lib/utils'; type LeadProps = { - points: number; - winner?: boolean; - }; + points: number; + winner?: boolean; +}; export function LeadBoardCard({ points, winner }: LeadProps) { - return ( -
+
+ {winner ? ( + + ) : ( + 01 )} - > -
- {winner ? ( - - ) : ( - 01 - )} -
-
- Victor X -
+
+
+ Victor X
- - {points} PTS
- ); - }; \ No newline at end of file + + {points} PTS +
+ ); +} diff --git a/src/components/game.tsx b/src/components/game.tsx new file mode 100644 index 0000000..c040af2 --- /dev/null +++ b/src/components/game.tsx @@ -0,0 +1,133 @@ +'use client'; + +// import { Button } from "@/components/ui/button" +// import { Input } from "@/components/ui/input" +// import { Label } from "@/components/ui/label" +import { + Sheet, + SheetClose, + SheetContent, + SheetDescription, + SheetFooter, + SheetHeader, + SheetTitle, + SheetTrigger +} from '@/components/ui/sheet'; +import { Button } from './ui/button'; +import { Icons } from './icons'; +import Image from 'next/image'; +import { cn } from '@/lib/utils'; +import Form, { useZodForm } from './ui/form'; +import { gameSchema } from '@/lib/validations/game'; +import { Input } from './ui/input'; +import useLiveCountdown from '@/hooks/use-live-countdown'; +import { useEffect } from 'react'; + +export function SheetDemo() { + const { secondsLeft, startTimeout } = useLiveCountdown(); + + useEffect(() => { + startTimeout(60); + }, [startTimeout]); + + const form = useZodForm({ + schema: gameSchema + }); + + return ( + + + + + { + e.preventDefault(); + }} + onEscapeKeyDown={e => { + e.preventDefault(); + }} + > +
+
+
+ +
+ points + 1500 +
+
+
+ {/* image */} +
+ +
+ {/* data */} +
+
+

Property 1

+ +

One bed luxury apartment,

+
+ + +
+ + + +
+
+

Key features

+

+ Private balcony. Communal roof terrace. Resident's concierge service. Close + proximity to green spaces. 999 year lease with peppercorn ground rent +

+
+
+
+ + +
+
+
+ {/* timer */} +
+ + {secondsLeft > 0 && `${secondsLeft}`} + +
+
+
+
+
+
+ ); +} + +interface DescriptionProps extends React.HTMLAttributes { + title: string; + description?: string; +} + +const DescriptionList = ({ className, title, description }: DescriptionProps) => { + return ( +
+
{title}:
+
{description}
+
+ ); +}; diff --git a/src/components/layouts/sidebar-nav.tsx b/src/components/layouts/sidebar-nav.tsx index 4d93397..80ef89a 100644 --- a/src/components/layouts/sidebar-nav.tsx +++ b/src/components/layouts/sidebar-nav.tsx @@ -7,6 +7,8 @@ import Link from 'next/link'; import { usePathname } from 'next/navigation'; import { Icons } from '../icons'; import { siteConfig } from '@/config/site'; +import { useSubstrateContext } from '@/context/polkadot-contex'; +import ConnectWallet from './connect-wallet'; export default function SidebarNav() { return ( @@ -25,6 +27,7 @@ export default function SidebarNav() { } const SidebarNavList = ({ items }: { items: NavItem[] }) => { + const { selectedAccount, disconnectWallet } = useSubstrateContext(); const path = usePathname(); return (
- + {selectedAccount ? ( + + ) : ( + + )} ); }; diff --git a/src/components/ui/alert-dialog.tsx b/src/components/ui/alert-dialog.tsx index 76ea8d2..1afb1e0 100644 --- a/src/components/ui/alert-dialog.tsx +++ b/src/components/ui/alert-dialog.tsx @@ -36,7 +36,7 @@ const AlertDialogContent = React.forwardRef< [1]; +}; + +type DirectionOption = 'ltr' | 'rtl' | undefined; + +type CarouselContextType = { + emblaMainApi: ReturnType[1]; + mainRef: ReturnType[0]; + thumbsRef: ReturnType[0]; + scrollNext: () => void; + scrollPrev: () => void; + canScrollNext: boolean; + canScrollPrev: boolean; + activeIndex: number; + onThumbClick: (index: number) => void; + handleKeyDown: (event: React.KeyboardEvent) => void; + orientation: 'vertical' | 'horizontal'; + direction: DirectionOption; +} & CarouselContextProps; + +const useCarousel = () => { + const context = useContext(CarouselContext); + if (!context) { + throw new Error('useCarousel must be used within a CarouselProvider'); + } + return context; +}; + +const CarouselContext = createContext(null); + +// TODO : add support for vertical rtl support for the carousel +// ref : https://github.com/davidjerleke/embla-carousel/issues/784 + +const Carousel = forwardRef< + HTMLDivElement, + CarouselContextProps & React.HTMLAttributes +>( + ( + { + carouselOptions, + orientation = 'horizontal', + dir, + plugins, + children, + className, + ...props + }, + ref + ) => { + const [emblaMainRef, emblaMainApi] = useEmblaCarousel( + { + ...carouselOptions, + axis: orientation === 'vertical' ? 'y' : 'x', + direction: carouselOptions?.direction ?? (dir as DirectionOption) + }, + plugins + ); + + const [emblaThumbsRef, emblaThumbsApi] = useEmblaCarousel( + { + ...carouselOptions, + axis: orientation === 'vertical' ? 'y' : 'x', + direction: carouselOptions?.direction ?? (dir as DirectionOption), + containScroll: 'keepSnaps', + dragFree: true + }, + plugins + ); + + const [canScrollPrev, setCanScrollPrev] = useState(false); + const [canScrollNext, setCanScrollNext] = useState(false); + const [activeIndex, setActiveIndex] = useState(0); + + const ScrollNext = useCallback(() => { + if (!emblaMainApi) return; + emblaMainApi.scrollNext(); + }, [emblaMainApi]); + + const ScrollPrev = useCallback(() => { + if (!emblaMainApi) return; + emblaMainApi.scrollPrev(); + }, [emblaMainApi]); + + const direction = carouselOptions?.direction ?? (dir as DirectionOption); + + const handleKeyDown = useCallback( + (event: React.KeyboardEvent) => { + event.preventDefault(); + if (!emblaMainApi) return; + switch (event.key) { + case 'ArrowLeft': + if (orientation === 'horizontal') { + if (direction === 'rtl') { + ScrollNext(); + return; + } + ScrollPrev(); + } + break; + case 'ArrowRight': + if (orientation === 'horizontal') { + if (direction === 'rtl') { + ScrollPrev(); + return; + } + ScrollNext(); + } + break; + case 'ArrowUp': + if (orientation === 'vertical') { + ScrollPrev(); + } + break; + case 'ArrowDown': + if (orientation === 'vertical') { + ScrollNext(); + } + break; + } + }, + [emblaMainApi, orientation, direction] + ); + + const onThumbClick = useCallback( + (index: number) => { + if (!emblaMainApi || !emblaThumbsApi) return; + emblaMainApi.scrollTo(index); + }, + [emblaMainApi, emblaThumbsApi] + ); + + const onSelect = useCallback(() => { + if (!emblaMainApi || !emblaThumbsApi) return; + const selected = emblaMainApi.selectedScrollSnap(); + setActiveIndex(selected); + emblaThumbsApi.scrollTo(selected); + setCanScrollPrev(emblaMainApi.canScrollPrev()); + setCanScrollNext(emblaMainApi.canScrollNext()); + }, [emblaMainApi, emblaThumbsApi]); + + useEffect(() => { + if (!emblaMainApi) return; + onSelect(); + emblaMainApi.on('select', onSelect); + emblaMainApi.on('reInit', onSelect); + return () => { + emblaMainApi.off('select', onSelect); + }; + }, [emblaMainApi, onSelect]); + + return ( + +
+ {children} +
+
+ ); + } +); + +Carousel.displayName = 'Carousel'; + +const CarouselMainContainer = forwardRef< + HTMLDivElement, + {} & React.HTMLAttributes +>(({ className, dir, children, ...props }, ref) => { + const { mainRef, orientation, direction } = useCarousel(); + + return ( +
+
+ {children} +
+
+ ); +}); + +CarouselMainContainer.displayName = 'CarouselMainContainer'; + +const CarouselThumbsContainer = forwardRef< + HTMLDivElement, + {} & React.HTMLAttributes +>(({ className, dir, children, ...props }, ref) => { + const { thumbsRef, orientation, direction } = useCarousel(); + + return ( +
+
+ {children} +
+
+ ); +}); + +CarouselThumbsContainer.displayName = 'CarouselThumbsContainer'; + +const SliderMainItem = forwardRef>( + ({ className, children, ...props }, ref) => { + const { orientation } = useCarousel(); + return ( +
+ {children} +
+ ); + } +); + +SliderMainItem.displayName = 'SliderMainItem'; + +const SliderThumbItem = forwardRef< + HTMLDivElement, + { + index: number; + } & React.HTMLAttributes +>(({ className, index, children, ...props }, ref) => { + const { activeIndex, onThumbClick, orientation } = useCarousel(); + const isSlideActive = activeIndex === index; + return ( +
onThumbClick(index)} + className={cn( + 'flex bg-background p-1', + `${orientation === 'vertical' ? 'pb-1' : 'pr-1'}`, + className + )} + > +
+ {children} +
+
+ ); +}); + +SliderThumbItem.displayName = 'SliderThumbItem'; + +const CarouselIndicator = forwardRef< + HTMLButtonElement, + { index: number } & React.ComponentProps +>(({ className, index, children, ...props }, ref) => { + const { activeIndex, onThumbClick } = useCarousel(); + const isSlideActive = activeIndex === index; + return ( + + ); +}); + +CarouselIndicator.displayName = 'CarouselIndicator'; + +const CarouselPrevious = forwardRef>( + ({ className, dir, variant = 'outline', size = 'icon', ...props }, ref) => { + const { canScrollNext, canScrollPrev, scrollNext, scrollPrev, orientation, direction } = + useCarousel(); + + const scroll = direction === 'rtl' ? scrollNext : scrollPrev; + const canScroll = direction === 'rtl' ? canScrollNext : canScrollPrev; + return ( + + ); + } +); +CarouselPrevious.displayName = 'CarouselPrevious'; + +const CarouselNext = forwardRef>( + ({ className, dir, variant = 'outline', size = 'icon', ...props }, ref) => { + const { canScrollNext, canScrollPrev, scrollNext, scrollPrev, orientation, direction } = + useCarousel(); + const scroll = direction === 'rtl' ? scrollPrev : scrollNext; + const canScroll = direction === 'rtl' ? canScrollPrev : canScrollNext; + return ( + + ); + } +); + +CarouselNext.displayName = 'CarouselNext'; + +export { + Carousel, + CarouselMainContainer, + CarouselThumbsContainer, + SliderMainItem, + SliderThumbItem, + CarouselIndicator, + CarouselPrevious, + CarouselNext, + useCarousel +}; diff --git a/src/components/ui/input.tsx b/src/components/ui/input.tsx new file mode 100644 index 0000000..d1dbcc6 --- /dev/null +++ b/src/components/ui/input.tsx @@ -0,0 +1,24 @@ +import * as React from 'react'; + +import { cn } from '@/lib/utils'; + +export interface InputProps extends React.InputHTMLAttributes {} + +const Input = React.forwardRef( + ({ className, type, ...props }, ref) => { + return ( + + ); + } +); +Input.displayName = 'Input'; + +export { Input }; diff --git a/src/components/ui/sheet.tsx b/src/components/ui/sheet.tsx new file mode 100644 index 0000000..e1f1fa0 --- /dev/null +++ b/src/components/ui/sheet.tsx @@ -0,0 +1,128 @@ +'use client'; + +import * as React from 'react'; +import * as SheetPrimitive from '@radix-ui/react-dialog'; +import { cva, type VariantProps } from 'class-variance-authority'; +import { X } from 'lucide-react'; + +import { cn } from '@/lib/utils'; + +const Sheet = SheetPrimitive.Root; + +const SheetTrigger = SheetPrimitive.Trigger; + +const SheetClose = SheetPrimitive.Close; + +const SheetPortal = SheetPrimitive.Portal; + +const SheetOverlay = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +SheetOverlay.displayName = SheetPrimitive.Overlay.displayName; + +const sheetVariants = cva( + 'fixed z-50 gap-4 bg-background p-6 transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500', + { + variants: { + side: { + top: 'inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top', + bottom: + 'inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom', + left: 'inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm', + right: + 'inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right' + } + }, + defaultVariants: { + side: 'right' + } + } +); + +interface SheetContentProps + extends React.ComponentPropsWithoutRef, + VariantProps {} + +const SheetContent = React.forwardRef< + React.ElementRef, + SheetContentProps +>(({ side = 'right', className, children, ...props }, ref) => ( + + + + {children} + {/* + + Close + */} + + +)); +SheetContent.displayName = SheetPrimitive.Content.displayName; + +const SheetHeader = ({ className, ...props }: React.HTMLAttributes) => ( +
+); +SheetHeader.displayName = 'SheetHeader'; + +const SheetFooter = ({ className, ...props }: React.HTMLAttributes) => ( +
+); +SheetFooter.displayName = 'SheetFooter'; + +const SheetTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +SheetTitle.displayName = SheetPrimitive.Title.displayName; + +const SheetDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +SheetDescription.displayName = SheetPrimitive.Description.displayName; + +export { + Sheet, + SheetPortal, + SheetOverlay, + SheetTrigger, + SheetClose, + SheetContent, + SheetHeader, + SheetFooter, + SheetTitle, + SheetDescription +}; diff --git a/src/components/ui/sonner.tsx b/src/components/ui/sonner.tsx new file mode 100644 index 0000000..01e8338 --- /dev/null +++ b/src/components/ui/sonner.tsx @@ -0,0 +1,29 @@ +'use client'; + +import { useTheme } from 'next-themes'; +import { Toaster as Sonner } from 'sonner'; + +type ToasterProps = React.ComponentProps; + +const Toaster = ({ ...props }: ToasterProps) => { + const { theme = 'system' } = useTheme(); + + return ( + + ); +}; + +export { Toaster }; diff --git a/src/components/ui/toast.tsx b/src/components/ui/toast.tsx new file mode 100644 index 0000000..12b09ab --- /dev/null +++ b/src/components/ui/toast.tsx @@ -0,0 +1,129 @@ +'use client'; + +import * as React from 'react'; +import * as ToastPrimitives from '@radix-ui/react-toast'; +import { cva, type VariantProps } from 'class-variance-authority'; +import { X } from 'lucide-react'; + +import { cn } from '@/lib/utils'; + +const ToastProvider = ToastPrimitives.Provider; + +const ToastViewport = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +ToastViewport.displayName = ToastPrimitives.Viewport.displayName; + +const toastVariants = cva( + 'group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full', + { + variants: { + variant: { + default: 'border bg-background text-foreground', + destructive: + 'destructive group border-destructive bg-destructive text-destructive-foreground' + } + }, + defaultVariants: { + variant: 'default' + } + } +); + +const Toast = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & + VariantProps +>(({ className, variant, ...props }, ref) => { + return ( + + ); +}); +Toast.displayName = ToastPrimitives.Root.displayName; + +const ToastAction = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +ToastAction.displayName = ToastPrimitives.Action.displayName; + +const ToastClose = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + +)); +ToastClose.displayName = ToastPrimitives.Close.displayName; + +const ToastTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +ToastTitle.displayName = ToastPrimitives.Title.displayName; + +const ToastDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +ToastDescription.displayName = ToastPrimitives.Description.displayName; + +type ToastProps = React.ComponentPropsWithoutRef; + +type ToastActionElement = React.ReactElement; + +export { + type ToastProps, + type ToastActionElement, + ToastProvider, + ToastViewport, + Toast, + ToastTitle, + ToastDescription, + ToastClose, + ToastAction +}; diff --git a/src/components/ui/toaster.tsx b/src/components/ui/toaster.tsx new file mode 100644 index 0000000..1a51ba3 --- /dev/null +++ b/src/components/ui/toaster.tsx @@ -0,0 +1,33 @@ +'use client'; + +import { + Toast, + ToastClose, + ToastDescription, + ToastProvider, + ToastTitle, + ToastViewport +} from '@/components/ui/toast'; +import { useToast } from '@/components/ui/use-toast'; + +export function Toaster() { + const { toasts } = useToast(); + + return ( + + {toasts.map(function ({ id, title, description, action, ...props }) { + return ( + +
+ {title && {title}} + {description && {description}} +
+ {action} + +
+ ); + })} + +
+ ); +} diff --git a/src/components/ui/use-toast.ts b/src/components/ui/use-toast.ts new file mode 100644 index 0000000..377cbfd --- /dev/null +++ b/src/components/ui/use-toast.ts @@ -0,0 +1,191 @@ +'use client'; + +// Inspired by react-hot-toast library +import * as React from 'react'; + +import type { ToastActionElement, ToastProps } from '@/components/ui/toast'; + +const TOAST_LIMIT = 1; +const TOAST_REMOVE_DELAY = 1000000; + +type ToasterToast = ToastProps & { + id: string; + title?: React.ReactNode; + description?: React.ReactNode; + action?: ToastActionElement; +}; + +const actionTypes = { + ADD_TOAST: 'ADD_TOAST', + UPDATE_TOAST: 'UPDATE_TOAST', + DISMISS_TOAST: 'DISMISS_TOAST', + REMOVE_TOAST: 'REMOVE_TOAST' +} as const; + +let count = 0; + +function genId() { + count = (count + 1) % Number.MAX_SAFE_INTEGER; + return count.toString(); +} + +type ActionType = typeof actionTypes; + +type Action = + | { + type: ActionType['ADD_TOAST']; + toast: ToasterToast; + } + | { + type: ActionType['UPDATE_TOAST']; + toast: Partial; + } + | { + type: ActionType['DISMISS_TOAST']; + toastId?: ToasterToast['id']; + } + | { + type: ActionType['REMOVE_TOAST']; + toastId?: ToasterToast['id']; + }; + +interface State { + toasts: ToasterToast[]; +} + +const toastTimeouts = new Map>(); + +const addToRemoveQueue = (toastId: string) => { + if (toastTimeouts.has(toastId)) { + return; + } + + const timeout = setTimeout(() => { + toastTimeouts.delete(toastId); + dispatch({ + type: 'REMOVE_TOAST', + toastId: toastId + }); + }, TOAST_REMOVE_DELAY); + + toastTimeouts.set(toastId, timeout); +}; + +export const reducer = (state: State, action: Action): State => { + switch (action.type) { + case 'ADD_TOAST': + return { + ...state, + toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT) + }; + + case 'UPDATE_TOAST': + return { + ...state, + toasts: state.toasts.map(t => + t.id === action.toast.id ? { ...t, ...action.toast } : t + ) + }; + + case 'DISMISS_TOAST': { + const { toastId } = action; + + // ! Side effects ! - This could be extracted into a dismissToast() action, + // but I'll keep it here for simplicity + if (toastId) { + addToRemoveQueue(toastId); + } else { + state.toasts.forEach(toast => { + addToRemoveQueue(toast.id); + }); + } + + return { + ...state, + toasts: state.toasts.map(t => + t.id === toastId || toastId === undefined + ? { + ...t, + open: false + } + : t + ) + }; + } + case 'REMOVE_TOAST': + if (action.toastId === undefined) { + return { + ...state, + toasts: [] + }; + } + return { + ...state, + toasts: state.toasts.filter(t => t.id !== action.toastId) + }; + } +}; + +const listeners: Array<(state: State) => void> = []; + +let memoryState: State = { toasts: [] }; + +function dispatch(action: Action) { + memoryState = reducer(memoryState, action); + listeners.forEach(listener => { + listener(memoryState); + }); +} + +type Toast = Omit; + +function toast({ ...props }: Toast) { + const id = genId(); + + const update = (props: ToasterToast) => + dispatch({ + type: 'UPDATE_TOAST', + toast: { ...props, id } + }); + const dismiss = () => dispatch({ type: 'DISMISS_TOAST', toastId: id }); + + dispatch({ + type: 'ADD_TOAST', + toast: { + ...props, + id, + open: true, + onOpenChange: open => { + if (!open) dismiss(); + } + } + }); + + return { + id: id, + dismiss, + update + }; +} + +function useToast() { + const [state, setState] = React.useState(memoryState); + + React.useEffect(() => { + listeners.push(setState); + return () => { + const index = listeners.indexOf(setState); + if (index > -1) { + listeners.splice(index, 1); + } + }; + }, [state]); + + return { + ...state, + toast, + dismiss: (toastId?: string) => dispatch({ type: 'DISMISS_TOAST', toastId }) + }; +} + +export { useToast, toast }; diff --git a/src/components/wallet-icon.tsx b/src/components/wallet-icon.tsx index 418c1fc..8aa4631 100644 --- a/src/components/wallet-icon.tsx +++ b/src/components/wallet-icon.tsx @@ -210,5 +210,10 @@ export const WalletIcon = { /> + ), + account: (props: IconProps) => ( + + + ) }; diff --git a/src/context/con-old.tsx b/src/context/con-old.tsx new file mode 100644 index 0000000..e0bf69b --- /dev/null +++ b/src/context/con-old.tsx @@ -0,0 +1,108 @@ +'use client'; + +import React from 'react'; +import { useRouter } from 'next/navigation'; +import { web3Accounts, web3Enable, web3FromSource } from '@polkadot/extension-dapp'; +import { InjectedExtension, InjectedAccountWithMeta } from '@polkadot/extension-inject/types'; +import { createContext, useCallback, useContext, useEffect, useState } from 'react'; + +export type SubstrateContext = { + address: string; + points: number; + isConnected: boolean; + selectedAccount: InjectedAccountWithMeta | any; + handleConnect: any; + disconnectWallet: any; +}; + +const SubstrateContext = createContext({ + address: '', + points: 0, + isConnected: false, + selectedAccount: {}, + handleConnect: async () => {}, // Dummy function for handleConnect + disconnectWallet: () => {} // Dummy function for disconnectWallet +}); + +export function useSubstrateContext() { + return useContext(SubstrateContext); +} + +export interface SubstrateProps { + children?: React.ReactNode; +} + +export default function SubstrateContextProvider({ children }: SubstrateProps) { + const router = useRouter(); + const [isConnected, setIsConnected] = useState(false); + const [selectedAccount, setSelectedAccount] = useState(); + const [address, setAddress] = useState(''); + + const handleConnect = async (walletName: 'talisman' | 'subwallet-js') => { + try { + const extensions = await web3Enable('RealXChange'); + const extension = extensions.find( + ext => ext.name.toLowerCase() === walletName.toLowerCase() + ); + if (!extension) { + alert(`Please install the ${walletName} extension.`); + return; + } + + const injected = await web3FromSource(extension.name); + if (!injected) { + throw new Error(`Failed to connect with ${walletName}`); + } + + const accounts = await web3Accounts(); + if (accounts.length === 0) { + throw new Error('No accounts found in the wallet.'); + } + + setSelectedAccount(accounts[0]); + setAddress(accounts[0].address); + localStorage.setItem('selectedWalletAddress', accounts[0].address); + console.log('Connected with account:', accounts[0]); + console.log('Connected with account:', accounts); + + alert(`Connected successfully with ${walletName}`); + } catch (error) { + console.error('Error connecting to wallet:', error); + alert(`Failed to connect the wallet: ${error}`); + } + }; + + const disconnectWallet = () => { + setAddress(''); + localStorage.removeItem('selectedWalletAddress'); + setIsConnected(false); + router.refresh(); + }; + + const onReconnect = async () => { + const localStorageAddress = localStorage.getItem('selectedWalletAddress'); + if (localStorageAddress) { + setAddress(localStorageAddress); + setIsConnected(true); + } + }; + + useEffect(() => { + onReconnect(); + }, []); + + return ( + + {children} + + ); +} diff --git a/src/context/polkadot-contex.tsx b/src/context/polkadot-contex.tsx index b2d0ed5..d763449 100644 --- a/src/context/polkadot-contex.tsx +++ b/src/context/polkadot-contex.tsx @@ -41,9 +41,11 @@ export default function SubstrateContextProvider({ children }: SubstrateProps) { const handleConnect = async (walletName: 'talisman' | 'subwallet-js') => { try { const extensions = await web3Enable('RealXChange'); + console.log(extensions); const extension = extensions.find( ext => ext.name.toLowerCase() === walletName.toLowerCase() ); + console.log(extension); if (!extension) { alert(`Please install the ${walletName} extension.`); return; diff --git a/src/hooks/use-live-countdown.ts b/src/hooks/use-live-countdown.ts new file mode 100644 index 0000000..18210e3 --- /dev/null +++ b/src/hooks/use-live-countdown.ts @@ -0,0 +1,15 @@ +import { useEffect, useState } from 'react'; + +export default function useLiveCountdown(length: number) { + const [seconds, setSeconds] = useState(length); + + useEffect(() => { + const timeout = setTimeout(() => { + setSeconds(prevSeconds => (prevSeconds > 0 ? prevSeconds - 1 : prevSeconds)); + }, 1000); + + return () => clearTimeout(timeout); + }, [seconds]); + + return { seconds }; +} diff --git a/src/lib/extrinsic.ts b/src/lib/extrinsic.ts new file mode 100644 index 0000000..1e8a038 --- /dev/null +++ b/src/lib/extrinsic.ts @@ -0,0 +1,5 @@ +import { getApi } from './polkadot'; + +class Extrinsic { + // Initialize other properties or perform additional setup here +} diff --git a/src/lib/queries.ts b/src/lib/queries.ts new file mode 100644 index 0000000..b0a7230 --- /dev/null +++ b/src/lib/queries.ts @@ -0,0 +1,28 @@ +import { getApi } from './polkadot'; + +export async function getAvailableNFTs(collectionId?: number) { + const api = await getApi(); + const result = await api.query.gameModule.collectionColor(collectionId); + const output = result.toHuman(); + return output; +} + +export async function getLeadBoards() { + const api = await getApi(); + const result = await api.query.gameModule.leaderboard(); + const output = result.toHuman(); + return output; +} + +export async function getCurrentRoundID() { + const api = await getApi(); + const result = await api.query.gameModule.currentRound(); + const output = result.toHuman(); + return output; +} +export async function getGameInfo(gameId: number) { + const api = await getApi(); + const result = await api.query.gameModule.gameInfo(gameId); + const output = result.toHuman(); + return output; +} diff --git a/src/lib/validations/game.ts b/src/lib/validations/game.ts new file mode 100644 index 0000000..94d0bff --- /dev/null +++ b/src/lib/validations/game.ts @@ -0,0 +1,5 @@ +import { z } from 'zod'; + +export const gameSchema = z.object({ + price: z.number() +}); diff --git a/tailwind.config.ts b/tailwind.config.ts index b55ea02..d239b55 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -18,6 +18,8 @@ const config = { // }, extend: { colors: { + ring: '#172234', + input: '#172234', border: '#3B4F74', background: '#27354E', foreground: 'hsla(0, 0%, 100%)', @@ -60,7 +62,8 @@ const config = { }, boxShadow: { header: '0px 0px 30px 0px rgba(0, 0, 0, 0.32)', - card: '0px 0px 30px 0px rgba(0, 0, 0, 0.32)' + card: '0px 0px 30px 0px rgba(0, 0, 0, 0.32)', + time: '-4.41px -8.82px 13.671px 0px rgba(87, 160, 197, 0.25) inset, 7.35px 14.7px 11.466px 0px rgba(55, 114, 144, 0.25) inset' }, keyframes: { 'accordion-down': {