Skip to content

Commit

Permalink
Merge pull request #33 from RedDuck-Software/feat/admin
Browse files Browse the repository at this point in the history
Feat/admin
  • Loading branch information
NikiTaysRD authored Jan 15, 2025
2 parents 3377418 + ff9ebe8 commit a3fbeb3
Show file tree
Hide file tree
Showing 13 changed files with 234 additions and 28 deletions.
4 changes: 2 additions & 2 deletions .prettierrc.cjs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
module.exports = {
singleQuote: true,
trailingComma: "all",
trailingComma: 'all',
printWidth: 120,
semi: true,
plugins: ["prettier-plugin-tailwindcss"],
plugins: ['prettier-plugin-tailwindcss'],
};
4 changes: 2 additions & 2 deletions src/components/common/About/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,8 @@ const INSTRUCTIONS = [
subtitle: 'Ask another question:',
description: (
<p>
To start a new session, click &quot;<span className="font-bold">Make a New Forecast</span>&quot; or refresh the
page
To start a new session, click &quot;
<span className="font-bold">Make a New Forecast</span>&quot; or refresh the page
</p>
),
},
Expand Down
17 changes: 17 additions & 0 deletions src/components/common/AdminBtn/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { Link } from 'react-router';

import { Button } from '@/components/ui/button';
import { routes } from '@/lib/router';

export const AdminBtn = () => {
return (
<Link to={routes.ADMIN}>
<Button
variant={'outline'}
className="min-h-[60px] w-full bg-[url('/images/textures/sand.png')] bg-repeat text-[22px] transition-all ease-in-out hover:opacity-80"
>
Admin
</Button>
</Link>
);
};
4 changes: 3 additions & 1 deletion src/components/pages/game/game.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
5 changes: 4 additions & 1 deletion src/components/pages/home/faq.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
2 changes: 2 additions & 0 deletions src/components/pages/home/hero.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { AdminBtn } from '@/components/common/AdminBtn';
import { GoToTwitterBtn } from '@/components/common/GoToTwitterBtn';
import { PredictFutureBtn } from '@/components/common/PredictFutureBtn';

Expand All @@ -8,6 +9,7 @@ export const Hero = () => {
<div className="flex flex-col gap-6 md:flex-row md:gap-10">
<GoToTwitterBtn />
<PredictFutureBtn />
<AdminBtn />
</div>
<img
src="/images/landing/hero-bg.webp"
Expand Down
48 changes: 48 additions & 0 deletions src/hooks/api/use-control-service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { useWallet } from '@solana/wallet-adapter-react';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import bs58 from 'bs58';

import { post } from './utils';

import { authClient } from '@/lib/fetcher';

const useControlService = () => {
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;
22 changes: 22 additions & 0 deletions src/hooks/api/use-status.ts
Original file line number Diff line number Diff line change
@@ -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;
82 changes: 61 additions & 21 deletions src/hooks/contracts/write/use-make-prediction.ts
Original file line number Diff line number Diff line change
@@ -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();
Expand All @@ -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);
},
});
};
Expand Down
4 changes: 4 additions & 0 deletions src/lib/fetcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ export class Fetcher {
this._headers = headers;
}

public setHeader(key: string, value: string) {
this._headers[key] = value;
}

public async get<T>(url: string) {
return this._processResponse<T>(
fetch(new URL(url, this._baseURL), {
Expand Down
5 changes: 5 additions & 0 deletions src/lib/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { MainLayout } from '@/layouts/main-layout';
export const routes = {
HOME: '/',
GAME: '/game',
ADMIN: '/admin',
} as const;

export const router = createBrowserRouter([
Expand All @@ -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')),
},
],
},
{
Expand Down
5 changes: 4 additions & 1 deletion src/lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
60 changes: 60 additions & 0 deletions src/pages/admin-page.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<>
<Header />

<div className="mt-8 space-y-4 px-4">
<p>Status: {status?.isShutDown === true ? 'Disabled' : 'Enabled'}</p>

<div className="flex h-[50vh] w-1/2 flex-row justify-between gap-4">
<Button
onClick={() => 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
</Button>
<Button
onClick={() => 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
</Button>
</div>
</div>
</>
);
}

0 comments on commit a3fbeb3

Please sign in to comment.