diff --git a/.prettierrc.cjs b/.prettierrc.cjs
index e643a35..77c651e 100644
--- a/.prettierrc.cjs
+++ b/.prettierrc.cjs
@@ -1,7 +1,7 @@
module.exports = {
singleQuote: true,
- trailingComma: "all",
+ trailingComma: 'all',
printWidth: 120,
semi: true,
- plugins: ["prettier-plugin-tailwindcss"],
+ plugins: ['prettier-plugin-tailwindcss'],
};
diff --git a/src/components/common/About/index.tsx b/src/components/common/About/index.tsx
index 78a890b..dc7491f 100644
--- a/src/components/common/About/index.tsx
+++ b/src/components/common/About/index.tsx
@@ -36,8 +36,8 @@ const INSTRUCTIONS = [
subtitle: 'Ask another question:',
description: (
- To start a new session, click "Make a New Forecast " or refresh the
- page
+ To start a new session, click "
+ Make a New Forecast " or refresh the page
),
},
diff --git a/src/components/common/AdminBtn/index.tsx b/src/components/common/AdminBtn/index.tsx
new file mode 100644
index 0000000..3d34920
--- /dev/null
+++ b/src/components/common/AdminBtn/index.tsx
@@ -0,0 +1,17 @@
+import { Link } from 'react-router';
+
+import { Button } from '@/components/ui/button';
+import { routes } from '@/lib/router';
+
+export const AdminBtn = () => {
+ return (
+
+
+ Admin
+
+
+ );
+};
diff --git a/src/components/pages/game/game.tsx b/src/components/pages/game/game.tsx
index d759302..4c3a603 100644
--- a/src/components/pages/game/game.tsx
+++ b/src/components/pages/game/game.tsx
@@ -20,7 +20,9 @@ const TarotRequestSchema = z.object({
.min(3, 'Min 3 symbols')
.max(1000, 'Max 1000 symbols')
// .regex(/^[a-zA-Z0-9.,!?-\s]+$/, 'Only English letters and numbers are allowed')
- .refine((value) => value.trim() !== '', { message: 'String cannot consist of only spaces' }),
+ .refine((value) => value.trim() !== '', {
+ message: 'String cannot consist of only spaces',
+ }),
});
const DEFAULT_IMAGE = 'images/tarot-game/bord.png';
diff --git a/src/components/pages/home/faq.tsx b/src/components/pages/home/faq.tsx
index 44f20f8..456bafd 100644
--- a/src/components/pages/home/faq.tsx
+++ b/src/components/pages/home/faq.tsx
@@ -28,7 +28,10 @@ const FAQ_MOCK = [
title: 'Which wallets are supported?',
description: 'We currently support two Solana-compatible wallets: Phantom and Solflare',
},
- { title: 'What payment tokens can I use?', description: 'You can choose SOL for your payment.' },
+ {
+ title: 'What payment tokens can I use?',
+ description: 'You can choose SOL for your payment.',
+ },
{
title: 'Can I use this service anonymously?',
description:
diff --git a/src/components/pages/home/hero.tsx b/src/components/pages/home/hero.tsx
index d8b0bd3..d0aac47 100644
--- a/src/components/pages/home/hero.tsx
+++ b/src/components/pages/home/hero.tsx
@@ -1,3 +1,4 @@
+import { AdminBtn } from '@/components/common/AdminBtn';
import { GoToTwitterBtn } from '@/components/common/GoToTwitterBtn';
import { PredictFutureBtn } from '@/components/common/PredictFutureBtn';
@@ -8,6 +9,7 @@ export const Hero = () => {
{
+ const { publicKey, signMessage } = useWallet();
+
+ const client = useQueryClient();
+
+ return useMutation({
+ async mutationFn({ status }: { status: boolean }) {
+ if (!publicKey || !signMessage) {
+ return;
+ }
+
+ const client = authClient();
+
+ const uuid = crypto.randomUUID();
+
+ const message = `I am the admin of Tarot ${uuid}`;
+
+ const encodedMessage = new TextEncoder().encode(message);
+ const signature = bs58.encode(await signMessage(encodedMessage));
+
+ client.setHeader('x-admin-signature', signature);
+ client.setHeader('x-admin-uuid', uuid);
+
+ return await post<{
+ isShutDown: boolean;
+ id: number;
+ }>(client, status ? `shutdown/enable` : 'shutdown/shutdown');
+ },
+ async onSuccess() {
+ await client.invalidateQueries({
+ queryKey: ['status'],
+ });
+ },
+ onError(error) {
+ console.trace(error);
+ },
+ });
+};
+
+export default useControlService;
diff --git a/src/hooks/api/use-status.ts b/src/hooks/api/use-status.ts
new file mode 100644
index 0000000..2922f44
--- /dev/null
+++ b/src/hooks/api/use-status.ts
@@ -0,0 +1,22 @@
+import { useQuery } from '@tanstack/react-query';
+
+import { get } from './utils';
+
+import { authClient } from '@/lib/fetcher';
+
+const useStatus = () => {
+ return useQuery({
+ queryKey: ['status'],
+ queryFn: async () => {
+ const client = authClient();
+
+ return await get<{
+ isShutDown: boolean;
+ id: number;
+ } | null>(client, 'shutdown/status');
+ },
+ refetchInterval: 10000,
+ });
+};
+
+export default useStatus;
diff --git a/src/hooks/contracts/write/use-make-prediction.ts b/src/hooks/contracts/write/use-make-prediction.ts
index fd0df8a..bd754f9 100644
--- a/src/hooks/contracts/write/use-make-prediction.ts
+++ b/src/hooks/contracts/write/use-make-prediction.ts
@@ -1,13 +1,48 @@
import { useWallet } from '@solana/wallet-adapter-react';
import { SystemProgram, Transaction } from '@solana/web3.js';
import { useMutation } from '@tanstack/react-query';
+import { toast } from 'react-toastify';
import { OwnerAddress } from '@/constants/addresses';
import { env } from '@/env';
import useSubmitTarotCards from '@/hooks/api/use-submit-cards';
import { network } from '@/lib/solana';
import { sendAndConfirmTransaction } from '@/lib/solana/utils';
-import { getRandomTarotCards, showTxToast } from '@/lib/utils';
+import { getRandomTarotCards } from '@/lib/utils';
+
+let toastId: string | number | null = null;
+
+const notify = () => {
+ toastId = toast('Making prediction...', {
+ autoClose: false,
+ closeOnClick: false,
+ draggable: false,
+ isLoading: true,
+ type: 'default',
+ });
+};
+
+const updateToast = (toastId: string | number | null) => {
+ if (toastId !== null) {
+ toast.update(toastId, {
+ render: 'Done!',
+ type: 'success',
+ autoClose: 3000,
+ isLoading: false,
+ });
+ }
+};
+
+const handleErrorToast = (toastId: string | number | null) => {
+ if (toastId !== null) {
+ toast.update(toastId, {
+ render: 'Error occurred!',
+ type: 'error',
+ autoClose: 3000,
+ isLoading: false,
+ });
+ }
+};
const useMakePrediction = () => {
const { publicKey, sendTransaction } = useWallet();
@@ -19,30 +54,35 @@ const useMakePrediction = () => {
return;
}
- return await showTxToast('Making prediction', async () => {
- const rawTx = new Transaction();
-
- rawTx.add(
- SystemProgram.transfer({
- fromPubkey: publicKey,
- toPubkey: OwnerAddress[network],
- lamports: Number(env.VITE_DEPOSIT_AMOUNT_SOL) * 1e9,
- }),
- );
-
- const txHash = await sendAndConfirmTransaction(publicKey, rawTx, sendTransaction);
- const tarots = getRandomTarotCards(txHash + publicKey.toBase58());
- const result = await submitCards({ tarots, hash: txHash, question });
-
- return {
- tarots,
- answer: result?.response ?? '',
- };
- });
+ notify();
+
+ const rawTx = new Transaction();
+
+ rawTx.add(
+ SystemProgram.transfer({
+ fromPubkey: publicKey,
+ toPubkey: OwnerAddress[network],
+ lamports: Number(env.VITE_DEPOSIT_AMOUNT_SOL) * 1e9,
+ }),
+ );
+
+ const txHash = await sendAndConfirmTransaction(publicKey, rawTx, sendTransaction);
+
+ const tarots = getRandomTarotCards(txHash + publicKey.toBase58());
+
+ const result = await submitCards({ tarots, hash: txHash, question });
+
+ updateToast(toastId);
+
+ return {
+ tarots,
+ answer: result?.response ?? '',
+ };
},
onError(error) {
console.trace(error);
+ handleErrorToast(toastId);
},
});
};
diff --git a/src/lib/fetcher.ts b/src/lib/fetcher.ts
index 51d85f4..7b2027d 100644
--- a/src/lib/fetcher.ts
+++ b/src/lib/fetcher.ts
@@ -16,6 +16,10 @@ export class Fetcher {
this._headers = headers;
}
+ public setHeader(key: string, value: string) {
+ this._headers[key] = value;
+ }
+
public async get(url: string) {
return this._processResponse(
fetch(new URL(url, this._baseURL), {
diff --git a/src/lib/router.ts b/src/lib/router.ts
index e156e36..1f4cc66 100644
--- a/src/lib/router.ts
+++ b/src/lib/router.ts
@@ -7,6 +7,7 @@ import { MainLayout } from '@/layouts/main-layout';
export const routes = {
HOME: '/',
GAME: '/game',
+ ADMIN: '/admin',
} as const;
export const router = createBrowserRouter([
@@ -21,6 +22,10 @@ export const router = createBrowserRouter([
path: routes.GAME,
Component: lazy(() => import('@/pages/game-page')),
},
+ {
+ path: routes.ADMIN,
+ Component: lazy(() => import('@/pages/admin-page')),
+ },
],
},
{
diff --git a/src/lib/utils.ts b/src/lib/utils.ts
index c6a745e..dee146f 100644
--- a/src/lib/utils.ts
+++ b/src/lib/utils.ts
@@ -43,7 +43,10 @@ export const getRandomTarotCards = (hash: string): TarotCard[] => {
.map((sum) => sum % 50 === 0)
.slice(0, 3);
- return tarotIds.map((tarot, idx) => ({ id: tarot, reverted: isReverted[idx] }));
+ return tarotIds.map((tarot, idx) => ({
+ id: tarot,
+ reverted: isReverted[idx],
+ }));
};
const calculateByteSum = (str: string): number => stringToBytes(str).reduce((sum, byte) => sum + byte, 0);
diff --git a/src/pages/admin-page.tsx b/src/pages/admin-page.tsx
new file mode 100644
index 0000000..7a14822
--- /dev/null
+++ b/src/pages/admin-page.tsx
@@ -0,0 +1,60 @@
+/* eslint-disable @typescript-eslint/no-floating-promises */
+import { useWallet } from '@solana/wallet-adapter-react';
+import { useEffect } from 'react';
+import { useNavigate } from 'react-router';
+
+import { Header } from '@/components/common/Header';
+import { Button } from '@/components/ui/button';
+import { OwnerAddress } from '@/constants/addresses';
+import useControlService from '@/hooks/api/use-control-service';
+import useStatus from '@/hooks/api/use-status';
+import { network } from '@/lib/solana';
+
+export default function AdminPage() {
+ const { publicKey } = useWallet();
+ const navigate = useNavigate();
+
+ const { data: status } = useStatus();
+
+ const { mutateAsync } = useControlService();
+
+ useEffect(() => {
+ if (!publicKey?.equals(OwnerAddress[network])) {
+ console.log(publicKey?.toBase58(), OwnerAddress[network].toBase58());
+ navigate('/');
+ }
+ }, [navigate, publicKey]);
+
+ const handleControl = async (status: boolean) => {
+ await mutateAsync({
+ status: status,
+ });
+ };
+
+ return (
+ <>
+
+
+
+
Status: {status?.isShutDown === true ? 'Disabled' : 'Enabled'}
+
+
+ handleControl(true)}
+ variant={'outline'}
+ className="min-h-[60px] w-full bg-[url('/images/textures/green.png')] bg-repeat text-[22px] transition-all ease-in-out hover:opacity-80"
+ >
+ Enable
+
+ handleControl(false)}
+ variant={'outline'}
+ className="min-h-[60px] w-full bg-[url('/images/textures/green.png')] bg-repeat text-[22px] transition-all ease-in-out hover:opacity-80"
+ >
+ Disable
+
+
+
+ >
+ );
+}