Merge feature/api
* Added backend capabilities with SQLite3 * Added routes for Next.js backend
This commit is contained in:
parent
7098346b51
commit
99bc7a319f
2
.gitignore
vendored
2
.gitignore
vendored
@ -37,3 +37,5 @@ next-env.d.ts
|
|||||||
|
|
||||||
# vscode
|
# vscode
|
||||||
.vscode
|
.vscode
|
||||||
|
|
||||||
|
database.db
|
13
Dockerfile
Normal file
13
Dockerfile
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
FROM node:16
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY package*.json ./
|
||||||
|
|
||||||
|
RUN npm install
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
EXPOSE 3000
|
||||||
|
|
||||||
|
CMD ["npm", "run", "dev"]
|
19
README.md
19
README.md
@ -12,12 +12,15 @@ yarn dev
|
|||||||
pnpm dev
|
pnpm dev
|
||||||
```
|
```
|
||||||
|
|
||||||
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
## API Endpoints
|
||||||
|
|
||||||
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
|
POST `/user/login` check if user exists
|
||||||
|
GET `/user/USER_ID/stakes` get stake event
|
||||||
[API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.ts`.
|
POST `/user/USER_ID/stakes/claim` claim stake
|
||||||
|
POST `/user/USER_ID/stakes/start` start stake
|
||||||
The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages.
|
GET `/user/USER_ID/bank-account` get balance
|
||||||
|
PUT `/user/USER_ID/bank-account` sell resource
|
||||||
This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font.
|
GET `/user/USER_ID/inventory-items` get inventory items
|
||||||
|
POST `/user/USER_ID/inventory-items` buy item
|
||||||
|
GET `/user/USER_ID/staking-sources` get staking sources
|
||||||
|
POST `/user/USER_ID/staking-sources` create staking source
|
||||||
|
56
config/game-config.json
Normal file
56
config/game-config.json
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
{
|
||||||
|
"resources": ["Sollux", "Shadowstone", "Azurium", "Novafor", "Nebulance"],
|
||||||
|
"moons": {
|
||||||
|
"price": 100,
|
||||||
|
"resourceChance": 1.0,
|
||||||
|
"resourceMinStartAmount": 50,
|
||||||
|
"resourceMaxStartAmount": 200
|
||||||
|
},
|
||||||
|
"store": [
|
||||||
|
{
|
||||||
|
"id": "item1",
|
||||||
|
"name": "Drill A",
|
||||||
|
"description": "This is drill A",
|
||||||
|
"price": 250,
|
||||||
|
"claimAmount": 50,
|
||||||
|
"completionTimeInMins": 5,
|
||||||
|
"upgrades": [
|
||||||
|
{ "tier": 1, "price": 200, "claimBoost": 10 },
|
||||||
|
{ "tier": 2, "price": 300, "claimBoost": 20 },
|
||||||
|
{ "tier": 3, "price": 400, "claimBoost": 30 },
|
||||||
|
{ "tier": 4, "price": 500, "claimBoost": 40 },
|
||||||
|
{ "tier": 5, "price": 600, "claimBoost": 50 }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "item2",
|
||||||
|
"name": "Drill B",
|
||||||
|
"description": "This is drill B",
|
||||||
|
"price": 250,
|
||||||
|
"claimAmount": 50,
|
||||||
|
"completionTimeInMins": 5,
|
||||||
|
"upgrades": [
|
||||||
|
{ "tier": 1, "price": 200, "claimBoost": 10 },
|
||||||
|
{ "tier": 2, "price": 300, "claimBoost": 20 },
|
||||||
|
{ "tier": 3, "price": 400, "claimBoost": 30 },
|
||||||
|
{ "tier": 4, "price": 500, "claimBoost": 40 },
|
||||||
|
{ "tier": 5, "price": 600, "claimBoost": 50 }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "item3",
|
||||||
|
"name": "Drill C",
|
||||||
|
"description": "This is drill C",
|
||||||
|
"price": 250,
|
||||||
|
"claimAmount": 50,
|
||||||
|
"completionTimeInMins": 5,
|
||||||
|
"upgrades": [
|
||||||
|
{ "tier": 1, "price": 200, "claimBoost": 10 },
|
||||||
|
{ "tier": 2, "price": 300, "claimBoost": 20 },
|
||||||
|
{ "tier": 3, "price": 400, "claimBoost": 30 },
|
||||||
|
{ "tier": 4, "price": 500, "claimBoost": 40 },
|
||||||
|
{ "tier": 5, "price": 600, "claimBoost": 50 }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
7
db.ts
Normal file
7
db.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import sqlite3 from "sqlite3";
|
||||||
|
import { open } from "sqlite";
|
||||||
|
|
||||||
|
export const dbConnection = open({
|
||||||
|
filename: "database.db",
|
||||||
|
driver: sqlite3.Database,
|
||||||
|
});
|
@ -1,8 +1,16 @@
|
|||||||
/** @type {import('next').NextConfig} */
|
/** @type {import('next').NextConfig} */
|
||||||
|
const fs = require("fs");
|
||||||
|
|
||||||
|
const gameConfigContent = fs.readFileSync("config/game-config.json", "utf-8");
|
||||||
|
const gameConfig = JSON.parse(gameConfigContent);
|
||||||
|
|
||||||
const nextConfig = {
|
const nextConfig = {
|
||||||
experimental: {
|
experimental: {
|
||||||
appDir: true,
|
appDir: true,
|
||||||
},
|
},
|
||||||
}
|
env: {
|
||||||
|
gameConfig: gameConfig,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
module.exports = nextConfig
|
module.exports = nextConfig;
|
||||||
|
2351
package-lock.json
generated
2351
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -10,6 +10,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@next/font": "13.1.6",
|
"@next/font": "13.1.6",
|
||||||
|
"@solana/web3.js": "^1.73.3",
|
||||||
"@types/node": "18.13.0",
|
"@types/node": "18.13.0",
|
||||||
"@types/react": "18.0.27",
|
"@types/react": "18.0.27",
|
||||||
"@types/react-dom": "18.0.10",
|
"@types/react-dom": "18.0.10",
|
||||||
@ -18,6 +19,8 @@
|
|||||||
"next": "13.1.6",
|
"next": "13.1.6",
|
||||||
"react": "18.2.0",
|
"react": "18.2.0",
|
||||||
"react-dom": "18.2.0",
|
"react-dom": "18.2.0",
|
||||||
|
"sqlite": "^4.1.2",
|
||||||
|
"sqlite3": "^5.1.4",
|
||||||
"typescript": "4.9.5"
|
"typescript": "4.9.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
42
sql/data.sql
Normal file
42
sql/data.sql
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
INSERT INTO users(name) VALUES
|
||||||
|
('Joe'),
|
||||||
|
('Emil'),
|
||||||
|
('Niko'),
|
||||||
|
('Plug'),
|
||||||
|
('Upgrade');
|
||||||
|
|
||||||
|
|
||||||
|
INSERT INTO bank_account(user_id, balance) VALUES (1, 500);
|
||||||
|
|
||||||
|
INSERT INTO resource_account(user_id, resname, balance) VALUES
|
||||||
|
(1, 'Sollux', 100),
|
||||||
|
(1, 'Shadowstone', 100),
|
||||||
|
(1, 'Azurium', 100),
|
||||||
|
(1, 'Novafor', 100),
|
||||||
|
(1, 'Nebulance', 100);
|
||||||
|
|
||||||
|
INSERT INTO staking_source(name, description, user_id, address) VALUES
|
||||||
|
('Selene''s Eye',
|
||||||
|
'Selene''s Eye is a large and mysterious moon, named for its distinctive appearance - a bright, glowing eye that seems to stare out from the void of space',
|
||||||
|
1,
|
||||||
|
'0x1234568');
|
||||||
|
|
||||||
|
INSERT INTO resource_well(source_id, resname, supply) VALUES
|
||||||
|
(1, 'Sollux', 200),
|
||||||
|
(1, 'Shadowstone', 175),
|
||||||
|
(1, 'Azurium', 0),
|
||||||
|
(1, 'Novafor', 25),
|
||||||
|
(1, 'Nebulance', 10);
|
||||||
|
|
||||||
|
|
||||||
|
INSERT INTO inventory_item(user_id, store_item_id) VALUES
|
||||||
|
(1, 'item1'),
|
||||||
|
(1, 'item2');
|
||||||
|
|
||||||
|
|
||||||
|
INSERT INTO staking_event(user_id, well_id, inventory_item_id, duration_in_mins, stake_amount) VALUES
|
||||||
|
(1, 1, 1, 5, 50),
|
||||||
|
(1, 3, 2, 5, 50);
|
||||||
|
|
||||||
|
INSERT INTO claim_event(staking_event_id, claim_amount) VALUES
|
||||||
|
(2, 50);
|
73
sql/queries.sql
Normal file
73
sql/queries.sql
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
-- Give extra moon bucks
|
||||||
|
|
||||||
|
UPDATE bank_account SET balance = 100 WHERE user_id = 1;
|
||||||
|
SELECT * FROM bank_account WHERE user_id = 1;
|
||||||
|
|
||||||
|
--
|
||||||
|
SELECT staking_event.id,well_id,staking_event.source_id,
|
||||||
|
inventory_item_id,staking_event.created_at,expiration_at
|
||||||
|
FROM staking_event
|
||||||
|
INNER JOIN resource_well ON resource_well.id = well_id
|
||||||
|
INNER JOIN staking_source on staking_event.source_id = staking_source.id
|
||||||
|
WHERE staking_event.source_id = ? AND staking_source.user_id = ?;
|
||||||
|
|
||||||
|
SELECT name,init_supply
|
||||||
|
FROM resource_well
|
||||||
|
INNER JOIN resource ON resource.id = resource_well.resource_id
|
||||||
|
WHERE source_id = 1;
|
||||||
|
|
||||||
|
SELECT inventory_item.id,store_item_id, COUNT(upgrade_event.id) as upgrades
|
||||||
|
FROM inventory_item
|
||||||
|
LEFT JOIN upgrade_event ON inventory_item.id = upgrade_event.inventory_item_id
|
||||||
|
WHERE inventory_item.user_id = 1
|
||||||
|
GROUP BY inventory_item.id;
|
||||||
|
|
||||||
|
SELECT inventory_item.id,store_item_id
|
||||||
|
FROM inventory_item;
|
||||||
|
|
||||||
|
SELECT staking_event.id,well_id,staking_event.source_id,
|
||||||
|
inventory_item_id,staking_event.created_at,expiration_at
|
||||||
|
FROM staking_event
|
||||||
|
INNER JOIN staking_source on staking_event.source_id = staking_source.id
|
||||||
|
WHERE staking_event.source_id = 4 AND staking_source.user_id = 1;
|
||||||
|
|
||||||
|
SELECT staking_event.id, staking_event.well_id, staking_event.source_id,
|
||||||
|
staking_event.inventory_item_id, staking_event.duration_in_mins,
|
||||||
|
staking_event.created_at
|
||||||
|
FROM staking_event
|
||||||
|
LEFT JOIN claim_event ON staking_event.id = claim_event.staking_event_id
|
||||||
|
WHERE staking_event.source_id = 4 AND claim_event.staking_event_id IS NULL;
|
||||||
|
|
||||||
|
UPDATE staking_event SET created_at = '2023-03-16 09:39:37' WHERE id = 3;
|
||||||
|
|
||||||
|
SELECT staking_event.id, staking_source.id as sourceId, resname as resourceType,
|
||||||
|
inventory_item_id, duration_in_mins, stake_amount, staking_event.created_at,
|
||||||
|
CASE WHEN claim_event.staking_event_id IS NULL THEN 1 ELSE 0 END AS unclaimed
|
||||||
|
FROM staking_event
|
||||||
|
INNER JOIN resource_well ON well_id = resource_well.id
|
||||||
|
INNER JOIN staking_source ON source_id = staking_source.id
|
||||||
|
LEFT JOIN claim_event ON staking_event.id = claim_event.staking_event_id
|
||||||
|
WHERE staking_event.user_id = 1;
|
||||||
|
|
||||||
|
SELECT resource_account.id, resource_id,resource.name,balance
|
||||||
|
FROM resource_account
|
||||||
|
INNER JOIN resource ON resource_id = resource.id
|
||||||
|
WHERE user_id = 1;
|
||||||
|
|
||||||
|
SELECT staking_source.id as sourceId,resource_well.id as wellId,resname,supply FROM resource_well
|
||||||
|
INNER JOIN staking_source ON staking_source.id = resource_well.source_id
|
||||||
|
WHERE staking_source.user_id = 1;
|
||||||
|
|
||||||
|
SELECT inventory_item.id, tier, store_item_id,
|
||||||
|
CASE WHEN claim_event.staking_event_id IS NULL THEN 0 ELSE 1 END AS staking
|
||||||
|
FROM inventory_item
|
||||||
|
LEFT JOIN staking_event ON inventory_item_id = inventory_item.id
|
||||||
|
LEFT JOIN claim_event ON staking_event.id = claim_event.staking_event_id
|
||||||
|
WHERE inventory_item.id = 3;
|
||||||
|
|
||||||
|
SELECT inventory_item.id, tier, store_item_id, staking_event.id as stakeId,
|
||||||
|
staking_event.created_at as stakeTime, duration_in_mins, stake_amount
|
||||||
|
FROM inventory_item
|
||||||
|
LEFT JOIN staking_event ON inventory_item_id = inventory_item.id
|
||||||
|
WHERE inventory_item.store_item_id = 'item3' AND user_id = 1
|
||||||
|
ORDER BY staking_event.created_at DESC;
|
88
sql/tables.sql
Normal file
88
sql/tables.sql
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
PRAGMA foreign_keys = ON;
|
||||||
|
|
||||||
|
CREATE TABLE users (
|
||||||
|
id integer primary key autoincrement,
|
||||||
|
wallet varchar,
|
||||||
|
name varchar(32) not null
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE staking_source(
|
||||||
|
id integer primary key autoincrement,
|
||||||
|
name varchar not null,
|
||||||
|
description varchar not null,
|
||||||
|
user_id int not null,
|
||||||
|
address varchar(128) not null,
|
||||||
|
created_at timestamp DEFAULT (current_timestamp || 'Z'),
|
||||||
|
CONSTRAINT fk_user FOREIGN KEY(user_id)
|
||||||
|
REFERENCES users(id)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE resource_well(
|
||||||
|
id integer primary key autoincrement,
|
||||||
|
resname varchar not null,
|
||||||
|
source_id int not null,
|
||||||
|
supply int not null,
|
||||||
|
CONSTRAINT fk_sid FOREIGN KEY(source_id)
|
||||||
|
REFERENCES staking_source(id)
|
||||||
|
ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE inventory_item(
|
||||||
|
id integer primary key autoincrement,
|
||||||
|
user_id int not null,
|
||||||
|
tier int not null default 0,
|
||||||
|
store_item_id varchar not null unique,
|
||||||
|
created_at timestamp DEFAULT (current_timestamp || 'Z'),
|
||||||
|
CONSTRAINT fk_user FOREIGN KEY(user_id)
|
||||||
|
REFERENCES users(id)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE upgrade_event(
|
||||||
|
id integer primary key autoincrement,
|
||||||
|
inventory_item_id int not null,
|
||||||
|
created_at timestamp DEFAULT (current_timestamp || 'Z'),
|
||||||
|
CONSTRAINT fk_iid FOREIGN KEY(inventory_item_id)
|
||||||
|
REFERENCES inventory_item(id)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE staking_event(
|
||||||
|
id integer primary key autoincrement,
|
||||||
|
user_id int not null,
|
||||||
|
well_id int not null,
|
||||||
|
inventory_item_id int not null,
|
||||||
|
duration_in_mins int not null,
|
||||||
|
stake_amount int not null,
|
||||||
|
created_at timestamp DEFAULT (current_timestamp || 'Z'),
|
||||||
|
CONSTRAINT fk_user FOREIGN KEY(user_id)
|
||||||
|
REFERENCES users(id)
|
||||||
|
CONSTRAINT fk_wid FOREIGN KEY(well_id)
|
||||||
|
REFERENCES resource_well(id)
|
||||||
|
CONSTRAINT fk_iiid FOREIGN KEY(inventory_item_id)
|
||||||
|
REFERENCES inventory_item(id)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE claim_event(
|
||||||
|
id integer primary key autoincrement,
|
||||||
|
staking_event_id int not null,
|
||||||
|
claim_amount int not null,
|
||||||
|
created_at timestamp DEFAULT (current_timestamp || 'Z'),
|
||||||
|
CONSTRAINT fk_se_id FOREIGN KEY(staking_event_id)
|
||||||
|
REFERENCES staking_event(id)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE bank_account(
|
||||||
|
id integer primary key autoincrement,
|
||||||
|
user_id int not null,
|
||||||
|
balance int not null default 0 CHECK (balance >= 0),
|
||||||
|
CONSTRAINT fk_user FOREIGN KEY(user_id)
|
||||||
|
REFERENCES users(id)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE resource_account(
|
||||||
|
id integer primary key autoincrement,
|
||||||
|
resname varchar not null,
|
||||||
|
user_id int not null,
|
||||||
|
balance int not null default 0 CHECK (balance >= 0),
|
||||||
|
CONSTRAINT fk_user FOREIGN KEY(user_id)
|
||||||
|
REFERENCES users(id)
|
||||||
|
);
|
@ -1,25 +1,16 @@
|
|||||||
"use client";
|
"use client";
|
||||||
import React, { useEffect, useState } from "react";
|
import React from "react";
|
||||||
import { IBankAccount } from "typings";
|
import { IBankAccount } from "typings";
|
||||||
|
|
||||||
const BankAccount = (props: { bankAccount: IBankAccount }) => {
|
const BankAccount = (props: { account: IBankAccount }) => {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={
|
className={" bg-gradient-to-br hover:bg-gradient-to-tr rounded-lg p-3"}
|
||||||
props.bankAccount.resourceType.bgColorClass +
|
|
||||||
" bg-gradient-to-br hover:bg-gradient-to-tr rounded-lg p-3"
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<div className="text-white">
|
<div className="text-white">
|
||||||
<span
|
<span className={" font-bold"}>MoonBucks</span>
|
||||||
className={
|
|
||||||
props.bankAccount.resourceType.fontColorClass + " font-bold"
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{props.bankAccount.resourceType.name}
|
|
||||||
</span>
|
|
||||||
<h3 className="text-2xl font-bold">
|
<h3 className="text-2xl font-bold">
|
||||||
{props.bankAccount.balance.toLocaleString("en-US", {
|
{props.account.primaryBalance.toLocaleString("en-US", {
|
||||||
minimumFractionDigits: 2,
|
minimumFractionDigits: 2,
|
||||||
maximumFractionDigits: 2,
|
maximumFractionDigits: 2,
|
||||||
})}{" "}
|
})}{" "}
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
"use client";
|
"use client";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { IBankAccount } from "typings";
|
import { IBankAccount } from "typings";
|
||||||
import BankAccount from "./BankAccount";
|
import ResourceAccount from "./ResourceAccount";
|
||||||
|
|
||||||
const BankAccountsView = (props: {
|
const BankAccountsView = (props: {
|
||||||
bankAccounts: IBankAccount[];
|
bankAccount: IBankAccount | undefined;
|
||||||
setLightBoxIsActive: () => void;
|
setLightBoxIsActive: () => void;
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
@ -22,9 +22,10 @@ const BankAccountsView = (props: {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{props.bankAccounts.map((bankAccount, id) => {
|
{props.bankAccount &&
|
||||||
return <BankAccount key={id} bankAccount={bankAccount} />;
|
props.bankAccount.resourceAccounts.map((account, id) => {
|
||||||
})}
|
return <ResourceAccount key={id} account={account} />;
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
45
src/app/Components/ErrorPopover.tsx
Normal file
45
src/app/Components/ErrorPopover.tsx
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
interface ErrorPopoverProps {
|
||||||
|
message: string;
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ErrorPopover: React.FC<ErrorPopoverProps> = ({ message, onClose }) => {
|
||||||
|
return (
|
||||||
|
<div className="fixed bottom-2 right-2 flex items-center justify-center z-50">
|
||||||
|
<div className="bg-white border border-red-500 rounded shadow-lg p-6 max-w-md mx-auto">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<div className="flex-shrink-0 text-red-500">
|
||||||
|
<i className="fas fa-exclamation-triangle"></i>
|
||||||
|
</div>
|
||||||
|
<div className="ml-3">
|
||||||
|
<h3 className="text-sm font-medium text-red-600">Error</h3>
|
||||||
|
<div className="mt-2 text-sm text-gray-700">{message}</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
className="ml-auto bg-transparent border-none text-red-600 p-1 focus:outline-none"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
strokeWidth={1.5}
|
||||||
|
stroke="currentColor"
|
||||||
|
className="w-6 h-6"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
d="M6 18L18 6M6 6l12 12"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ErrorPopover;
|
@ -4,11 +4,10 @@ import CardLayout from "../Layouts/CardLayout";
|
|||||||
|
|
||||||
const InventoryItem = (props: {
|
const InventoryItem = (props: {
|
||||||
inventoryItem: IInventoryItem;
|
inventoryItem: IInventoryItem;
|
||||||
inUse: boolean | undefined;
|
upgradeInventoryItem: (itemId: number) => void;
|
||||||
handleIncrementTier: (inventoryItem: IInventoryItem) => void;
|
|
||||||
}) => {
|
}) => {
|
||||||
const getCurrentTier = (index: number) => {
|
const getCurrentTier = (index: number) => {
|
||||||
return props.inventoryItem.storeItem.tiers[index];
|
return props.inventoryItem.storeItem.upgrades[index].tier;
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -16,7 +15,7 @@ const InventoryItem = (props: {
|
|||||||
<h3 className="text-xl font-bold mb-2">
|
<h3 className="text-xl font-bold mb-2">
|
||||||
{props.inventoryItem.storeItem.name}{" "}
|
{props.inventoryItem.storeItem.name}{" "}
|
||||||
<span className="bg-green-600 rounded-full px-2">
|
<span className="bg-green-600 rounded-full px-2">
|
||||||
{props.inventoryItem.currentTierIndex + 1}
|
{props.inventoryItem.currentTierIndex}
|
||||||
</span>
|
</span>
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-sm">{props.inventoryItem.storeItem.description}</p>
|
<p className="text-sm">{props.inventoryItem.storeItem.description}</p>
|
||||||
@ -24,18 +23,21 @@ const InventoryItem = (props: {
|
|||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<p className="font-bold mt-4">Yield</p>
|
<p className="font-bold mt-4">Yield</p>
|
||||||
<ul className="list-none">
|
<ul className="list-none">
|
||||||
{getCurrentTier(props.inventoryItem.currentTierIndex).tier}
|
{getCurrentTier(props.inventoryItem.currentTierIndex)}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<div className="flex">
|
<div className="flex">
|
||||||
{props.inUse ? (
|
{/* { Check if the item is in use } */}
|
||||||
|
{false ? (
|
||||||
<button className="bg-slate-400 text-slate-600 px-4 py-2 rounded-lg font-bold w-28 text-center cursor-not-allowed">
|
<button className="bg-slate-400 text-slate-600 px-4 py-2 rounded-lg font-bold w-28 text-center cursor-not-allowed">
|
||||||
Upgrade
|
Upgrade
|
||||||
</button>
|
</button>
|
||||||
) : (
|
) : (
|
||||||
<button
|
<button
|
||||||
onClick={() => props.handleIncrementTier(props.inventoryItem)}
|
onClick={() =>
|
||||||
|
props.upgradeInventoryItem(props.inventoryItem.id)
|
||||||
|
}
|
||||||
className="bg-slate-100 text-slate-900 px-4 py-2 rounded-lg font-bold w-28 text-center"
|
className="bg-slate-100 text-slate-900 px-4 py-2 rounded-lg font-bold w-28 text-center"
|
||||||
>
|
>
|
||||||
Upgrade
|
Upgrade
|
||||||
|
@ -5,15 +5,8 @@ import InventoryItem from "./InventoryItem";
|
|||||||
const InventoryItemView = (props: {
|
const InventoryItemView = (props: {
|
||||||
stakingSources: IStakingSource[] | null;
|
stakingSources: IStakingSource[] | null;
|
||||||
inventoryItems: IInventoryItem[] | null | undefined;
|
inventoryItems: IInventoryItem[] | null | undefined;
|
||||||
handleIncrementTier: (inventoryItem: IInventoryItem) => void;
|
upgradeInventoryItem: (itemId: number) => void;
|
||||||
}) => {
|
}) => {
|
||||||
const inUse = (inventoryItemId: number) => {
|
|
||||||
return props.stakingSources?.some((source) => {
|
|
||||||
if (!source.inventoryItem) return false;
|
|
||||||
return source.inventoryItem.id == inventoryItemId;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-slate-800 text-white p-4 rounded-lg">
|
<div className="bg-slate-800 text-white p-4 rounded-lg">
|
||||||
<h2 className="text-2xl font-bold mb-4">Your Inventory</h2>
|
<h2 className="text-2xl font-bold mb-4">Your Inventory</h2>
|
||||||
@ -22,8 +15,7 @@ const InventoryItemView = (props: {
|
|||||||
<InventoryItem
|
<InventoryItem
|
||||||
key={id}
|
key={id}
|
||||||
inventoryItem={inventoryItem}
|
inventoryItem={inventoryItem}
|
||||||
handleIncrementTier={props.handleIncrementTier}
|
upgradeInventoryItem={props.upgradeInventoryItem}
|
||||||
inUse={inUse(inventoryItem.id)}
|
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
@ -7,15 +7,13 @@ const ResourceAccount = (props: { account: IResourceAccount }) => {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={
|
className={
|
||||||
resourceToBg(props.account.resourceType) +
|
resourceToBg(props.account.name) +
|
||||||
" bg-gradient-to-br hover:bg-gradient-to-tr rounded-lg p-3 flex-1"
|
" bg-gradient-to-br hover:bg-gradient-to-tr rounded-lg p-3"
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div className="text-white">
|
<div className="text-white">
|
||||||
<span
|
<span className={resourceToFc(props.account.name) + " font-bold"}>
|
||||||
className={resourceToFc(props.account.resourceType) + " font-bold"}
|
{props.account.name}
|
||||||
>
|
|
||||||
{props.account.resourceType}
|
|
||||||
</span>
|
</span>
|
||||||
<h3 className="text-2xl font-bold">
|
<h3 className="text-2xl font-bold">
|
||||||
{props.account.balance.toLocaleString("en-US", {
|
{props.account.balance.toLocaleString("en-US", {
|
||||||
|
32
src/app/Components/SelectDropdown.tsx
Normal file
32
src/app/Components/SelectDropdown.tsx
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import React, { useState, ChangeEvent } from "react";
|
||||||
|
import { ISelectDropdownProps } from "typings";
|
||||||
|
|
||||||
|
const SelectDropdown: React.FC<ISelectDropdownProps> = (props) => {
|
||||||
|
const [selectedValue, setSelectedValue] = useState("");
|
||||||
|
|
||||||
|
const handleChange = (event: ChangeEvent<HTMLSelectElement>) => {
|
||||||
|
setSelectedValue(event.target.value);
|
||||||
|
if (props.onChange) {
|
||||||
|
props.onChange(event.target.value);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<select
|
||||||
|
value={selectedValue}
|
||||||
|
onChange={handleChange}
|
||||||
|
className="text-black block w-full p-2 border border-gray-300 rounded-lg"
|
||||||
|
>
|
||||||
|
<option value="">Select an option</option>
|
||||||
|
{props.options.map((option, index) => (
|
||||||
|
<option key={index} value={option.value}>
|
||||||
|
{option.label}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SelectDropdown;
|
@ -1,114 +1,87 @@
|
|||||||
import React, { useState, useEffect } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
import { IInventoryItem, IStakingSource, IClaimableResource } from "typings";
|
import { IInventoryItem, IStakingSource, IStake, IOption } from "typings";
|
||||||
import CardLayout from "../Layouts/CardLayout";
|
import CardLayout from "../Layouts/CardLayout";
|
||||||
|
import {
|
||||||
|
calculateRemainingTime,
|
||||||
|
prettifyTime,
|
||||||
|
getObjectFromArray,
|
||||||
|
} from "../../utils/helpers";
|
||||||
|
import SelectDropdown from "./SelectDropdown";
|
||||||
|
|
||||||
const StakingSource = (props: {
|
const StakingSource = (props: {
|
||||||
stakingSource: IStakingSource;
|
stakingSource: IStakingSource;
|
||||||
inventoryItems: IInventoryItem[] | null | undefined;
|
inventoryItems: IInventoryItem[] | null | undefined;
|
||||||
handleAddItem: (
|
claimStake: (stakingEventId: number) => void;
|
||||||
inventoryItem: IInventoryItem,
|
|
||||||
stakingSource: IStakingSource
|
|
||||||
) => void;
|
|
||||||
claimResource: (
|
|
||||||
stakingSource: IStakingSource,
|
|
||||||
claimedResource: IClaimableResource
|
|
||||||
) => boolean;
|
|
||||||
}) => {
|
}) => {
|
||||||
const [isMining, setIsMining] = useState(false);
|
const [activeStakes, setActiveStakes] = useState<IStake[]>([]);
|
||||||
const [miningTime, setMiningTime] = useState(0);
|
|
||||||
const [claimableResource, setClaimableResource] =
|
|
||||||
useState<IClaimableResource | null>(null);
|
|
||||||
const [activeInventoryItem, setActiveInventoryItem] =
|
|
||||||
useState<IInventoryItem | null>(null);
|
|
||||||
const [activeTier, setActiveTier] = useState(0);
|
|
||||||
|
|
||||||
|
// Check if claimable every second
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isMining && activeInventoryItem) {
|
const handleIsClaimable = props.stakingSource.activeStakes.map((stake) => {
|
||||||
const intervalId = setInterval(() => {
|
const remainingTime = calculateRemainingTime(
|
||||||
setMiningTime((prev) => prev - 1);
|
stake.startTime,
|
||||||
}, 1000);
|
stake.durationInMins
|
||||||
|
|
||||||
if (miningTime === 0) {
|
|
||||||
setIsMining(false);
|
|
||||||
|
|
||||||
// Get a random resource from available wells
|
|
||||||
let randResource =
|
|
||||||
props.stakingSource.resourceWells[
|
|
||||||
Math.floor(Math.random() * props.stakingSource.resourceWells.length)
|
|
||||||
];
|
|
||||||
|
|
||||||
// Get yield based on the tier
|
|
||||||
const totalYield = activeInventoryItem.storeItem.tiers[activeTier].tier;
|
|
||||||
|
|
||||||
// Construct the claimableResource
|
|
||||||
const claimableResource: IClaimableResource = {
|
|
||||||
resourceType: randResource.resourceType,
|
|
||||||
balance: totalYield,
|
|
||||||
};
|
|
||||||
|
|
||||||
setClaimableResource(claimableResource);
|
|
||||||
clearInterval(intervalId);
|
|
||||||
setMiningTime(activeInventoryItem.storeItem.timeToClaim);
|
|
||||||
}
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
clearInterval(intervalId);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}, [isMining, miningTime]);
|
|
||||||
|
|
||||||
const isChecked = (item: IInventoryItem) => {
|
|
||||||
if (!props.stakingSource.inventoryItem) return false;
|
|
||||||
return item.id === props.stakingSource.inventoryItem.id;
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleStartMining = () => {
|
|
||||||
if (props.stakingSource.inventoryItem) {
|
|
||||||
setActiveInventoryItem(props.stakingSource.inventoryItem);
|
|
||||||
setMiningTime(props.stakingSource.inventoryItem.storeItem.timeToClaim);
|
|
||||||
setActiveTier(props.stakingSource.inventoryItem.currentTierIndex);
|
|
||||||
setIsMining(true);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleClaim = () => {
|
|
||||||
if (!claimableResource) return;
|
|
||||||
if (props.claimResource(props.stakingSource, claimableResource)) {
|
|
||||||
setClaimableResource(null);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderButton = () => {
|
|
||||||
if (props.stakingSource.inventoryItem) {
|
|
||||||
if (!claimableResource && !isMining) {
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
onClick={() => handleStartMining()}
|
|
||||||
className="bg-slate-100 text-slate-900 px-4 py-2 rounded-lg font-bold text-center"
|
|
||||||
>
|
|
||||||
Start Mining
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isMining)
|
|
||||||
return (
|
|
||||||
<button className="bg-slate-400 text-slate-600 px-4 py-2 rounded-lg font-bold w-28 text-center cursor-not-allowed">
|
|
||||||
{miningTime}
|
|
||||||
</button>
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (claimableResource) {
|
const obj = {
|
||||||
|
...stake,
|
||||||
|
remainingTime: remainingTime,
|
||||||
|
isClaimable: remainingTime <= 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
return obj;
|
||||||
|
});
|
||||||
|
|
||||||
|
const intervalId = setInterval(() => {
|
||||||
|
setActiveStakes(handleIsClaimable);
|
||||||
|
}, 1000);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
clearInterval(intervalId);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleStartMining = () => {
|
||||||
|
console.log("Start mining..");
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClaim = (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
|
||||||
|
console.log("Start claiming..");
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSelectChange = (selectedValue: string) => {
|
||||||
|
console.log(selectedValue);
|
||||||
|
// Render a staking button
|
||||||
|
};
|
||||||
|
|
||||||
|
const RenderButtonOrCountdown = (props: { stake: IStake }) => {
|
||||||
|
if (!props.stake.remainingTime) return <p>Error</p>;
|
||||||
|
if (!props.stake.unclaimed)
|
||||||
|
return <p className="text-red-400 font-bold">Claimed</p>;
|
||||||
|
|
||||||
|
if (props.stake.remainingTime <= 0) {
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
onClick={() => handleClaim()}
|
onClick={(e) => handleClaim(e)}
|
||||||
className="bg-slate-100 text-slate-900 px-4 py-2 rounded-lg font-bold text-center"
|
className="bg-slate-100 text-slate-900 px-4 py-2 rounded-lg font-bold text-center"
|
||||||
>
|
>
|
||||||
Claim
|
Claim
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if (true) {
|
||||||
|
return (
|
||||||
|
<div className="flex">
|
||||||
|
<p>{prettifyTime(props.stake.remainingTime).hours} hrs.</p>
|
||||||
|
<p>{prettifyTime(props.stake.remainingTime).minutes} mins.</p>
|
||||||
|
<p>{prettifyTime(props.stake.remainingTime).seconds} sec.</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const isChecked = (item: IInventoryItem): boolean => {
|
||||||
|
return props.stakingSource.activeStakes.some((obj) => obj.id === item.id);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -117,69 +90,54 @@ const StakingSource = (props: {
|
|||||||
<p className="text-sm">{props.stakingSource.description}</p>
|
<p className="text-sm">{props.stakingSource.description}</p>
|
||||||
<div className="flex gap-4">
|
<div className="flex gap-4">
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<p className="font-bold mt-4">Resources</p>
|
<p className="font-bold mt-4 mb-2">Stakes</p>
|
||||||
<ul className="list-none">
|
{activeStakes &&
|
||||||
{props.stakingSource.resourceWells.map((resourceWell, id) => (
|
activeStakes.map((stake, id) => (
|
||||||
<li key={id}>
|
<div key={id} className="mb-3 border border-white p-2">
|
||||||
<span>{resourceWell.resourceType.name}</span>{" "}
|
<p>
|
||||||
<span className="text-teal-500 font-bold">
|
<span className="font-bold">Drill: </span>
|
||||||
{resourceWell.supply}
|
{props.inventoryItems &&
|
||||||
</span>
|
getObjectFromArray(props.inventoryItems, "id", stake.id)
|
||||||
</li>
|
?.storeItem.name}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<span className="font-bold">Resource: </span>
|
||||||
|
{stake.resourceType}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<span className="font-bold">Stake amount: </span>
|
||||||
|
{stake.stakeAmount}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<span className="font-bold">Start Time:</span>{" "}
|
||||||
|
{stake.startTime}
|
||||||
|
</p>
|
||||||
|
<RenderButtonOrCountdown stake={stake} />
|
||||||
|
</div>
|
||||||
))}
|
))}
|
||||||
</ul>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<p className="font-bold mt-4">Status</p>
|
<p className="font-bold mt-4 mb-2">Activate Drills</p>
|
||||||
<ul className="list-none">
|
{props.inventoryItems &&
|
||||||
<li className="flex">
|
props.inventoryItems.map(
|
||||||
<span className="text-green-600 mr-1">
|
(item, id) =>
|
||||||
<svg
|
!isChecked(item) && (
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
<div key={id} className="border border-white p-2">
|
||||||
fill="none"
|
<p>{item.storeItem.name}</p>
|
||||||
viewBox="0 0 24 24"
|
<SelectDropdown
|
||||||
strokeWidth="1.5"
|
options={props.stakingSource.resourceWells.map(
|
||||||
stroke="currentColor"
|
(well): IOption => ({
|
||||||
className="w-6 h-6"
|
value: well.resourceType,
|
||||||
>
|
label: well.resourceType,
|
||||||
<path
|
})
|
||||||
strokeLinecap="round"
|
)}
|
||||||
strokeLinejoin="round"
|
onChange={handleSelectChange}
|
||||||
d="M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
/>
|
||||||
/>
|
</div>
|
||||||
</svg>
|
)
|
||||||
</span>
|
)}
|
||||||
Operational
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
<p className="font-bold mt-4">Equipment</p>
|
|
||||||
{!isMining
|
|
||||||
? props.inventoryItems &&
|
|
||||||
props.inventoryItems.map((item, id) => (
|
|
||||||
<div key={id}>
|
|
||||||
<input
|
|
||||||
type="radio"
|
|
||||||
name="inventoryItem"
|
|
||||||
value={item.id}
|
|
||||||
checked={isChecked(item)}
|
|
||||||
onChange={() =>
|
|
||||||
props.handleAddItem(item, props.stakingSource)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<label htmlFor={item.id.toString()}>
|
|
||||||
{item.storeItem.name}{" "}
|
|
||||||
<span className="bg-white text-black rounded-full px-2">
|
|
||||||
{item.currentTierIndex + 1}
|
|
||||||
</span>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
))
|
|
||||||
: props.stakingSource.inventoryItem && (
|
|
||||||
<p>{props.stakingSource.inventoryItem.storeItem.name}</p>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{renderButton()}
|
|
||||||
</CardLayout>
|
</CardLayout>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,19 +1,12 @@
|
|||||||
"use client";
|
"use client";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { IInventoryItem, IStakingSource, IClaimableResource } from "typings";
|
import { IInventoryItem, IStakingSource, IStake, IGameConfig } from "typings";
|
||||||
import StakingSource from "./StakingSource";
|
import StakingSource from "./StakingSource";
|
||||||
|
|
||||||
const StakingSourcesView = (props: {
|
const StakingSourcesView = (props: {
|
||||||
stakingSources: IStakingSource[] | null;
|
stakingSources: IStakingSource[] | null;
|
||||||
inventoryItems: IInventoryItem[] | null | undefined;
|
inventoryItems: IInventoryItem[] | null | undefined;
|
||||||
handleAddItem: (
|
claimStake: (stakingEventId: number) => void;
|
||||||
inventoryItem: IInventoryItem,
|
|
||||||
stakingSource: IStakingSource
|
|
||||||
) => void;
|
|
||||||
claimResource: (
|
|
||||||
stakingSource: IStakingSource,
|
|
||||||
claimedResource: IClaimableResource
|
|
||||||
) => boolean;
|
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<div className="bg-slate-800 p-4 rounded-lg text-white">
|
<div className="bg-slate-800 p-4 rounded-lg text-white">
|
||||||
@ -24,8 +17,7 @@ const StakingSourcesView = (props: {
|
|||||||
key={id}
|
key={id}
|
||||||
stakingSource={stakingSource}
|
stakingSource={stakingSource}
|
||||||
inventoryItems={props.inventoryItems}
|
inventoryItems={props.inventoryItems}
|
||||||
handleAddItem={props.handleAddItem}
|
claimStake={props.claimStake}
|
||||||
claimResource={props.claimResource}
|
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
"use client";
|
"use client";
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { IStoreItem, IInventoryItem } from "typings";
|
import { IStoreItem } from "typings";
|
||||||
import CommonCardLayout from "../Layouts/CommonCardLayout";
|
import CommonCardLayout from "../Layouts/CommonCardLayout";
|
||||||
|
|
||||||
const StoreItem = (props: {
|
const StoreItem = (props: {
|
||||||
storeItem: IStoreItem;
|
storeItem: IStoreItem;
|
||||||
handleBuy: (storeItem: IStoreItem) => void;
|
buyStoreItem: (itemId: string) => void;
|
||||||
owned: boolean | undefined;
|
owned: boolean | undefined;
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
@ -22,20 +22,22 @@ const StoreItem = (props: {
|
|||||||
<p className="text-lg font-bold">$ {props.storeItem.price}</p>
|
<p className="text-lg font-bold">$ {props.storeItem.price}</p>
|
||||||
<p className="font-bold mt-3">Tier Upgrades</p>
|
<p className="font-bold mt-3">Tier Upgrades</p>
|
||||||
<table className="table-auto text-center">
|
<table className="table-auto text-center">
|
||||||
<tr>
|
<tbody>
|
||||||
<th className="border border-white py-1 px-3 w-18">Tier</th>
|
|
||||||
<th className="border border-white py-1 px-3 w-28">Price</th>
|
|
||||||
</tr>
|
|
||||||
{props.storeItem.tiers.map((tier) => (
|
|
||||||
<tr>
|
<tr>
|
||||||
<td className="border border-white py-1 px-3 w-18">
|
<th className="border border-white py-1 px-3 w-18">Tier</th>
|
||||||
{tier.tier}
|
<th className="border border-white py-1 px-3 w-28">Price</th>
|
||||||
</td>
|
|
||||||
<td className="border border-white py-1 px-3 w-28">
|
|
||||||
${tier.price}
|
|
||||||
</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
{props.storeItem.upgrades.map((tier, id) => (
|
||||||
|
<tr key={id}>
|
||||||
|
<td className="border border-white py-1 px-3 w-18">
|
||||||
|
{tier.tier}
|
||||||
|
</td>
|
||||||
|
<td className="border border-white py-1 px-3 w-28">
|
||||||
|
${tier.price}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center h-100">
|
<div className="flex items-center h-100">
|
||||||
@ -45,7 +47,7 @@ const StoreItem = (props: {
|
|||||||
</button>
|
</button>
|
||||||
) : (
|
) : (
|
||||||
<button
|
<button
|
||||||
onClick={() => props.handleBuy(props.storeItem)}
|
onClick={() => props.buyStoreItem(props.storeItem.id)}
|
||||||
className="bg-slate-100 text-slate-900 px-4 py-2 rounded-lg font-bold w-28 text-center"
|
className="bg-slate-100 text-slate-900 px-4 py-2 rounded-lg font-bold w-28 text-center"
|
||||||
>
|
>
|
||||||
Buy
|
Buy
|
||||||
|
@ -5,9 +5,9 @@ import StoreItem from "./StoreItem";
|
|||||||
const StoreItemView = (props: {
|
const StoreItemView = (props: {
|
||||||
storeItems: IStoreItem[] | null;
|
storeItems: IStoreItem[] | null;
|
||||||
inventoryItems: IInventoryItem[] | null | undefined;
|
inventoryItems: IInventoryItem[] | null | undefined;
|
||||||
handleBuy: (storeItem: IStoreItem) => void;
|
buyStoreItem: (itemId: string) => void;
|
||||||
}) => {
|
}) => {
|
||||||
const isOwned = (storeItemId: number) => {
|
const isOwned = (storeItemId: string) => {
|
||||||
return props.inventoryItems?.some(
|
return props.inventoryItems?.some(
|
||||||
(item) => item.storeItem.id == storeItemId
|
(item) => item.storeItem.id == storeItemId
|
||||||
);
|
);
|
||||||
@ -19,7 +19,6 @@ const StoreItemView = (props: {
|
|||||||
owned: isOwned(storeItem.id),
|
owned: isOwned(storeItem.id),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-slate-800 p-4 rounded-lg">
|
<div className="bg-slate-800 p-4 rounded-lg">
|
||||||
<h2 className="text-2xl font-bold mb-4 text-white">Store</h2>
|
<h2 className="text-2xl font-bold mb-4 text-white">Store</h2>
|
||||||
@ -30,7 +29,7 @@ const StoreItemView = (props: {
|
|||||||
<StoreItem
|
<StoreItem
|
||||||
key={id}
|
key={id}
|
||||||
storeItem={storeItem}
|
storeItem={storeItem}
|
||||||
handleBuy={props.handleBuy}
|
buyStoreItem={props.buyStoreItem}
|
||||||
owned={storeItem.owned}
|
owned={storeItem.owned}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
@ -1,14 +1,80 @@
|
|||||||
|
"use client";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import type { PublicKey } from "@solana/web3.js";
|
||||||
|
|
||||||
import "./globals.css";
|
import "./globals.css";
|
||||||
|
import { PhantomProvider } from "typings";
|
||||||
|
|
||||||
export default function RootLayout({
|
export default function RootLayout({
|
||||||
children,
|
children,
|
||||||
}: {
|
}: {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}) {
|
}) {
|
||||||
|
const [provider, setProvider] = useState<PhantomProvider>();
|
||||||
|
const [walletAddress, setWalletAddress] = useState("");
|
||||||
|
const [showErrorMessage, setShowErrorMessage] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const provider = getProvider();
|
||||||
|
setProvider(provider);
|
||||||
|
if (!provider) return;
|
||||||
|
provider.on("connect", async (publicKey: PublicKey) => {
|
||||||
|
const body = { publicKey: publicKey };
|
||||||
|
const res = await fetch("/api/user/login", {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
});
|
||||||
|
if (res.status === 200) {
|
||||||
|
setWalletAddress(publicKey.toBase58());
|
||||||
|
}
|
||||||
|
if (res.status === 404) {
|
||||||
|
setShowErrorMessage(true);
|
||||||
|
setTimeout(() => {
|
||||||
|
setShowErrorMessage(false);
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const getProvider = () => {
|
||||||
|
if ("phantom" in window) {
|
||||||
|
const anyWindow: any = window;
|
||||||
|
const provider = anyWindow.phantom.solana;
|
||||||
|
if (provider.isPhantom) return provider;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const connectWallet = () => {
|
||||||
|
if (!provider) return console.log("no provider");
|
||||||
|
provider.connect();
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head />
|
<head />
|
||||||
<body className="bg-slate-900">{children}</body>
|
<body className="bg-slate-900">
|
||||||
|
<nav className="text-white flex justify-between mt-4">
|
||||||
|
<div className="p-2 m-4 bg-slate-800 rounded-lg">Moon Miners</div>
|
||||||
|
{showErrorMessage && (
|
||||||
|
<div className="p-2 m-4 bg-red-800 rounded-lg">
|
||||||
|
Wallet address not registered!
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{walletAddress.length === 0 ? (
|
||||||
|
<button
|
||||||
|
onClick={connectWallet}
|
||||||
|
className="p-2 m-4 bg-slate-800 rounded-lg">
|
||||||
|
Connect Wallet
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<div className="p-2 m-4 bg-slate-800 rounded-lg">
|
||||||
|
{walletAddress}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</nav>
|
||||||
|
{children}
|
||||||
|
</body>
|
||||||
</html>
|
</html>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
346
src/app/page.tsx
346
src/app/page.tsx
@ -6,12 +6,14 @@ import StakingSourcesView from "./Components/StakingSourcesView";
|
|||||||
import BankAccountsView from "./Components/BankAccountsView";
|
import BankAccountsView from "./Components/BankAccountsView";
|
||||||
import StoreItemView from "./Components/StoreItemView";
|
import StoreItemView from "./Components/StoreItemView";
|
||||||
import ResourceStore from "./Components/ResourceStore";
|
import ResourceStore from "./Components/ResourceStore";
|
||||||
|
import ErrorPopover from "./Components/ErrorPopover";
|
||||||
|
import { gameConfig } from "@/utils/gameLogic";
|
||||||
import {
|
import {
|
||||||
IStoreItem,
|
IStoreItem,
|
||||||
IInventoryItem,
|
IInventoryItem,
|
||||||
IStakingSource,
|
IStakingSource,
|
||||||
IBankAccount,
|
IBankAccount,
|
||||||
IClaimableResource,
|
IStake,
|
||||||
} from "typings";
|
} from "typings";
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
@ -21,176 +23,220 @@ export default function Home() {
|
|||||||
const [stakingSources, setStakingSources] = useState<IStakingSource[] | null>(
|
const [stakingSources, setStakingSources] = useState<IStakingSource[] | null>(
|
||||||
[]
|
[]
|
||||||
);
|
);
|
||||||
const [bankAccounts, setBankAccounts] = useState<IBankAccount[]>([]);
|
const [bankAccount, setBankAccount] = useState<IBankAccount>();
|
||||||
const [storeItems, setStoreItems] = useState<IStoreItem[]>([]);
|
const [storeItems, setStoreItems] = useState<IStoreItem[]>([]);
|
||||||
|
const [userStakes, setUserStakes] = useState<IStake[]>([]);
|
||||||
const [lightBoxIsActive, setLightBoxIsActive] = useState(false);
|
const [lightBoxIsActive, setLightBoxIsActive] = useState(false);
|
||||||
|
const [userId, setUserId] = useState<number | null>(null);
|
||||||
|
const [error, setError] = useState<Error | null>(null);
|
||||||
|
const [errorTime, setErrorTime] = useState(3);
|
||||||
|
|
||||||
|
// EMIL
|
||||||
|
// It should be possible to add infinite drills on each resource (however many the user has)
|
||||||
|
|
||||||
|
// JOE
|
||||||
|
// Transaction table
|
||||||
|
// Add dev buttons to prototype
|
||||||
|
|
||||||
|
// DONE POST /user/login check if user exists
|
||||||
|
// DONE GET /user/USER_ID/stakes get stake event
|
||||||
|
// DONE POST /user/USER_ID/stakes/claim claim stake
|
||||||
|
// DONE POST /user/USER_ID/stakes/start start stake
|
||||||
|
// DONE GET /user/USER_ID/bank-account get balance
|
||||||
|
// DONE PUT /user/USER_ID/bank-account sell resource
|
||||||
|
// DONE GET /user/USER_ID/inventory-items get inventory items
|
||||||
|
// DONE POST /user/USER_ID/inventory-items buy item
|
||||||
|
// DONE GET /user/USER_ID/staking-sources get staking sources
|
||||||
|
// DONE POST /user/USER_ID/staking-sources create staking source
|
||||||
|
|
||||||
// Connect to DB here
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// get the user who is currently logged in
|
const fetchUser = async (wallet: string) => {
|
||||||
const loggedInUser = 1;
|
const response = await fetch(`/api/user/login`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({
|
||||||
|
wallet: wallet,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
if (response.status === 200) {
|
||||||
|
console.log("Logged in");
|
||||||
|
const { userId } = await response.json();
|
||||||
|
setUserId(userId);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const fetchInventoryItems = async () => {
|
const fetchBankAccount = async () => {
|
||||||
const response = await fetch(`/api/user/${loggedInUser}/inventory-items`);
|
const response = await fetch(`/api/user/${userId}/bank-account`);
|
||||||
const DBInventoryItems = await response.json();
|
const bankAccount = await response.json();
|
||||||
setInventoryItems(DBInventoryItems.message);
|
setBankAccount(bankAccount);
|
||||||
};
|
};
|
||||||
|
|
||||||
const fetchStakingSources = async () => {
|
const fetchStakingSources = async () => {
|
||||||
const response = await fetch(`/api/user/${loggedInUser}/staking-sources`);
|
const response = await fetch(`/api/user/${userId}/staking-sources`);
|
||||||
const DBStakingSources = await response.json();
|
const sources = await response.json();
|
||||||
setStakingSources(DBStakingSources.message);
|
setStakingSources(sources);
|
||||||
};
|
};
|
||||||
|
|
||||||
const fetchBankAccounts = async () => {
|
const fetchInventoryItems = async () => {
|
||||||
const response = await fetch(`/api/user/${loggedInUser}/bank-accounts`);
|
const response = await fetch(`/api/user/${userId}/inventory-items`);
|
||||||
const DBBankAccounts = await response.json();
|
const DBInventoryItems = await response.json();
|
||||||
setBankAccounts(DBBankAccounts.message);
|
|
||||||
};
|
|
||||||
|
|
||||||
const fetchStoreItems = async () => {
|
for (const invItem of DBInventoryItems) {
|
||||||
const response = await fetch(`/api/store/items`);
|
invItem.storeItem = gameConfig.store.find(
|
||||||
const DBStoreItems = await response.json();
|
(item) => item.id === invItem.storeItemId
|
||||||
setStoreItems(DBStoreItems.message);
|
|
||||||
};
|
|
||||||
|
|
||||||
fetchBankAccounts();
|
|
||||||
fetchStakingSources();
|
|
||||||
fetchInventoryItems();
|
|
||||||
fetchStoreItems();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// Use effect to update the items on staking sources when inventoryItems are updated
|
|
||||||
useEffect(() => {
|
|
||||||
const updateItemsOnStakingSources = () => {
|
|
||||||
const updatedStakingSources = stakingSources?.map((source) => {
|
|
||||||
const item = inventoryItems?.find(
|
|
||||||
(item) => source.inventoryItem?.id === item.id
|
|
||||||
);
|
);
|
||||||
if (item) {
|
}
|
||||||
return { ...source, inventoryItem: item };
|
|
||||||
} else {
|
setInventoryItems(DBInventoryItems);
|
||||||
return source;
|
};
|
||||||
}
|
|
||||||
|
const fetchStakes = async () => {
|
||||||
|
const response = await fetch(`/api/user/${userId}/stakes`);
|
||||||
|
const stakes = await response.json();
|
||||||
|
setUserStakes(
|
||||||
|
stakes.map((stake: IStake) => ({
|
||||||
|
...stake,
|
||||||
|
unclaimed: stake.unclaimed == 1,
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchUser("abcdefg"); // Public key goes here
|
||||||
|
// Nico is there a better way of checking if a user is logged in?
|
||||||
|
if (userId) {
|
||||||
|
setStoreItems(gameConfig.store);
|
||||||
|
fetchBankAccount();
|
||||||
|
fetchStakingSources();
|
||||||
|
fetchInventoryItems();
|
||||||
|
fetchStakes();
|
||||||
|
}
|
||||||
|
}, [userId]);
|
||||||
|
|
||||||
|
// Hide error automatically
|
||||||
|
useEffect(() => {
|
||||||
|
if (error) {
|
||||||
|
const intervalId = setInterval(() => {
|
||||||
|
setErrorTime((prev) => prev - 1);
|
||||||
|
}, 1000);
|
||||||
|
if (errorTime === 0) {
|
||||||
|
setError(null);
|
||||||
|
clearInterval(intervalId);
|
||||||
|
setErrorTime(3);
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
clearInterval(intervalId);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}, [error, errorTime]);
|
||||||
|
|
||||||
|
const claimStake = async (stakingEventId: number) => {
|
||||||
|
const response = await fetch(`/api/user/${userId}/stakes/claim`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ stakingEventId: stakingEventId }),
|
||||||
|
});
|
||||||
|
return await response.json();
|
||||||
|
};
|
||||||
|
|
||||||
|
const startStake = async (inventoryItemId: number, wellId: number) => {
|
||||||
|
const response = await fetch(`/api/user/${userId}/stakes/start`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({
|
||||||
|
inventoryItemId: inventoryItemId,
|
||||||
|
wellId: wellId,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
return await response.json();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Which object is it?
|
||||||
|
const sellResource = async (obj: any) => {
|
||||||
|
const response = await fetch(`/api/user/${userId}/bank-account`, {
|
||||||
|
method: "PUT",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({
|
||||||
|
obj: obj,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
return await response.json();
|
||||||
|
};
|
||||||
|
|
||||||
|
const buyStoreItem = async (itemId: string) => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/user/${userId}/inventory-items`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({
|
||||||
|
itemId: itemId,
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
updatedStakingSources && setStakingSources(updatedStakingSources);
|
|
||||||
};
|
|
||||||
if (stakingSources && stakingSources?.length > 0) {
|
|
||||||
updateItemsOnStakingSources();
|
|
||||||
}
|
|
||||||
}, [inventoryItems]);
|
|
||||||
|
|
||||||
const handleAddItem = (
|
if (!response.ok) {
|
||||||
inventoryItem: IInventoryItem,
|
const error = await response.text();
|
||||||
stakingSource: IStakingSource
|
setError(new Error(error));
|
||||||
) => {
|
return;
|
||||||
const newStakingSources = stakingSources?.map((source) => {
|
|
||||||
if (source.id === stakingSource.id) {
|
|
||||||
return { ...source, inventoryItem: inventoryItem };
|
|
||||||
} else {
|
|
||||||
return source;
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
if (newStakingSources) {
|
const data = await response.json();
|
||||||
setStakingSources(newStakingSources);
|
|
||||||
|
if (response.status == 200) {
|
||||||
|
// Return success message
|
||||||
|
console.log(data.message);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof Error) {
|
||||||
|
setError(error);
|
||||||
|
} else {
|
||||||
|
setError(new Error("An unknown error occurred."));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleBuy = (storeItem: IStoreItem) => {
|
const upgradeInventoryItem = async (itemId: number) => {
|
||||||
const hasItem = inventoryItems?.some((item) => {
|
try {
|
||||||
item.storeItem.id === storeItem.id;
|
const response = await fetch(`/api/user/${userId}/inventory-items`, {
|
||||||
});
|
method: "PUT",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({
|
||||||
|
itemId: itemId,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
if (hasItem) return;
|
if (!response.ok) {
|
||||||
|
const error = await response.text();
|
||||||
|
setError(new Error(error));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const getNewIndex = () => {
|
const data = await response.json();
|
||||||
if (!inventoryItems) return 0;
|
|
||||||
return inventoryItems.length;
|
|
||||||
};
|
|
||||||
|
|
||||||
const newInventoryItem = {
|
if (response.status == 200) {
|
||||||
id: getNewIndex(),
|
// Return success message
|
||||||
stakingSource: null,
|
console.log(data.message);
|
||||||
storeItem: storeItem,
|
}
|
||||||
currentTierIndex: 0,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (inventoryItems && inventoryItems !== undefined) {
|
if (response.status == 400) {
|
||||||
setInventoryItems([...inventoryItems, newInventoryItem]);
|
// return error message
|
||||||
|
setError(data.error);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof Error) {
|
||||||
|
setError(error);
|
||||||
|
} else {
|
||||||
|
setError(new Error("An unknown error occurred."));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const claimResource = (
|
const createStakingSource = async () => {
|
||||||
stakingSource: IStakingSource,
|
const response = await fetch(`/api/user/${userId}/staking-sources`, {
|
||||||
claimedResource: IClaimableResource
|
method: "POST",
|
||||||
): boolean => {
|
headers: { "Content-Type": "application/json" },
|
||||||
const bankAccount = bankAccounts.find(
|
body: JSON.stringify({}),
|
||||||
(obj) => obj["resourceType"]["name"] === claimedResource.resourceType.name
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!bankAccount) return false;
|
|
||||||
decrementResourceWell(stakingSource, claimedResource);
|
|
||||||
incrementBalance(bankAccount, claimedResource.balance);
|
|
||||||
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
|
|
||||||
const decrementResourceWell = (
|
|
||||||
stakingSource: IStakingSource,
|
|
||||||
claimedResource: IClaimableResource
|
|
||||||
) => {
|
|
||||||
const updatedResourceWells = stakingSource.resourceWells.map((well) => {
|
|
||||||
if (well.resourceType.name === claimedResource.resourceType.name) {
|
|
||||||
return { ...well, supply: well.supply - claimedResource.balance };
|
|
||||||
} else {
|
|
||||||
return well;
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
return await response.json();
|
||||||
const updatedStakingSources = stakingSources?.map((source) => {
|
|
||||||
if (source.id === stakingSource.id) {
|
|
||||||
return { ...source, resourceWells: updatedResourceWells };
|
|
||||||
} else {
|
|
||||||
return source;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
updatedStakingSources && setStakingSources(updatedStakingSources);
|
|
||||||
};
|
|
||||||
|
|
||||||
const decrementBalance = () => {};
|
|
||||||
|
|
||||||
const incrementBalance = (bankAccount: IBankAccount, amount: number) => {
|
|
||||||
const updatedBankAccounts = bankAccounts.map((account) => {
|
|
||||||
if (account.id === bankAccount.id) {
|
|
||||||
return { ...account, balance: account.balance + amount };
|
|
||||||
} else {
|
|
||||||
return account;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
setBankAccounts(updatedBankAccounts);
|
|
||||||
};
|
|
||||||
|
|
||||||
const getStoreItemConfiguration = () => {};
|
|
||||||
|
|
||||||
const handleIncrementTier = (inventoryItem: IInventoryItem) => {
|
|
||||||
console.log("Incrementing Tier");
|
|
||||||
// Check user has balance
|
|
||||||
// Decrement user balance
|
|
||||||
|
|
||||||
if (inventoryItem.currentTierIndex === 4) return;
|
|
||||||
|
|
||||||
const updatedInventoryItems = inventoryItems?.map((item) => {
|
|
||||||
if (item.id === inventoryItem.id) {
|
|
||||||
return { ...item, currentTierIndex: item.currentTierIndex + 1 };
|
|
||||||
} else {
|
|
||||||
return item;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (updatedInventoryItems !== undefined)
|
|
||||||
setInventoryItems(updatedInventoryItems);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSetLightBox = () => {
|
const handleSetLightBox = () => {
|
||||||
@ -206,8 +252,11 @@ export default function Home() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
{error && (
|
||||||
|
<ErrorPopover message={error.message} onClose={handleCloseError} />
|
||||||
|
)}
|
||||||
<BankAccountsView
|
<BankAccountsView
|
||||||
bankAccounts={bankAccounts}
|
bankAccount={bankAccount}
|
||||||
setLightBoxIsActive={handleSetLightBox}
|
setLightBoxIsActive={handleSetLightBox}
|
||||||
/>
|
/>
|
||||||
{lightBoxIsActive && (
|
{lightBoxIsActive && (
|
||||||
@ -220,18 +269,17 @@ export default function Home() {
|
|||||||
<StakingSourcesView
|
<StakingSourcesView
|
||||||
stakingSources={stakingSources}
|
stakingSources={stakingSources}
|
||||||
inventoryItems={inventoryItems}
|
inventoryItems={inventoryItems}
|
||||||
handleAddItem={handleAddItem}
|
claimStake={claimStake}
|
||||||
claimResource={claimResource}
|
|
||||||
/>
|
/>
|
||||||
<InventoryItemView
|
<InventoryItemView
|
||||||
stakingSources={stakingSources}
|
stakingSources={stakingSources}
|
||||||
inventoryItems={inventoryItems}
|
inventoryItems={inventoryItems}
|
||||||
handleIncrementTier={handleIncrementTier}
|
upgradeInventoryItem={upgradeInventoryItem}
|
||||||
/>
|
/>
|
||||||
<StoreItemView
|
<StoreItemView
|
||||||
storeItems={storeItems}
|
storeItems={storeItems}
|
||||||
inventoryItems={inventoryItems}
|
inventoryItems={inventoryItems}
|
||||||
handleBuy={handleBuy}
|
buyStoreItem={buyStoreItem}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
39
src/pages/api/drop.ts
Normal file
39
src/pages/api/drop.ts
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import { dbConnection } from "db";
|
||||||
|
import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
|
|
||||||
|
export default async function handler(
|
||||||
|
req: NextApiRequest,
|
||||||
|
res: NextApiResponse
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
if (req.method === "GET") {
|
||||||
|
const db = await dbConnection;
|
||||||
|
const users = "DROP TABLE users";
|
||||||
|
await db.all(users);
|
||||||
|
const resource = "DROP TABLE resource";
|
||||||
|
await db.all(resource);
|
||||||
|
const staking_source = "DROP TABLE staking_source";
|
||||||
|
await db.all(staking_source);
|
||||||
|
const resource_well = "DROP TABLE resource_well";
|
||||||
|
await db.all(resource_well);
|
||||||
|
const staking_event = "DROP TABLE staking_event";
|
||||||
|
await db.all(staking_event);
|
||||||
|
const claim_event = "DROP TABLE claim_event";
|
||||||
|
await db.all(claim_event);
|
||||||
|
const upgrade_event = "DROP TABLE upgrade_event";
|
||||||
|
await db.all(upgrade_event);
|
||||||
|
const store_item = "DROP TABLE store_item";
|
||||||
|
await db.all(store_item);
|
||||||
|
const inventory_item = "DROP TABLE inventory_item";
|
||||||
|
await db.all(inventory_item);
|
||||||
|
const bank_account = "DROP TABLE bank_account";
|
||||||
|
await db.all(bank_account);
|
||||||
|
const resource_account = "DROP TABLE resource_account";
|
||||||
|
await db.all(resource_account);
|
||||||
|
|
||||||
|
res.status(200).json("Tables dropped");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json(error);
|
||||||
|
}
|
||||||
|
}
|
27
src/pages/api/seed.ts
Normal file
27
src/pages/api/seed.ts
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import { dbConnection } from "db";
|
||||||
|
import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
|
import * as fs from 'fs';
|
||||||
|
|
||||||
|
export default async function handler(
|
||||||
|
req: NextApiRequest,
|
||||||
|
res: NextApiResponse
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
if (req.method === "GET") {
|
||||||
|
const db = await dbConnection;
|
||||||
|
|
||||||
|
// Read SQL queries from file
|
||||||
|
const sql1 = fs.readFileSync('sql/tables.sql', 'utf-8');
|
||||||
|
|
||||||
|
// Execute the SQL queries
|
||||||
|
await db.exec(sql1);
|
||||||
|
|
||||||
|
const sql2 = fs.readFileSync('sql/data.sql', 'utf-8');
|
||||||
|
await db.exec(sql2);
|
||||||
|
|
||||||
|
res.status(200).json("Database seeded");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json(error.message);
|
||||||
|
}
|
||||||
|
}
|
@ -1,80 +0,0 @@
|
|||||||
import type { NextApiRequest, NextApiResponse } from "next";
|
|
||||||
import { IStoreItem } from "typings";
|
|
||||||
|
|
||||||
export default function handler(req: NextApiRequest, res: NextApiResponse) {
|
|
||||||
try {
|
|
||||||
if (req.method === "GET") {
|
|
||||||
// query all store items
|
|
||||||
const storeItems: IStoreItem[] = [
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
name: "Eclipse Drill",
|
|
||||||
description:
|
|
||||||
"A compact and lightweight drill designed for use in tight and narrow mining tunnels.",
|
|
||||||
price: 225,
|
|
||||||
timeToClaim: 3,
|
|
||||||
tiers: [
|
|
||||||
{
|
|
||||||
tier: 500,
|
|
||||||
price: 50,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
tier: 600,
|
|
||||||
price: 75,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
tier: 700,
|
|
||||||
price: 100,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
tier: 800,
|
|
||||||
price: 150,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
tier: 900,
|
|
||||||
price: 200,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
name: "Moon Saw 2000",
|
|
||||||
description:
|
|
||||||
"A compact and lightweight drill designed for use in tight and narrow mining tunnels.",
|
|
||||||
price: 100,
|
|
||||||
timeToClaim: 3,
|
|
||||||
tiers: [
|
|
||||||
{
|
|
||||||
tier: 500,
|
|
||||||
price: 50,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
tier: 550,
|
|
||||||
price: 75,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
tier: 600,
|
|
||||||
price: 100,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
tier: 650,
|
|
||||||
price: 150,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
tier: 700,
|
|
||||||
price: 200,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
// if no store items send empty array
|
|
||||||
if (!storeItems) return res.status(200).json({ message: [] });
|
|
||||||
|
|
||||||
// if store items found send store items
|
|
||||||
return res.status(200).json({ message: storeItems });
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
res.status(500).json({ message: "Unexpexted server error" });
|
|
||||||
}
|
|
||||||
}
|
|
27
src/pages/api/user/[userId]/add-balance/[amount].ts
Normal file
27
src/pages/api/user/[userId]/add-balance/[amount].ts
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import { dbConnection } from "db";
|
||||||
|
import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
|
|
||||||
|
export default async function handler(
|
||||||
|
req: NextApiRequest,
|
||||||
|
res: NextApiResponse
|
||||||
|
) {
|
||||||
|
// http://127.0.0.1:3000/api/user/1/add-balance/100
|
||||||
|
// Remember to change to POST
|
||||||
|
try {
|
||||||
|
if (req.method === "GET") {
|
||||||
|
const { userId, amount } = req.query;
|
||||||
|
const db = await dbConnection;
|
||||||
|
const sql = `
|
||||||
|
UPDATE bank_account SET balance = balance + ?
|
||||||
|
WHERE user_id = ?`;
|
||||||
|
const result = await db.run(sql, [amount, userId])
|
||||||
|
if (result.changes == 0) {
|
||||||
|
return res.status(400).json({error: `User ${userId} was not found`});
|
||||||
|
} else {
|
||||||
|
return res.status(200).json({message: result});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
return res.status(500).json(error);
|
||||||
|
}
|
||||||
|
}
|
48
src/pages/api/user/[userId]/bank-account.ts
Normal file
48
src/pages/api/user/[userId]/bank-account.ts
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
import { dbConnection } from "db";
|
||||||
|
import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
|
|
||||||
|
export default async function handler(
|
||||||
|
req: NextApiRequest,
|
||||||
|
res: NextApiResponse
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
if (req.method === "GET") {
|
||||||
|
const { userId } = req.query;
|
||||||
|
const db = await dbConnection;
|
||||||
|
const userSql = `SELECT * FROM users WHERE id = ?`;
|
||||||
|
const user = await db.all(userSql, [userId]);
|
||||||
|
if (user.length === 0) return res.status(404).json("User not found");
|
||||||
|
|
||||||
|
const resourceAccountsSql = `
|
||||||
|
SELECT id,resname as resourceType,balance
|
||||||
|
FROM resource_account
|
||||||
|
WHERE user_id = ?`;
|
||||||
|
// need resourceName or at least a way to map together with gameConfiq
|
||||||
|
const resourceAccounts = await db.all(resourceAccountsSql, [userId]);
|
||||||
|
|
||||||
|
const bankAccountSql = `SELECT * FROM bank_account WHERE user_id = ?`;
|
||||||
|
const bankAccount = await db.all(bankAccountSql, [userId]);
|
||||||
|
|
||||||
|
const bankAccountObj = {
|
||||||
|
id: bankAccount[0].id,
|
||||||
|
primaryBalance: bankAccount[0].balance,
|
||||||
|
resourceAccounts: resourceAccounts,
|
||||||
|
};
|
||||||
|
|
||||||
|
return res.status(200).json(bankAccountObj);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.method === "PUT") {
|
||||||
|
// sell resource
|
||||||
|
// payload userId, key:resourceName/value:amount
|
||||||
|
const { userId } = req.query;
|
||||||
|
const { obj } = req.body;
|
||||||
|
// make sure they have each resource they say they have
|
||||||
|
// calculate conversion rates
|
||||||
|
// increment balance
|
||||||
|
// decrement resources
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json(error);
|
||||||
|
}
|
||||||
|
}
|
@ -1,73 +0,0 @@
|
|||||||
import type { NextApiRequest, NextApiResponse } from "next";
|
|
||||||
import { IBankAccount, IResourceType } from "typings";
|
|
||||||
|
|
||||||
export default function handler(req: NextApiRequest, res: NextApiResponse) {
|
|
||||||
try {
|
|
||||||
if (req.method === "GET") {
|
|
||||||
const { userId } = req.query;
|
|
||||||
|
|
||||||
// query db for user ID
|
|
||||||
const user = true;
|
|
||||||
// if user not found send error
|
|
||||||
if (!user) return res.status(404).json({ message: "User not found" });
|
|
||||||
|
|
||||||
// if user found query all bank accounts attached to this user
|
|
||||||
const DBresourceTypes: IResourceType[] = [
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
name: "Moonstone",
|
|
||||||
fontColorClass: "text-teal-400",
|
|
||||||
bgColorClass: "from-teal-800 to-teal-900",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
name: "Lunarite",
|
|
||||||
fontColorClass: "text-cyan-400",
|
|
||||||
bgColorClass: "from-cyan-800 to-cyan-900",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 3,
|
|
||||||
name: "Selenite",
|
|
||||||
fontColorClass: "text-purple-300",
|
|
||||||
bgColorClass: "from-purple-800 to-purple-900",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 4,
|
|
||||||
name: "Heliogem",
|
|
||||||
fontColorClass: "text-rose-300",
|
|
||||||
bgColorClass: "from-rose-800 to-rose-900",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
const bankAccounts: IBankAccount[] = [
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
resourceType: DBresourceTypes[0],
|
|
||||||
balance: 0,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
resourceType: DBresourceTypes[1],
|
|
||||||
balance: 0,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 3,
|
|
||||||
resourceType: DBresourceTypes[2],
|
|
||||||
balance: 0,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 4,
|
|
||||||
resourceType: DBresourceTypes[3],
|
|
||||||
balance: 0,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
// if bank accounts not found send empty array
|
|
||||||
if (!bankAccounts) return res.status(200).json({ message: [] });
|
|
||||||
|
|
||||||
// if bank accounts found send bank accounts
|
|
||||||
return res.status(200).json({ message: bankAccounts });
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
res.status(500).json({ message: "Unexpexted server error" });
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,26 +1,91 @@
|
|||||||
|
import { dbConnection } from "db";
|
||||||
import type { NextApiRequest, NextApiResponse } from "next";
|
import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
import { IInventoryItem } from "typings";
|
import { gameConfig } from "@/utils/gameLogic";
|
||||||
|
|
||||||
export default function handler(req: NextApiRequest, res: NextApiResponse) {
|
export default async function handler(
|
||||||
|
req: NextApiRequest,
|
||||||
|
res: NextApiResponse
|
||||||
|
) {
|
||||||
try {
|
try {
|
||||||
if (req.method === "GET") {
|
if (req.method === "GET") {
|
||||||
|
// Get all owned items
|
||||||
|
|
||||||
const { userId } = req.query;
|
const { userId } = req.query;
|
||||||
|
const db = await dbConnection;
|
||||||
|
const inventorySql = `
|
||||||
|
SELECT id, store_item_id as storeItemId, tier as currentTierIndex
|
||||||
|
FROM inventory_item WHERE user_id = ?`;
|
||||||
|
const inventoryItems = await db.all(inventorySql, [userId]);
|
||||||
|
return res.status(200).json(inventoryItems);
|
||||||
|
|
||||||
// query db for user ID
|
} else if (req.method === "POST") {
|
||||||
const user = true;
|
// Buy a new item
|
||||||
// if user not found send error
|
const { userId } = req.query;
|
||||||
if (!user) return res.status(404).json({ message: "User not found" });
|
const { itemId } = req.body;
|
||||||
|
const db = await dbConnection;
|
||||||
|
|
||||||
// if user found query all inventory items attached to this user
|
const storeItem = gameConfig.store.find((item) => item.id == itemId);
|
||||||
const inventoryItems: IInventoryItem[] = [];
|
|
||||||
|
|
||||||
// if inventory items not found send empty array
|
if (storeItem == undefined) {
|
||||||
if (!inventoryItems) return res.status(200).json({ message: [] });
|
return res.status(400).json({ error: "Item does not exist" });
|
||||||
|
}
|
||||||
|
const itemPrice = storeItem.price;
|
||||||
|
// TODO: Split the try catch to report already owned item error
|
||||||
|
try {
|
||||||
|
await db.run("BEGIN");
|
||||||
|
await db.run(`UPDATE bank_account SET balance = balance - ?
|
||||||
|
WHERE user_id = ?`, [itemPrice, userId]);
|
||||||
|
await db.run(`INSERT INTO inventory_item (user_id, store_item_id)
|
||||||
|
VALUES (?, ?)`, [userId, itemId]);
|
||||||
|
await db.run("COMMIT");
|
||||||
|
} catch (error) {
|
||||||
|
await db.exec("ROLLBACK");
|
||||||
|
return res.status(400).json({ error: "Either Insufficient funds or item is already owned" });
|
||||||
|
}
|
||||||
|
return res.status(200).json({ message: "Item purchased successfully" });
|
||||||
|
|
||||||
// if inventory items found send inventory items
|
} else if (req.method === "PUT") {
|
||||||
return res.status(200).json({ message: inventoryItems });
|
// Upgrade an existing item
|
||||||
|
const { userId } = req.query;
|
||||||
|
const { itemId } = req.body;
|
||||||
|
const db = await dbConnection;
|
||||||
|
|
||||||
|
const invSql = "SELECT id,tier,store_item_id FROM inventory_item WHERE id = ? AND user_id = ?";
|
||||||
|
|
||||||
|
const invItem = await db.get(invSql, [itemId, userId]);
|
||||||
|
|
||||||
|
const storeItem = gameConfig.store.find((item) => item.id == invItem.store_item_id);
|
||||||
|
if (storeItem == undefined) {
|
||||||
|
return res.status(400).json({ error: "Item does not exist" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const tier = invItem.tier;
|
||||||
|
if (tier == undefined) {
|
||||||
|
return res.status(400).json({ error: "Item does not exist" });
|
||||||
|
}
|
||||||
|
if (tier >= storeItem.upgrades.length) {
|
||||||
|
return res.status(400).json({ error: "Max upgrade reached" });
|
||||||
|
}
|
||||||
|
const upgradePrice = storeItem.upgrades[tier].price;
|
||||||
|
try {
|
||||||
|
await db.run("BEGIN");
|
||||||
|
await db.run(`UPDATE bank_account SET balance = balance - ?
|
||||||
|
WHERE user_id = ?`, [upgradePrice, userId]);
|
||||||
|
await db.run(`UPDATE inventory_item SET tier = tier + 1
|
||||||
|
WHERE user_id = ? AND store_item_id = ?;`, [userId, itemId]);
|
||||||
|
await db.run("INSERT INTO upgrade_event(inventory_item_id) VALUES ( ? )", [invItem.store_item_id]);
|
||||||
|
await db.run("COMMIT");
|
||||||
|
} catch (error) {
|
||||||
|
await db.exec("ROLLBACK");
|
||||||
|
return res.status(400).json({ error: "Insufficient funds" });
|
||||||
|
}
|
||||||
|
return res.status(200).json({ message: "Successfully upgraded item" });
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
res.status(500).json({ message: "Unexpexted server error" });
|
if(error instanceof Error){
|
||||||
|
res.status(500).json(error.message);
|
||||||
|
}else {
|
||||||
|
res.status(500).json("Unknow Error");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
60
src/pages/api/user/[userId]/stakes/claim.ts
Normal file
60
src/pages/api/user/[userId]/stakes/claim.ts
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
import { dbConnection } from "db";
|
||||||
|
import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
|
import { IInventoryItem } from "typings";
|
||||||
|
import {
|
||||||
|
calculateRemainingTime,
|
||||||
|
} from "../../../../../utils/helpers";
|
||||||
|
|
||||||
|
export default async function handler(
|
||||||
|
req: NextApiRequest,
|
||||||
|
res: NextApiResponse
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
if (req.method === "POST") {
|
||||||
|
const { userId } = req.query;
|
||||||
|
const { stakingEventId } = req.body;
|
||||||
|
|
||||||
|
const db = await dbConnection;
|
||||||
|
try {
|
||||||
|
const stake = await db.get("SELECT * FROM staking_event WHERE id = ? AND user_id = ?",
|
||||||
|
[stakingEventId, userId]);
|
||||||
|
|
||||||
|
if (stake == undefined) {
|
||||||
|
return res.status(400).json({error: "Could not find stake"});
|
||||||
|
}
|
||||||
|
|
||||||
|
const claim = await db.get("SELECT id FROM claim_event WHERE staking_event_id = ?",
|
||||||
|
[stakingEventId])
|
||||||
|
if (claim != undefined) {
|
||||||
|
return res.status(400).json({error: "Stake already claimed"});
|
||||||
|
}
|
||||||
|
|
||||||
|
const timeRemaining = calculateRemainingTime(stake.created_at, stake.duration_in_mins);
|
||||||
|
if (timeRemaining > 0) {
|
||||||
|
return res.status(400).json({error: "Staking period has not ended"});
|
||||||
|
}
|
||||||
|
const well = await db.get("SELECT id,resname,supply FROM resource_well WHERE id = ?",
|
||||||
|
[stake.well_id])
|
||||||
|
|
||||||
|
const claimAmount = stake.stake_amount > well.supply ? well.supply : stake.stake_amount;
|
||||||
|
|
||||||
|
await db.run("BEGIN");
|
||||||
|
await db.run("INSERT INTO claim_event(staking_event_id, claim_amount) VALUES (?, ?)",
|
||||||
|
[stake.id, claimAmount])
|
||||||
|
console.log(well.id);
|
||||||
|
const r1 = await db.run(`UPDATE resource_well SET supply = supply - ? WHERE id = ?`,
|
||||||
|
[claimAmount, well.id]);
|
||||||
|
console.log(stake.user_id, well.resname);
|
||||||
|
const r2 = await db.run(`UPDATE resource_account SET balance = balance + ?
|
||||||
|
WHERE user_id = ? AND resname = ?`, [claimAmount, stake.user_id, well.resname]);
|
||||||
|
await db.run("COMMIT");
|
||||||
|
} catch (error) {
|
||||||
|
await db.exec("ROLLBACK");
|
||||||
|
return res.status(400).json({ error: error.message});
|
||||||
|
}
|
||||||
|
return res.status(200).json({result: "Successfully claimed stake"});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json(error);
|
||||||
|
}
|
||||||
|
}
|
28
src/pages/api/user/[userId]/stakes/index.ts
Normal file
28
src/pages/api/user/[userId]/stakes/index.ts
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import { dbConnection } from "db";
|
||||||
|
import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
|
import { IInventoryItem } from "typings";
|
||||||
|
|
||||||
|
export default async function handler(
|
||||||
|
req: NextApiRequest,
|
||||||
|
res: NextApiResponse
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
if (req.method === "GET") {
|
||||||
|
const { userId } = req.query;
|
||||||
|
const db = await dbConnection;
|
||||||
|
const inventorySql = `
|
||||||
|
SELECT staking_event.id, staking_source.id as sourceId, resname as resourceType,
|
||||||
|
inventory_item_id, duration_in_mins, stake_amount, staking_event.created_at,
|
||||||
|
CASE WHEN claim_event.staking_event_id IS NULL THEN 1 ELSE 0 END AS unclaimed
|
||||||
|
FROM staking_event
|
||||||
|
INNER JOIN resource_well ON well_id = resource_well.id
|
||||||
|
INNER JOIN staking_source ON source_id = staking_source.id
|
||||||
|
LEFT JOIN claim_event ON staking_event.id = claim_event.staking_event_id
|
||||||
|
WHERE staking_event.user_id = ?`;
|
||||||
|
const inventoryItems = await db.all(inventorySql, [userId]);
|
||||||
|
return res.status(200).json(inventoryItems);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json(error);
|
||||||
|
}
|
||||||
|
}
|
79
src/pages/api/user/[userId]/stakes/start.ts
Normal file
79
src/pages/api/user/[userId]/stakes/start.ts
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
import { dbConnection } from "db";
|
||||||
|
import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
|
import {
|
||||||
|
calculateRemainingTime,
|
||||||
|
} from "../../../../../utils/helpers";
|
||||||
|
|
||||||
|
import { gameConfig } from "@/utils/gameLogic";
|
||||||
|
|
||||||
|
export default async function handler(
|
||||||
|
req: NextApiRequest,
|
||||||
|
res: NextApiResponse
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
if (req.method === "POST") {
|
||||||
|
const { userId } = req.query;
|
||||||
|
const {
|
||||||
|
inventoryItemId,
|
||||||
|
wellId,
|
||||||
|
} = req.body;
|
||||||
|
|
||||||
|
const db = await dbConnection;
|
||||||
|
try {
|
||||||
|
const inventorySql = `
|
||||||
|
SELECT inventory_item.id, tier, store_item_id, staking_event.id as stakeId,
|
||||||
|
staking_event.created_at as stakeTime, duration_in_mins, stake_amount
|
||||||
|
FROM inventory_item
|
||||||
|
LEFT JOIN staking_event ON inventory_item_id = inventory_item.id
|
||||||
|
WHERE inventory_item.store_item_id = ? AND inventory_item.user_id = ?
|
||||||
|
ORDER BY staking_event.created_at;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const invItem = await db.get(inventorySql, [inventoryItemId, userId]);
|
||||||
|
const well = await db.get(`
|
||||||
|
SELECT resource_well.id, resname, source_id, supply as wellId FROM resource_well
|
||||||
|
INNER JOIN staking_source ON source_id = staking_source.id
|
||||||
|
WHERE resource_well.id = ? AND user_id = ?;
|
||||||
|
`, [wellId, userId]);
|
||||||
|
|
||||||
|
if (well == undefined) {
|
||||||
|
return res.status(400).json({error: `Well ${wellId} not found`});
|
||||||
|
}
|
||||||
|
|
||||||
|
const item = gameConfig.store.find((i) => i.id == invItem.store_item_id);
|
||||||
|
if (invItem == undefined || well == undefined || item == undefined) {
|
||||||
|
return res.status(400).json({ error: "A resource was not found" });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (invItem.stakeId != undefined) {
|
||||||
|
const timeRemaining = calculateRemainingTime(invItem.stakeTime, invItem.duration_in_mins);
|
||||||
|
console.log(timeRemaining);
|
||||||
|
if (timeRemaining > 0) {
|
||||||
|
return res.status(400).json({error: `Item is still in use ${timeRemaining}`});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const boost = invItem.tier > 0 ? item.upgrades[invItem.tier - 1].claimBoost : 0;
|
||||||
|
const totalClaim = item.claimAmount + boost;
|
||||||
|
console.log(userId + " " + well.id + " " + invItem.id)
|
||||||
|
await db.run(`INSERT INTO staking_event(user_id, well_id, inventory_item_id, duration_in_mins, stake_amount)
|
||||||
|
VALUES (?,?,?,?,?)`,
|
||||||
|
[userId, well.id, invItem.id, item.completionTimeInMins, totalClaim]);
|
||||||
|
} catch (error) {
|
||||||
|
if(error instanceof Error){
|
||||||
|
return res.status(400).json({ error: error.message});
|
||||||
|
}else {
|
||||||
|
return res.status(400).json({ error: "Unknown Error"});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(200).json({result: "Successfully started a stake"});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if(error instanceof Error){
|
||||||
|
res.status(500).json(error.message);
|
||||||
|
}else {
|
||||||
|
res.status(500).json({ error: "Unknown Error"});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,46 +1,93 @@
|
|||||||
|
import { dbConnection } from "db";
|
||||||
import type { NextApiRequest, NextApiResponse } from "next";
|
import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
import { IStakingSource } from "typings";
|
import { IStakingSource } from "typings";
|
||||||
|
import { gameConfig } from "@/utils/gameLogic";
|
||||||
|
import {
|
||||||
|
generateRandomBase64String,
|
||||||
|
} from "../../../../utils/helpers";
|
||||||
|
|
||||||
export default function handler(req: NextApiRequest, res: NextApiResponse) {
|
export default async function handler(
|
||||||
|
req: NextApiRequest,
|
||||||
|
res: NextApiResponse
|
||||||
|
) {
|
||||||
try {
|
try {
|
||||||
if (req.method === "GET") {
|
if (req.method === "GET") {
|
||||||
const { userId } = req.query;
|
const { userId } = req.query;
|
||||||
|
const db = await dbConnection;
|
||||||
|
const sourcesSql = `
|
||||||
|
SELECT id, name, description FROM staking_source
|
||||||
|
WHERE staking_source.user_id = ?`;
|
||||||
|
const stakingSources = await db.all(sourcesSql, [userId]);
|
||||||
|
for (const source of stakingSources) {
|
||||||
|
const wellsSql = `
|
||||||
|
SELECT id, resname as resourceType, supply FROM resource_well
|
||||||
|
WHERE source_id = ?`;
|
||||||
|
const resourceWells = await db.all(wellsSql, [source.id]);
|
||||||
|
source.resourceWells = resourceWells;
|
||||||
|
const stakesSql = `
|
||||||
|
SELECT staking_event.id, resname as resourceType, source_id as stakingSourceId,
|
||||||
|
inventory_item_id, duration_in_mins, stake_amount,
|
||||||
|
staking_event.created_at as startTime
|
||||||
|
FROM staking_event
|
||||||
|
INNER JOIN resource_well ON well_id = resource_well.id
|
||||||
|
INNER JOIN staking_source ON source_id = staking_source.id
|
||||||
|
LEFT JOIN claim_event ON staking_event.id = claim_event.staking_event_id
|
||||||
|
WHERE staking_source.id = ? AND claim_event.staking_event_id IS NULL;`;
|
||||||
|
const activeStakes = await db.all(stakesSql, [source.id]);
|
||||||
|
source.activeStakes = activeStakes;
|
||||||
|
}
|
||||||
|
const typedStakingSources: IStakingSource[] = stakingSources.map(source => ({
|
||||||
|
...source,
|
||||||
|
// Emil: The variable names are messed up so i'm renaming them
|
||||||
|
activeStakes: source.activeStakes.map((stake) => ({
|
||||||
|
...stake,
|
||||||
|
inventoryItemId: stake.inventory_item_id,
|
||||||
|
durationInMins: stake.duration_in_mins,
|
||||||
|
stakeAmount: stake.stake_amount
|
||||||
|
}))
|
||||||
|
}))
|
||||||
|
|
||||||
// query db for user ID
|
return res.status(200).json(typedStakingSources);
|
||||||
const user = true;
|
}
|
||||||
// if user not found send error
|
if (req.method === "POST") {
|
||||||
if (!user) return res.status(404).json({ message: "User not found" });
|
const { userId } = req.query;
|
||||||
|
const db = await dbConnection;
|
||||||
|
|
||||||
// if user found query all staking sources attached to this user
|
try {
|
||||||
const stakingSources: IStakingSource[] = [
|
const randomName = "Moon 1";
|
||||||
{
|
const randomDescription = "This is a moon orbiting Selene's planet";
|
||||||
id: 1,
|
const randomKey = "0x" + generateRandomBase64String(16);
|
||||||
name: "Selene's Eye",
|
const resMax = gameConfig.moons.resourceMaxStartAmount;
|
||||||
description:
|
const resMin = gameConfig.moons.resourceMinStartAmount;
|
||||||
"Selene's Eye is a large and mysterious moon, named for its distinctive appearance - a bright, glowing eye that seems to stare out from the void of space",
|
const moonPrice = gameConfig.moons.price;
|
||||||
resourceWells:[
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
resourceType: {
|
|
||||||
id: 1,
|
|
||||||
name: "Moonstone",
|
|
||||||
fontColorClass: "text-teal-400",
|
|
||||||
bgColorClass: "from-teal-800 to-teal-900",
|
|
||||||
},
|
|
||||||
supply: 10000,
|
|
||||||
}
|
|
||||||
],
|
|
||||||
inventoryItem: null,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
// if staking sources not found send empty array
|
await db.run("BEGIN");
|
||||||
if (!stakingSources) return res.status(200).json({ message: [] });
|
await db.run(`UPDATE bank_account SET balance = balance - ?
|
||||||
|
WHERE user_id = ?`, [moonPrice, userId]);
|
||||||
// if staking sources found send staking sources
|
const result = await db.run(`INSERT INTO staking_source(user_id, name, description, address)
|
||||||
return res.status(200).json({ message: stakingSources });
|
VALUES (?, ?, ?, ?)`, [userId, randomName, randomDescription, randomKey]);
|
||||||
|
const sourceId = result.lastID;
|
||||||
|
for (const resname of gameConfig.resources) {
|
||||||
|
if (Math.random() < gameConfig.moons.resourceChance) {
|
||||||
|
const randomNumber = Math.random();
|
||||||
|
const range = resMax - resMin + 1;
|
||||||
|
const initSupply = Math.floor(Math.random() * range + resMin);
|
||||||
|
await db.run(`INSERT INTO resource_well (source_id, resname, supply)
|
||||||
|
VALUES (?, ?, ?)`, [sourceId, resname, initSupply]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await db.run("COMMIT");
|
||||||
|
return res.status(200).json({"stakingSourceId": sourceId});
|
||||||
|
} catch (error) {
|
||||||
|
await db.exec("ROLLBACK");
|
||||||
|
if (error.message.includes("CHECK constraint failed")) {
|
||||||
|
return res.status(400).json({ "error": "Insuficcient funds" });
|
||||||
|
} else {
|
||||||
|
return res.status(400).json({ "error": error.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
res.status(500).json({ message: "Unexpexted server error" });
|
res.status(500).json(error.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
26
src/pages/api/user/login.ts
Normal file
26
src/pages/api/user/login.ts
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import { dbConnection } from "db";
|
||||||
|
import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
|
|
||||||
|
export default async function handler(
|
||||||
|
req: NextApiRequest,
|
||||||
|
res: NextApiResponse
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
if (req.method === "POST") {
|
||||||
|
const { wallet } = req.body;
|
||||||
|
// const db = await dbConnection;
|
||||||
|
// const user = await db.get("SELECT id FROM users WHERE wallet = ?", [wallet]);
|
||||||
|
|
||||||
|
// Comment me out
|
||||||
|
return res.status(200).json({ userId: 1});
|
||||||
|
|
||||||
|
/* if (user != undefined) {
|
||||||
|
return res.status(200).json({ userId: user.id});
|
||||||
|
} else {
|
||||||
|
return res.status(404).json({ message: "User not found" });
|
||||||
|
} */
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json(error);
|
||||||
|
}
|
||||||
|
}
|
3
src/utils/gameLogic.ts
Normal file
3
src/utils/gameLogic.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import { IGameConfig } from "typings";
|
||||||
|
|
||||||
|
export const gameConfig = process.env.gameConfig as unknown as IGameConfig;
|
@ -1,3 +1,6 @@
|
|||||||
|
import { TimeDuration } from "typings";
|
||||||
|
import crypto from 'crypto';
|
||||||
|
|
||||||
export function getObjectFromArray<T>(array: T[], key: keyof T, value: any): T | undefined {
|
export function getObjectFromArray<T>(array: T[], key: keyof T, value: any): T | undefined {
|
||||||
return array.find(obj => obj[key] === value);
|
return array.find(obj => obj[key] === value);
|
||||||
}
|
}
|
||||||
@ -9,5 +12,59 @@ export function pushElementToArray<T>(array: T[], element: T): T[] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function sumValues<T extends Record<K, number>, K extends keyof T>(array: T[], key: K): number {
|
export function sumValues<T extends Record<K, number>, K extends keyof T>(array: T[], key: K): number {
|
||||||
return array.reduce((acc, obj) => acc + obj[key], 0);
|
return array.reduce((acc, obj) => acc + obj[key], 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IMapping {
|
||||||
|
[key: string]: any
|
||||||
|
}
|
||||||
|
export function resourceToBg(resourceName: string): string {
|
||||||
|
|
||||||
|
const mapping: IMapping = {
|
||||||
|
"Sollux": "from-teal-800 to-teal-900",
|
||||||
|
"Shadowstone": "from-cyan-800 to-cyan-900",
|
||||||
|
"Azurium": "from-purple-800 to-purple-900",
|
||||||
|
"Novafor": "from-rose-800 to-rose-900",
|
||||||
|
"Nebulance": "from-rose-800 to-rose-900"
|
||||||
|
}
|
||||||
|
|
||||||
|
return mapping[resourceName]
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resourceToFc(resourceName: string): string {
|
||||||
|
|
||||||
|
const mapping: IMapping = {
|
||||||
|
"Sollux": "text-teal-400",
|
||||||
|
"Shadowstone": "text-cyan-400",
|
||||||
|
"Azurium": "text-purple-300",
|
||||||
|
"Novafor": "text-rose-300",
|
||||||
|
"Nebulance": "text-rose-300"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return mapping[resourceName]
|
||||||
|
}
|
||||||
|
|
||||||
|
export const calculateRemainingTime = (startTime: string, durationInMins: number) => {
|
||||||
|
const start = new Date(startTime);
|
||||||
|
const end = new Date(start.getTime() + durationInMins * 60000);
|
||||||
|
const remaining = end.getTime() - Date.now();
|
||||||
|
return remaining;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const prettifyTime = (remainingTime: number) => {
|
||||||
|
const hours = Math.floor((remainingTime / (1000 * 60 * 60)) % 24);
|
||||||
|
const minutes = Math.floor((remainingTime / (1000 * 60)) % 60);
|
||||||
|
const seconds = Math.floor((remainingTime / 1000) % 60);
|
||||||
|
return { hours, minutes, seconds };
|
||||||
|
}
|
||||||
|
|
||||||
|
export const generateRandomBase64String = (length: number) => {
|
||||||
|
// Create a random byte array of the desired length.
|
||||||
|
const byteArray = new Uint8Array(length);
|
||||||
|
crypto.getRandomValues(byteArray);
|
||||||
|
|
||||||
|
// Convert the byte array to a base64 string.
|
||||||
|
const base64String = btoa(String.fromCharCode(...byteArray));
|
||||||
|
|
||||||
|
return base64String;
|
||||||
|
}
|
||||||
|
52
test-endpoints.rest
Normal file
52
test-endpoints.rest
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
:headers = <<
|
||||||
|
Content-Type: application/json
|
||||||
|
#
|
||||||
|
|
||||||
|
# Get Inventory Items
|
||||||
|
GET http://localhost:3000/api/seed
|
||||||
|
:headers
|
||||||
|
|
||||||
|
# Get Inventory Items
|
||||||
|
POST http://localhost:3000/api/user/login
|
||||||
|
:headers
|
||||||
|
{ "wallet" : "Wallet12345678" }
|
||||||
|
|
||||||
|
# Get Inventory Items
|
||||||
|
GET http://localhost:3000/api/user/1/bank-account
|
||||||
|
:headers
|
||||||
|
|
||||||
|
# Get Inventory Items
|
||||||
|
GET http://localhost:3000/api/user/1/staking-sources
|
||||||
|
:headers
|
||||||
|
|
||||||
|
# Get Inventory Items
|
||||||
|
POST http://localhost:3000/api/user/1/staking-sources
|
||||||
|
:headers
|
||||||
|
|
||||||
|
# Get Inventory Items
|
||||||
|
GET http://localhost:3000/api/user/1/inventory-items
|
||||||
|
:headers
|
||||||
|
|
||||||
|
# Buy a new Item
|
||||||
|
POST http://localhost:3000/api/user/1/inventory-items/
|
||||||
|
:headers
|
||||||
|
{ "itemId" : "item3" }
|
||||||
|
|
||||||
|
# Upgrade an owned item
|
||||||
|
PUT http://localhost:3000/api/user/1/inventory-items/
|
||||||
|
:headers
|
||||||
|
{ "itemId" : "item1" }
|
||||||
|
|
||||||
|
# Get stakes
|
||||||
|
GET http://localhost:3000/api/user/1/stakes/
|
||||||
|
:headers
|
||||||
|
|
||||||
|
# Start a stake
|
||||||
|
POST http://localhost:3000/api/user/1/stakes/start
|
||||||
|
:headers
|
||||||
|
{ "inventoryItemId": "item1", "wellId": 7 }
|
||||||
|
|
||||||
|
# Claim a stake
|
||||||
|
POST http://localhost:3000/api/user/1/stakes/claim
|
||||||
|
:headers
|
||||||
|
{ "stakingEventId" : 4 }
|
91
typings.d.ts
vendored
91
typings.d.ts
vendored
@ -11,16 +11,29 @@ export interface TimeDuration {
|
|||||||
|
|
||||||
export interface IResourceWell {
|
export interface IResourceWell {
|
||||||
id: number;
|
id: number;
|
||||||
resourceType: IResourceType;
|
resourceType: string;
|
||||||
supply: number;
|
supply: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IStake {
|
||||||
|
id: number;
|
||||||
|
resourceType: string;
|
||||||
|
stakingSourceId: number;
|
||||||
|
startTime: string;
|
||||||
|
inventoryItemId: number;
|
||||||
|
stakeAmount: number;
|
||||||
|
durationInMins: number;
|
||||||
|
unclaimed: boolean;
|
||||||
|
claimable: boolean | undefined
|
||||||
|
remainingTime: number | undefined
|
||||||
|
}
|
||||||
|
|
||||||
export interface IStakingSource {
|
export interface IStakingSource {
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
description: string;
|
description: string;
|
||||||
resourceWells: IResourceWell[];
|
resourceWells: IResourceWell[];
|
||||||
inventoryItem: IInventoryItem | null;
|
activeStakes: IStake[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IInventoryItem {
|
export interface IInventoryItem {
|
||||||
@ -30,26 +43,35 @@ export interface IInventoryItem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface IStoreItem {
|
export interface IStoreItem {
|
||||||
id: number;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
description: string;
|
description: string;
|
||||||
price: number;
|
price: number;
|
||||||
timeToClaim: number;
|
completionTimeInMins: number;
|
||||||
tiers: {
|
claimAmount: number;
|
||||||
|
upgrades: {
|
||||||
tier: number;
|
tier: number;
|
||||||
price: number;
|
price: number;
|
||||||
|
claimBoost: number;
|
||||||
}[];
|
}[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IResourceAccount {
|
||||||
|
id: number;
|
||||||
|
resourceType: string;
|
||||||
|
balance: number;
|
||||||
|
}
|
||||||
|
|
||||||
export interface IBankAccount {
|
export interface IBankAccount {
|
||||||
id: number;
|
id: number;
|
||||||
resourceType: IResourceType;
|
primaryBalance: number;
|
||||||
balance: number;
|
resourceAccounts: IResourceAccount[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IClaimableResource {
|
export interface IGameConfig {
|
||||||
resourceType: IResourceType;
|
resources: string[];
|
||||||
balance: number;
|
[key: string]: any;
|
||||||
|
store: IStoreItem[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IConversionPair {
|
export interface IConversionPair {
|
||||||
@ -57,3 +79,52 @@ export interface IConversionPair {
|
|||||||
resourceAmount: number;
|
resourceAmount: number;
|
||||||
moneyAmount: number
|
moneyAmount: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Phantom
|
||||||
|
export interface PhantomProvider {
|
||||||
|
publicKey: PublicKey | null;
|
||||||
|
isConnected: boolean | null;
|
||||||
|
signAndSendTransaction: (
|
||||||
|
transaction: Transaction,
|
||||||
|
opts?: SendOptions
|
||||||
|
) => Promise<{ signature: string; publicKey: PublicKey }>;
|
||||||
|
signTransaction: (transaction: Transaction) => Promise<Transaction>;
|
||||||
|
signAllTransactions: (transactions: Transaction[]) => Promise<Transaction[]>;
|
||||||
|
signMessage: (
|
||||||
|
message: Uint8Array | string,
|
||||||
|
display?: DisplayEncoding
|
||||||
|
) => Promise<any>;
|
||||||
|
connect: (opts?: Partial<ConnectOpts>) => Promise<{ publicKey: PublicKey }>;
|
||||||
|
disconnect: () => Promise<void>;
|
||||||
|
on: (event: PhantomEvent, handler: (args: any) => void) => void;
|
||||||
|
request: (method: PhantomRequestMethod, params: any) => Promise<unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
type DisplayEncoding = "utf8" | "hex";
|
||||||
|
|
||||||
|
interface ConnectOpts {
|
||||||
|
onlyIfTrusted: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
type PhantomEvent = "connect" | "disconnect" | "accountChanged";
|
||||||
|
|
||||||
|
type PhantomRequestMethod =
|
||||||
|
| "connect"
|
||||||
|
| "disconnect"
|
||||||
|
| "signAndSendTransaction"
|
||||||
|
| "signTransaction"
|
||||||
|
| "signAllTransactions"
|
||||||
|
| "signMessage";
|
||||||
|
|
||||||
|
|
||||||
|
// Generic stuff
|
||||||
|
|
||||||
|
export interface IOption {
|
||||||
|
value: string,
|
||||||
|
label: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ISelectDropdownProps {
|
||||||
|
options: IOption[];
|
||||||
|
onChange?: (value: string) => void;
|
||||||
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user