feat: initial Pezkuwi Apps rebrand from polkadot-apps

Rebranded terminology:
- Polkadot → Pezkuwi
- Kusama → Dicle
- Westend → Zagros
- Rococo → PezkuwiChain
- Substrate → Bizinikiwi
- parachain → teyrchain

Custom logos with Kurdistan brand colors (#e6007a → #86e62a):
- bizinikiwi-hexagon.svg
- sora-bizinikiwi.svg
- hezscanner.svg
- heztreasury.svg
- pezkuwiscan.svg
- pezkuwistats.svg
- pezkuwiassembly.svg
- pezkuwiholic.svg
This commit is contained in:
2026-01-07 13:05:27 +03:00
commit d21bfb1320
5867 changed files with 329019 additions and 0 deletions
View File
View File
+26
View File
@@ -0,0 +1,26 @@
# @pezkuwi/app-files
## Description
***Bizinikiwi Files*** is a decentralized storage module which allows bizinikiwi-based chains(including `Pezkuwi/Dicle/Crust/Acala/Clover/Moonbeam/Astar/Phala/...`) users upload their files to [IPFS W3Auth Gateway](https://wiki.crust.network/docs/en/buildIPFSWeb3AuthGW) and decentralized pin their files on [Crust Network](https://crust.network) by using the standard [IPFS W3Auth Pinning Service](https://wiki.crust.network/docs/en/buildIPFSW3AuthPin). This module is a 100% IPFS compatible file storage module, users can pin **NFT files**, host **DApps** or store **on-chain data** in totally DECENTRALIZED way(`guaranteed by Crust protocol`).
Also, the Pinning Service is compatible with several Platforms like `Ethereum`, `Polygon`, `Solana` and `Near`, and funded by [Decentralized Cloud Foundation](https://decloudf.com/). So currently, `Bizinikiwi Files` is FREE for all the bizinikiwi-based chains!
## Features
Basically, the function is simple, it consist with 2 major parts: `Upload File` and `Manage File Directory`.
### Upload File
Upload files with bizinikiwi-based chains account, you can also copy the cid to [Crust Apps](https://apps.crust.network/?rpc=wss%3A%2F%2Frpc.crust.network#/storage) to see how many IPFS replicas of your file.
### Manage files directory
Because the whole files module is decentralized, so your file directory is cached in browser, but you can export and import your file directory info.
## References
- [Crust Network](https://crust.network)
- [IPFS Web3 Authentication Gateway](https://wiki.crust.network/docs/en/buildIPFSWeb3AuthGW)
- [IPFS Web3 Authentication Pinning Service](https://wiki.crust.network/docs/en/buildIPFSW3AuthPin)
- [IPFS Public Gateway](https://docs.ipfs.io/concepts/ipfs-gateway/)
- [IPFS Pinning Service](https://docs.ipfs.io/how-to/work-with-pinning-services/#use-an-existing-pinning-service)
+27
View File
@@ -0,0 +1,27 @@
{
"bugs": "https://github.com/pezkuwichain/pezkuwi-apps/issues",
"engines": {
"node": ">=18"
},
"homepage": "https://github.com/pezkuwichain/pezkuwi-apps/tree/master/packages/page-files#readme",
"license": "Apache-2.0",
"name": "@pezkuwi/app-files",
"private": true,
"repository": {
"directory": "packages/page-files",
"type": "git",
"url": "https://github.com/pezkuwichain/pezkuwi-apps.git"
},
"sideEffects": false,
"type": "module",
"version": "0.168.2-4-x",
"dependencies": {
"@pezkuwi/react-components": "^0.168.2-4-x",
"axios": "^1.6.2"
},
"peerDependencies": {
"react": "*",
"react-dom": "*",
"react-is": "*"
}
}
+403
View File
@@ -0,0 +1,403 @@
// Copyright 2017-2025 @pezkuwi/app-files authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { ActionStatusBase } from '@pezkuwi/react-components/Status/types';
import type { DirFile, FileInfo, SaveFile } from './types.js';
import FileSaver from 'file-saver';
import React, { useCallback, useRef, useState } from 'react';
import { Badge, Button, CopyButton, Icon, styled, Table } from '@pezkuwi/react-components';
import { useQueue } from '@pezkuwi/react-hooks';
import { useFiles } from './hooks.js';
import { useTranslation } from './translate.js';
import UploadModal from './UploadModal.js';
const MCopyButton = styled(CopyButton)`
.copySpan {
display: none;
}
`;
const ItemFile = styled.tr`
height: 3.5rem;
.end {
text-align: end;
}
.actions {
display: flex;
justify-content: flex-end;
align-items: center;
}
`;
const shortStr = (name: string, count = 6): string => {
if (name.length > (count * 2)) {
return `${name.substring(0, count)}...${name.substring(name.length - count)}`;
}
return name;
};
function createUrl (f: SaveFile) {
const endpoint = 'https://cf-ipfs.com';
return `${endpoint}/ipfs/${f.Hash}?filename=${f.Name}`;
}
const createOnDown = (f: SaveFile) => () => {
window.open(createUrl(f), '_blank');
// FileSaver.saveAs(createUrl(f), f.Name);
};
type FunInputFile = (e: React.ChangeEvent<HTMLInputElement>) => void
const Noop = (): void => undefined;
export interface Props {
className?: string,
}
function CrustFiles ({ className }: Props): React.ReactElement<Props> {
const { t } = useTranslation();
const { queueAction } = useQueue();
const [showUpMode, setShowUpMode] = useState(false);
const wFiles = useFiles();
const [file, setFile] = useState<FileInfo | undefined>(undefined);
const inputRef = useRef<HTMLInputElement>(null);
const _clickUploadFile = useCallback((dir = false) => {
if (!inputRef.current) {
return;
}
// eslint-disable-next-line
// @ts-ignore
// eslint-disable-next-line
inputRef.current.webkitdirectory = dir;
// eslint-disable-next-line
inputRef.current.multiple = dir;
inputRef.current.click();
}, [inputRef]);
const onClickUpFile = useCallback(() => _clickUploadFile(false), [_clickUploadFile]);
const onClickUpFolder = useCallback(() => _clickUploadFile(true), [_clickUploadFile]);
const _onInputFile = useCallback<FunInputFile>((e) => {
const files = e.target.files;
if (!files) {
return;
}
if (files.length > 2000) {
queueAction({
action: 'Upload Folder',
message: t('Please do not upload more than 2000 files'),
status: 'error'
});
return;
}
if (files.length === 0) {
queueAction({
action: 'Upload Folder',
message: t('Please select non-empty folder'),
status: 'error'
});
return;
}
// eslint-disable-next-line
// @ts-ignore
// eslint-disable-next-line
const isDirectory = e.target.webkitdirectory;
if (!isDirectory) {
setFile({ file: files[0] });
setShowUpMode(true);
} else if (files.length >= 1) {
// eslint-disable-next-line
// @ts-ignore eslint-disable-next-line
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
const dirFiles: DirFile[] = [];
for (let i = 0, count = files.length; i < count; i++) {
// console.info('f:', files[index]);
dirFiles.push(files[i]);
}
console.info(dirFiles);
const [dir] = dirFiles[0].webkitRelativePath.split('/');
setFile({ dir, files: dirFiles });
setShowUpMode(true);
}
e.target.value = '';
}, [setFile, setShowUpMode, queueAction, t]);
const _onImportResult = useCallback<(m: string, s?: ActionStatusBase['status']) => void>(
(message, status = 'queued') => {
queueAction && queueAction({
action: t('Import files'),
message,
status
});
},
[queueAction, t]
);
const importInputRef = useRef<HTMLInputElement>(null);
const _clickImport = useCallback(() => {
if (!importInputRef.current) {
return;
}
importInputRef.current.click();
}, [importInputRef]);
const _onInputImportFile = useCallback<FunInputFile>((e) => {
try {
_onImportResult(t('Importing'));
const fileReader = new FileReader();
const files = e.target.files;
if (!files) {
return;
}
fileReader.readAsText(files[0], 'UTF-8');
if (!(/(.json)$/i.test(e.target.value))) {
return _onImportResult(t('file error'), 'error');
}
fileReader.onload = (e) => {
const _list = JSON.parse(e.target?.result as string) as SaveFile[];
if (!Array.isArray(_list)) {
return _onImportResult(t('file content error'), 'error');
}
const fitter: SaveFile[] = [];
const mapImport: Record<string, boolean> = {};
for (const item of _list) {
if (item.Hash && item.Name && item.UpEndpoint && item.PinEndpoint) {
fitter.push(item);
mapImport[item.Hash] = true;
}
}
const filterOld = wFiles.files.filter((item) => !mapImport[item.Hash]);
wFiles.setFiles([...fitter, ...filterOld]);
_onImportResult(t('Import Success'), 'success');
};
} catch {
_onImportResult(t('file content error'), 'error');
}
}, [wFiles, _onImportResult, t]);
const _onClose = useCallback(() => {
setShowUpMode(false);
}, []);
const _onSuccess = useCallback((res: SaveFile) => {
setShowUpMode(false);
const filterFiles = wFiles.files.filter((f) => f.Hash !== res.Hash);
wFiles.setFiles([res, ...filterFiles]);
}, [wFiles]);
const _export = useCallback(() => {
const blob = new Blob([JSON.stringify(wFiles.files)], { type: 'application/json; charset=utf-8' });
// eslint-disable-next-line deprecation/deprecation
FileSaver.saveAs(blob, 'files.json');
}, [wFiles]);
return <StyledMain className={className}>
<header></header>
<input
onChange={_onInputFile}
ref={inputRef}
style={{ display: 'none' }}
type={'file'}
/>
<input
onChange={_onInputImportFile}
ref={importInputRef}
style={{ display: 'none' }}
type={'file'}
/>
{file && showUpMode && (
<UploadModal
file={file}
onClose={_onClose}
onSuccess={_onSuccess}
/>
)}
<div style={{ display: 'flex', paddingBottom: '1.5rem' }}>
<div className='uploadBtn'>
<Button
icon={'upload'}
label={t('Upload')}
onClick={Noop}
/>
<div className='uploadMenu'>
<div
className='menuItem'
onClick={onClickUpFile}
>{t('File')}</div>
<div
className='menuItem'
onClick={onClickUpFolder}
>{t('Folder')}</div>
</div>
</div>
<div style={{ flex: 1 }} />
<Button
icon={'file-import'}
label={t('Import')}
onClick={_clickImport}
/>
<Button
icon={'file-export'}
label={t('Export')}
onClick={_export}
/>
</div>
<Table
empty={t('No files')}
emptySpinner={t('Loading')}
header={[
[t('files'), 'start', 2],
[t('file cid'), 'expand', 2],
[undefined, 'start'],
[t('file size'), 'expand', 2],
[t('status'), 'expand'],
[t('action'), 'expand'],
[]
]}
>
{wFiles.files.map((f, index) =>
<ItemFile key={`files_item-${index}`}>
<td
className=''
colSpan={2}
>
{f.items && (
<Icon
className='highlight--color'
icon='folder'
/>
)}
{shortStr(f.Name)}</td>
<td
className='end'
colSpan={2}
>{f.Hash}</td>
<td
className=''
colSpan={1}
>
<MCopyButton value={f.Hash}>
<Badge
color='highlight'
hover={t('Copy file cid')}
icon='copy'
/>
</MCopyButton>
</td>
<td
className='end'
colSpan={2}
>{`${f.Size} bytes`}</td>
<td
className='end'
colSpan={1}
>
<a
href={'https://apps.crust.network/?rpc=wss%3A%2F%2Frpc.crust.network#/storage_files/status/' + f.Hash}
rel='noreferrer'
target='_blank'
>{t('View status in Crust')}</a>
</td>
<td
className='end'
colSpan={1}
>
<div className='actions'>
{!f.items && (
<Badge
color='highlight'
hover={t('Download')}
icon='download'
onClick={createOnDown(f)}
/>
)}
<MCopyButton value={createUrl(f)}>
<Badge
color='highlight'
hover={t('Copy link')}
icon='copy'
/>
</MCopyButton>
</div>
</td>
<td colSpan={1} />
</ItemFile>
)}
</Table>
<div>
{t('Note: The file list is cached locally, switching browsers or devices will not keep displaying the original browser information.')}
</div>
</StyledMain>;
}
const StyledMain = styled.main`
h1 {
text-transform: unset !important;
}
.uploadBtn {
position: relative;
padding: 5px 0;
&:hover {
.uploadMenu {
display: block;
}
}
}
.uploadMenu {
z-index: 200;
display: none;
background-color: var(--bg-table);
position: absolute;
top: 43px;
left: 0;
box-shadow: 0 0 8px rgba(0, 0, 0, 0.4);
border-radius: 4px;
overflow: hidden;
line-height: 40px;
.menuItem {
cursor: pointer;
padding: 0 2rem;
display: flex;
align-items: center;
white-space: nowrap;
&:hover {
background-color: var(--bg-page);
}
}
}
`;
export default React.memo(CrustFiles);
+46
View File
@@ -0,0 +1,46 @@
// Copyright 2017-2025 @pezkuwi/app-files authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { CSSProperties } from 'react';
import React from 'react';
import { styled } from '@pezkuwi/react-components';
export interface Props {
className?: string,
style?: CSSProperties,
progress: number
}
function Progress ({ className = '', progress, style }: Props) {
return (
<StyledDiv
className={`${className} highlight--border`}
style={style}
>
<div
className='file-progress-bar highlight--bg'
style={{ width: `${progress}%` }}
/>
</StyledDiv>
);
}
const StyledDiv = styled.div`
width: 100%;
background: unset;
overflow: hidden;
height: 1.4rem;
border-radius: 0.7rem;
border-style: solid;
border-width: 1px;
.file-progress-bar {
transition: width 100ms ease-in-out;
width: 0;
height: 100%;
}
`;
export default React.memo<Props>(Progress);
+401
View File
@@ -0,0 +1,401 @@
// Copyright 2017-2025 @pezkuwi/app-files authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { CancelTokenSource } from 'axios';
import type { Signer } from '@pezkuwi/api/types';
import type { AuthIpfsEndpoint, DirFile, FileInfo, SaveFile, UploadRes } from './types.js';
import axios from 'axios';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { web3FromSource } from '@pezkuwi/extension-dapp';
import { Available, Button, Dropdown, InputAddress, Label, MarkError, Modal, Password, styled } from '@pezkuwi/react-components';
import { keyring } from '@pezkuwi/ui-keyring';
import { isFunction, nextTick, stringToHex, stringToU8a, u8aToHex } from '@pezkuwi/util';
import Progress from './Progress.js';
import { useTranslation } from './translate.js';
export interface Props {
className?: string;
file: FileInfo,
onClose?: () => void,
onSuccess?: (res: SaveFile) => void,
}
interface AccountState {
isExternal: boolean;
isHardware: boolean;
isInjected: boolean;
}
interface SignerState {
isUsable: boolean;
signer: Signer | null;
}
const NOOP = (): void => undefined;
function ShowFile (p: { file: DirFile | File }) {
const f = p.file as DirFile;
return (
<div className='file'>
<Label label={f.webkitRelativePath || p.file.name} />
<span>{`${f.size} bytes`}</span>
</div>
);
}
function createAuthIpfsEndpoints (t: (key: string, options?: { replace: Record<string, unknown> }) => string): AuthIpfsEndpoint[] {
return [
{
location: t('Singapore'),
text: t('DCF'),
value: 'https://crustipfs.xyz'
},
{
location: t('Seattle'),
text: t('Crust Network'),
value: 'https://gw.crustfiles.app'
},
{
location: t('Berlin'),
text: t('⚡️ Thunder Gateway'),
value: 'https://gw.crustfiles.net'
}
];
}
function UploadModal ({ className, file, onClose = NOOP, onSuccess = NOOP }: Props): React.ReactElement<Props> {
const { t } = useTranslation();
const endpoints = useMemo(
() => createAuthIpfsEndpoints(t)
.sort(() => Math.random() > 0.5 ? -1 : 1)
.map((item) => ({ ...item, text: `${item.text ?? ''}(${item.location ?? ''})` })),
[t]
);
const [currentEndpoint, setCurrentEndpoint] = useState(endpoints[0]);
const pinEndpoints = useMemo(() => [
{
text: t('Crust Pinner'),
value: 'https://pin.crustcode.com'
}
], [t]);
const [currentPinEndpoint, setCurrentPinEndpoint] = useState(pinEndpoints[0]);
const [currentPair, setCurrentPair] = useState(() => keyring.getPairs()[0] || null);
const [account, setAccount] = useState('');
const [{ isInjected }, setAccountState] = useState<AccountState>({
isExternal: false,
isHardware: false,
isInjected: false
});
const [isLocked, setIsLocked] = useState(false);
const [{ isUsable, signer }, setSigner] = useState<SignerState>({ isUsable: true, signer: null });
const [password, setPassword] = useState('');
const [isBusy, setBusy] = useState(false);
const fileSizeError = useMemo(() => {
const MAX = 100 * 1024 * 1024;
if (file.file) {
return file.file.size > MAX;
} else if (file.files) {
let sum = 0;
for (const f of file.files) {
sum += f.size;
}
return sum > MAX;
}
return false;
}, [file]);
const [error, setError] = useState('');
const errorText = fileSizeError ? t('Do not upload files larger than 100MB!') : error;
const [upState, setUpState] = useState({ progress: 0, up: false });
const [cancelUp, setCancelUp] = useState<CancelTokenSource | null>(null);
const onAccountChange = useCallback((nAccount: string | null) => {
if (nAccount) {
setAccount(nAccount);
setCurrentPair(keyring.getPair(nAccount));
}
}, [setAccount, setCurrentPair]);
useEffect(() => {
const meta = currentPair?.meta || {};
const isExternal = meta.isExternal || false;
const isHardware = meta.isHardware || false;
const isInjected = meta.isInjected || false;
const isUsable = !(isExternal || isHardware || isInjected);
setAccountState({ isExternal, isHardware, isInjected });
setIsLocked(
isInjected
? false
: (currentPair && currentPair.isLocked) || false
);
setSigner({ isUsable, signer: null });
// for injected, retrieve the signer
if (meta.source && isInjected) {
web3FromSource(meta.source)
.catch(() => null)
.then((injected) => setSigner({
isUsable: isFunction(injected?.signer?.signRaw),
signer: injected?.signer || null
}))
.catch(console.error);
}
}, [currentPair]);
const unLock = useCallback(() => {
return new Promise((resolve, reject) => {
nextTick((): void => {
try {
currentPair.decodePkcs8(password);
resolve(1);
} catch (error) {
reject(error);
}
});
});
}, [currentPair, password]);
const _onClose = useCallback(() => {
if (cancelUp) {
cancelUp.cancel();
}
onClose();
}, [cancelUp, onClose]);
const _onClickUp = useCallback(async () => {
setError('');
if (!isUsable || !currentPair) {
return;
}
try {
// 1: sign
setBusy(true);
if (isLocked) {
await unLock();
}
let signature = '';
if (signer && isFunction(signer.signRaw)) {
const res = await signer.signRaw({
address: currentPair.address,
data: stringToHex(currentPair.address),
type: 'bytes'
});
signature = res.signature;
} else {
signature = u8aToHex(currentPair.sign(stringToU8a(currentPair.address)));
}
const perSignData = `${currentPair.address}:${signature}`;
const base64Signature = Buffer.from(perSignData).toString('base64');
const AuthBasic = `Basic ${base64Signature}`;
const AuthBearer = `Bearer ${base64Signature}`;
const cancel = axios.CancelToken.source();
setCancelUp(cancel);
setUpState({ progress: 0, up: true });
const form = new FormData();
if (file.file) {
form.append('file', file.file, file.file.name);
} else if (file.files) {
for (const f of file.files) {
form.append('file', f, f.webkitRelativePath);
}
}
const UpEndpoint = currentEndpoint.value;
const upResult = await axios.request<UploadRes | string>({
cancelToken: cancel.token,
data: form,
headers: { Authorization: AuthBasic },
maxContentLength: 100 * 1024 * 1024,
method: 'POST',
onUploadProgress: ({ loaded, total }) => {
const percent = loaded / (total || loaded || 1);
setUpState({ progress: Math.round(percent * 99), up: true });
},
params: { pin: true },
url: `${UpEndpoint}/api/v0/add`
});
let upRes: UploadRes;
if (typeof upResult.data === 'string') {
const jsonStr = upResult.data.replace(/}\n{/g, '},{');
const items = JSON.parse(`[${jsonStr}]`) as UploadRes[];
const folder = items.length - 1;
upRes = items[folder];
delete items[folder];
upRes.items = items;
} else {
upRes = upResult.data;
}
console.info('upResult:', upResult);
setCancelUp(null);
setUpState({ progress: 100, up: false });
// remote pin order
const PinEndpoint = currentPinEndpoint.value;
await axios.request({
data: {
cid: upRes.Hash,
name: upRes.Name
},
headers: { Authorization: AuthBearer },
method: 'POST',
url: `${PinEndpoint}/psa/pins`
});
onSuccess({
...upRes,
PinEndpoint,
UpEndpoint
});
} catch (e) {
setUpState({ progress: 0, up: false });
setBusy(false);
console.error(e);
setError((e as Error).message);
}
}, [file, unLock, signer, isLocked, isUsable, currentPair, currentPinEndpoint, currentEndpoint, onSuccess]);
const _onChangeGateway = useCallback((value: string) => {
const find = endpoints.find((item) => item.value === value);
if (find) {
setCurrentEndpoint(find);
}
}, [endpoints, setCurrentEndpoint]);
const _onChangePinner = useCallback((value: string) => {
const find = pinEndpoints.find((item) => item.value === value);
if (find) {
setCurrentPinEndpoint(find);
}
}, [pinEndpoints, setCurrentPinEndpoint]);
return (
<StyledModal
className={className}
header={t('Upload File')}
onClose={_onClose}
open={true}
size={'medium'}
>
<Modal.Content>
<Modal.Columns>
<div className='files'>
{file.file && <ShowFile file={file.file} />}
{file.files?.map((f, i) =>
<ShowFile
file={f}
key={`file_item:${i}`}
/>
)}
</div>
</Modal.Columns>
<Modal.Columns>
<Dropdown
isDisabled={isBusy}
label={t('Select a Web3 IPFS Gateway')}
onChange={_onChangeGateway}
options={endpoints}
value={currentEndpoint.value}
/>
</Modal.Columns>
<Modal.Columns>
<Dropdown
isDisabled={true}
label={t('Select a Web3 IPFS Pinner')}
onChange={_onChangePinner}
options={pinEndpoints}
value={currentPinEndpoint.value}
/>
</Modal.Columns>
<Modal.Columns>
<InputAddress
defaultValue={account}
isDisabled={isBusy}
label={t('Please choose account')}
labelExtra={
<Available
label={t('transferable')}
params={account}
/>
}
onChange={onAccountChange}
type='account'
/>
{
!upState.up && isLocked && !isInjected &&
<Password
isError={false}
label={t('password')}
onChange={setPassword}
value={password}
/>
}
<Progress
className='progress'
progress={upState.progress}
/>
{errorText && (
<MarkError content={errorText} />
)}
</Modal.Columns>
</Modal.Content>
<Modal.Actions>
<Button
icon={'arrow-circle-up'}
isBusy={isBusy}
isDisabled={fileSizeError}
label={t('Sign and Upload')}
onClick={_onClickUp}
/>
</Modal.Actions>
</StyledModal>
);
}
const StyledModal = styled(Modal)`
.files {
max-height: 300;
overflow: auto;
padding-left: 2rem;
width: 100%;
.file {
background-color: white;
border-bottom: 1px solid rgba(0, 0, 0, 0.15);
border-radius: 4px;
padding: 5px 2rem;
}
}
.progress {
margin-left: 2rem;
margin-top: 2rem;
width: calc(100% - 2rem);
}
`;
export default React.memo(UploadModal);
+46
View File
@@ -0,0 +1,46 @@
// Copyright 2017-2025 @pezkuwi/app-files authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { SaveFile } from './types.js';
import { useCallback, useEffect, useMemo, useState } from 'react';
import store from 'store';
export interface Files {
files: SaveFile[],
}
export interface WrapFiles extends Files {
isLoad: boolean,
setFiles: (files: SaveFile[]) => void
}
const defFilesObj = { files: [], isLoad: true };
export function useFiles (): WrapFiles {
const [filesObj, setFilesObj] = useState<Files>(defFilesObj);
const [isLoad, setIsLoad] = useState(true);
useEffect(() => {
try {
const f = store.get('files', defFilesObj) as Files;
if (f !== defFilesObj) {
setFilesObj(f);
}
setIsLoad(false);
} catch (e) {
setIsLoad(false);
console.error(e);
}
}, []);
const setFiles = useCallback((nFiles: SaveFile[]) => {
const nFilesObj = { ...filesObj, files: nFiles };
setFilesObj(nFilesObj);
store.set('files', nFilesObj);
}, [filesObj]);
return useMemo(() => ({ ...filesObj, isLoad, setFiles }), [filesObj, setFiles, isLoad]);
}
+40
View File
@@ -0,0 +1,40 @@
// Copyright 2017-2025 @pezkuwi/app-files authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { KeyedEvent } from '@pezkuwi/react-hooks/ctx/types';
import React, { useRef } from 'react';
import { Tabs } from '@pezkuwi/react-components';
import CrustFiles from './CrustFiles.js';
import { useTranslation } from './translate.js';
interface Props {
basePath: string;
className?: string;
newEvents?: KeyedEvent[];
}
function FilesApp ({ basePath, className }: Props): React.ReactElement<Props> {
const { t } = useTranslation();
const itemsRef = useRef([
{
isRoot: true,
name: 'crust',
text: t('Your Files')
}
]);
return (
<main className={className}>
<Tabs
basePath={basePath}
items={itemsRef.current}
/>
<CrustFiles />
</main>
);
}
export default React.memo(FilesApp);
+8
View File
@@ -0,0 +1,8 @@
// Copyright 2017-2025 @pezkuwi/app-files authors & contributors
// SPDX-License-Identifier: Apache-2.0
import { useTranslation as useTranslationBase } from 'react-i18next';
export function useTranslation (): { t: (key: string, options?: { replace: Record<string, unknown> }) => string } {
return useTranslationBase('app-files');
}
+30
View File
@@ -0,0 +1,30 @@
// Copyright 2017-2025 @pezkuwi/app-files authors & contributors
// SPDX-License-Identifier: Apache-2.0
export interface UploadRes {
Hash: string,
Size: string,
Name: string,
items?: UploadRes[],
}
export interface DirFile extends File {
webkitRelativePath: string,
}
export interface SaveFile extends UploadRes {
UpEndpoint: string,
PinEndpoint: string,
}
export interface FileInfo {
file?: File,
files?: DirFile[],
dir?: string,
}
export interface AuthIpfsEndpoint {
text?: string;
value: string;
location?: string;
}
+12
View File
@@ -0,0 +1,12 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"baseUrl": "..",
"outDir": "./build",
"rootDir": "./src"
},
"references": [
{ "path": "../react-components/tsconfig.build.json" },
{ "path": "../react-hooks/tsconfig.build.json" }
]
}