Skip to content

Commit

Permalink
Merge pull request #15 from ardriveapp/upload-folder-page
Browse files Browse the repository at this point in the history
feat(upload folder): turbo upload folder MVP PE-4643
fedellen authored Oct 3, 2024
2 parents 3fea84d + c9568ac commit f54a1e0
Showing 14 changed files with 1,714 additions and 369 deletions.
1 change: 1 addition & 0 deletions .nvmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
v20.12.2
6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "turbo-app",
"version": "1.0.0",
"version": "1.1.0",
"type": "module",
"description": "ArDrive Turbo App",
"homepage": "./",
@@ -19,7 +19,9 @@
"test": "echo \"TODO: add tests\" && exit 0"
},
"dependencies": {
"@ardrive/turbo-sdk": "latest",
"@ardrive/turbo-sdk": "1.19.0",
"@solana/web3.js": "^1.95.3",
"ethers": "^6.13.3",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router-dom": "^6.20.1"
2 changes: 2 additions & 0 deletions src/Router.tsx
Original file line number Diff line number Diff line change
@@ -3,6 +3,7 @@ import { GiftPage } from "./pages/GiftPage";
import { RedeemPage } from "./pages/RedeemPage";
import { FiatTopUpPage } from "./pages/FiatTopUpPage";
import { CryptoTopUpPage } from "./pages/CryptoTopUpPage";
import { UploadPage } from "./pages/UploadPage";

export function Router() {
return (
@@ -13,6 +14,7 @@ export function Router() {
<Route path="/top-up" element={<FiatTopUpPage />} />
<Route path="/top-up/fiat" element={<FiatTopUpPage />} />
<Route path="/top-up/crypto" element={<CryptoTopUpPage />} />
<Route path="/upload" element={<UploadPage />} />

<Route
path="/"
75 changes: 75 additions & 0 deletions src/components/TurboWalletConnector.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
.wallet-connector {
padding: 0rem 1.5rem 1.5rem 1.5rem;
}

.wallet-info {
font-family: Wavehaus-Bold;
font-size: 1rem;
color: var(--white);
background-color: var(--off-gray);
padding: 1.5rem;
border-radius: 8px;
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
}

.wallet-info .wallet-type {
font-size: 0.9rem;
font-weight: bold;
color: var(--ardrive-red);
margin-right: 0.5rem;
}

.wallet-info .wallet-address {
font-family: Wavehaus-Bold; /* Optional: to give the address a monospaced look */
font-size: 1.1rem;
color: var(--text-white);
margin-right: 1rem;
}

.wallet-connector-buttons {
display: flex;
flex-direction: row;
justify-content: center;
gap: 1rem;
width: 100%;
}

.wallet-connector button {
background-color: var(--ardrive-red);
color: var(--white);
padding: 0.75rem 1.25rem;
border: none;
border-radius: 0.5rem;
cursor: pointer;
font-family: Wavehaus-Bold;
width: auto;
}

.wallet-connector button:hover {
background-color: var(--ardrive-red);
filter: brightness(0.9);
}

.wallet-connector button:disabled {
background-color: var(--light-gray);
cursor: not-allowed;
}

.wallet-info button {
font-size: 0.6rem;
background-color: var(--light-gray);
color: var(--black);
border: none;
padding: 0.25rem 0.75rem;
border-radius: 0.25rem;
cursor: pointer;
margin-left: auto; /* Push the button to the right */
}

.wallet-info button:hover {
background-color: var(--ardrive-red);
color: var(--white);
}
136 changes: 136 additions & 0 deletions src/components/TurboWalletConnector.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
import {
TurboAuthenticatedClient,
TurboFactory,
SolanaWalletAdapter,
ArconnectSigner,
} from "@ardrive/turbo-sdk/web";
import { PublicKey } from "@solana/web3.js";
import { ethers } from "ethers";
import { useState } from "react";
import { turboConfig } from "../constants";
import { getArconnect } from "../utils/arconnect";
import "./TurboWalletConnector.css";

interface TurboWalletConnectorProps {
setTurbo: (turbo: TurboAuthenticatedClient | undefined) => void;
}

export default function TurboWalletConnector({
setTurbo,
}: TurboWalletConnectorProps): JSX.Element {
const [currentToken, setCurrentToken] = useState<string | undefined>(
undefined,
);
const [walletAddress, setWalletAddress] = useState<string | undefined>(
undefined,
);

const setupTurbo = async (turbo: TurboAuthenticatedClient) => {
setWalletAddress(await turbo.signer.getNativeAddress());
setTurbo(turbo);
};

const disconnectWallet = () => {
setTurbo(undefined);
setCurrentToken(undefined);
setWalletAddress(undefined);
};

const connectEthWallet = async () => {
try {
// Check if MetaMask is installed

if (window.ethereum) {
// Find the MetaMask provider in the list
const metaMaskProvider = window.ethereum.providers?.find(
(provider) => provider.isMetaMask,
);

const provider = new ethers.BrowserProvider(
metaMaskProvider ?? window.ethereum,
); // Use the MetaMask provider
const signer = await provider.getSigner();

await setupTurbo(
TurboFactory.authenticated({
token: "ethereum",
walletAdapter: {
getSigner: () => signer,
},
}),
);

setCurrentToken("ethereum");
}
} catch (err) {
console.error(err);
}
};

const connectSolWallet = async () => {
try {
// Check if Phantom is installed
if (window.solana) {
const provider = window.solana;

const publicKey = new PublicKey((await provider.connect()).publicKey);

const wallet: SolanaWalletAdapter = {
publicKey,
signMessage: async (message: Uint8Array) => {
// Call Phantom's signMessage method
const { signature } = await provider.signMessage(message);

return signature;
},
};

await setupTurbo(
TurboFactory.authenticated({
token: "solana",
walletAdapter: wallet,
}),
);
setCurrentToken("solana");
}
} catch (err) {
console.error(err);
}
};

const connectArWallet = async () => {
try {
await getArconnect();
const arconnect_signer = new ArconnectSigner(window.arweaveWallet);

await setupTurbo(
TurboFactory.authenticated({
token: "arweave",
signer: arconnect_signer,
...turboConfig,
}),
);
setCurrentToken("arweave");
} catch (err) {
console.error(err);
}
};
return (
<div className="form wallet-connector">
<h3>{currentToken ? "Current" : "Connect"} Wallet</h3>
{walletAddress && currentToken ? (
<p className="wallet-info">
<span className="wallet-type">{currentToken}</span>{" "}
<span className="wallet-address">{walletAddress}</span>
<button onClick={disconnectWallet}>Disconnect</button>
</p>
) : (
<div className="wallet-connector-buttons">
<button onClick={connectEthWallet}>Connect Ethereum Wallet</button>
<button onClick={connectSolWallet}>Connect Solana Wallet</button>
<button onClick={connectArWallet}>Connect Arweave Wallet</button>
</div>
)}
</div>
);
}
2 changes: 1 addition & 1 deletion src/hooks/useCreditsForFiat.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { TurboFactory, USD } from "@ardrive/turbo-sdk";
import { TurboFactory, USD } from "@ardrive/turbo-sdk/web";
import { useState, useRef, useEffect } from "react";
import { turboConfig, wincPerCredit } from "../constants";

32 changes: 32 additions & 0 deletions src/hooks/useCreditsForToken.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { TurboFactory } from "@ardrive/turbo-sdk/web";
import { useState, useRef, useEffect } from "react";
import { turboConfig, wincPerCredit } from "../constants";

export function useCreditsForToken(
debouncedTokenAmount: string,
errorCallback: (message: string) => void,
): [string | undefined, string | undefined] {
const [winc, setWinc] = useState<string | undefined>(undefined);
const usdWhenCreditsWereLastUpdatedRef = useRef<string | undefined>(
undefined,
);

// Get credits for USD amount when USD amount has stopped debouncing
useEffect(() => {
TurboFactory.unauthenticated(turboConfig)
.getWincForToken({ tokenAmount: debouncedTokenAmount })
.then(({ winc }) => {
usdWhenCreditsWereLastUpdatedRef.current = debouncedTokenAmount;
setWinc(winc);
})
.catch((err) => {
console.error(err);
errorCallback(`Error getting credits for USD amount: ${err.message}`);
});
}, [debouncedTokenAmount, errorCallback]);

return [
winc ? (+winc / wincPerCredit).toString() : undefined,
usdWhenCreditsWereLastUpdatedRef.current,
];
}
2 changes: 1 addition & 1 deletion src/hooks/useWincForOneGiB.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { TurboFactory } from "@ardrive/turbo-sdk";
import { TurboFactory } from "@ardrive/turbo-sdk/web";
import { useState, useEffect } from "react";
import { turboConfig } from "../constants";

3 changes: 2 additions & 1 deletion src/index.css
Original file line number Diff line number Diff line change
@@ -70,7 +70,8 @@ input[type="number"] {
color: var(--ardrive-red);
}

h1 {
h1,
h3 {
font-family: Wavehaus-Extra;
}

2 changes: 1 addition & 1 deletion src/pages/CryptoTopUpPage.tsx
Original file line number Diff line number Diff line change
@@ -13,7 +13,7 @@ import {
TurboFactory,
ARToTokenAmount,
ArconnectSigner,
} from "@ardrive/turbo-sdk";
} from "@ardrive/turbo-sdk/web";
import { getArconnect } from "../utils/arconnect";
import { NewToArDriveInfo } from "../components/NewToArDriveInfo";
import { useWincForOneGiB } from "../hooks/useWincForOneGiB";
2 changes: 1 addition & 1 deletion src/pages/FiatTopUpPage.tsx
Original file line number Diff line number Diff line change
@@ -9,7 +9,7 @@ import {
} from "../constants";
import { Page } from "./Page";
import { useLocation } from "react-router-dom";
import { TurboFactory, USD } from "@ardrive/turbo-sdk";
import { TurboFactory, USD } from "@ardrive/turbo-sdk/web";
import { NewToArDriveInfo } from "../components/NewToArDriveInfo";
import { getArconnect } from "../utils/arconnect";
import { useCreditsForFiat } from "../hooks/useCreditsForFiat";
108 changes: 108 additions & 0 deletions src/pages/UploadPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import { useState } from "react";
import { ErrMsgCallbackAsProps, ExtendedFileInputProps } from "../types";
import { ardriveAppUrl } from "../constants";
import { Page } from "./Page";
import { TurboAuthenticatedClient } from "@ardrive/turbo-sdk/web";
import { NewToArDriveInfo } from "../components/NewToArDriveInfo";
import TurboWalletConnector from "../components/TurboWalletConnector";

function UploadForm({ errorCallback }: ErrMsgCallbackAsProps) {
const [selectedFiles, setSelectedFiles] = useState<FileList | null>(null);

const [turbo, setTurbo] = useState<undefined | TurboAuthenticatedClient>(
undefined,
);
const [sending, setSending] = useState<boolean>(false);

// TODO: Query params if needed
// const location = useLocation();
// useEffect(() => {

// }, [location.search]);

const canSubmitForm = !!turbo && !!selectedFiles && !sending;

const handleSubmit = (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
e.preventDefault();

if (!canSubmitForm) {
return;
}

console.log("turbo", turbo);
setSending(true);
turbo
.uploadFolder({
files: Array.from(selectedFiles),
maxConcurrentUploads: 1,
})
.catch((err: unknown) => {
console.error("err", err);
errorCallback(
`Error uploading: ${err instanceof Error ? err.message : err}`,
);
// throw err;
})
.then((res: unknown) => {
console.log("res", res);
setSending(false);

// TODO: Success Modal or Page
alert("Great Upload!!\n" + JSON.stringify(res));
});
};

return (
<>
<h1>Upload a folder or file with your wallet</h1>
<p>
If you do not have an Arweave wallet, you can create one in{" "}
{/* TODO: Create wallet from turbo sdk/app */}
<a href={ardriveAppUrl}>ArDrive App</a>.
</p>

<TurboWalletConnector setTurbo={setTurbo} />

<form className="form">
<div className="form-section">
{/* TODO: Current balance of wallet in AR and Turbo Credits */}
{/* TODO: Inputs for manifest options, concurrent uploads, etc. */}

<label className="form-label">Upload Folder</label>
<input
type="file"
webkitdirectory="true"
onChange={(e) => setSelectedFiles(e.target.files)}
{...({} as ExtendedFileInputProps)} // This line is a workaround for declaring webkitdirectory in TypeScript
/>
<br />
<label className="form-label">Upload Files</label>
<input
// webkitdirectory
type="file"
multiple
onChange={(e) => setSelectedFiles(e.target.files)}
/>
</div>

{sending && <p>Now Uploading...</p>}

{/* TODO: Estimate price */}
<button
type="submit"
className="proceed-button"
onClick={(e) => handleSubmit(e)}
disabled={!canSubmitForm}
>
Send Upload
</button>
</form>

<NewToArDriveInfo />
</>
);
}

export const UploadPage = () => (
<Page page={(e) => <UploadForm errorCallback={e} />} />
);
27 changes: 27 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,31 @@
import { PublicKey } from "@solana/web3.js";
import { Eip1193Provider } from "ethers";
import { InputHTMLAttributes } from "react";

export type ErrMsgCallback = (error: string) => void;
export type ErrMsgCallbackAsProps = {
errorCallback: ErrMsgCallback;
};

declare global {
interface Window {
ethereum: {
isMetaMask?: boolean;
providers?: Array<Eip1193Provider & { isMetaMask: boolean }>;
request: (args: {
method: string;
params?: Array<unknown>;
}) => Promise<unknown>;
on: (eventName: string, callback: (...args: unknown[]) => void) => void;
};
solana: {
connect: () => Promise<{ publicKey: PublicKey }>;
signMessage: (message: Uint8Array) => Promise<{ signature: Uint8Array }>;
};
}
}

export interface ExtendedFileInputProps
extends InputHTMLAttributes<HTMLInputElement> {
webkitdirectory?: boolean;
}
1,685 changes: 1,323 additions & 362 deletions yarn.lock

Large diffs are not rendered by default.

0 comments on commit f54a1e0

Please sign in to comment.