initial version v0.1
This commit is contained in:
63
README.md
Normal file
63
README.md
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
# React + TypeScript + Vite
|
||||||
|
|
||||||
|
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
|
||||||
|
|
||||||
|
Currently, two official plugins are available:
|
||||||
|
|
||||||
|
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
|
||||||
|
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
|
||||||
|
|
||||||
|
## Expanding the ESLint configuration
|
||||||
|
|
||||||
|
If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
|
||||||
|
|
||||||
|
```js
|
||||||
|
export default tseslint.config({
|
||||||
|
extends: [
|
||||||
|
// Remove ...tseslint.configs.recommended and replace with this
|
||||||
|
...tseslint.configs.recommendedTypeChecked,
|
||||||
|
// Alternatively, use this for stricter rules
|
||||||
|
...tseslint.configs.strictTypeChecked,
|
||||||
|
// Optionally, add this for stylistic rules
|
||||||
|
...tseslint.configs.stylisticTypeChecked,
|
||||||
|
],
|
||||||
|
languageOptions: {
|
||||||
|
// other options...
|
||||||
|
parserOptions: {
|
||||||
|
project: ['./tsconfig.node.json', './tsconfig.app.json'],
|
||||||
|
tsconfigRootDir: import.meta.dirname,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:
|
||||||
|
|
||||||
|
```js
|
||||||
|
// eslint.config.js
|
||||||
|
import reactX from 'eslint-plugin-react-x';
|
||||||
|
import reactDom from 'eslint-plugin-react-dom';
|
||||||
|
|
||||||
|
export default tseslint.config({
|
||||||
|
plugins: {
|
||||||
|
// Add the react-x and react-dom plugins
|
||||||
|
'react-x': reactX,
|
||||||
|
'react-dom': reactDom,
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
// other rules...
|
||||||
|
// Enable its recommended typescript rules
|
||||||
|
...reactX.configs['recommended-typescript'].rules,
|
||||||
|
...reactDom.configs.recommended.rules,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Internationalization of the app (I18N)
|
||||||
|
|
||||||
|
This template supports internationalization (i18n) using [i18next](https://www.i18next.com/), allowing seamless translation of UI text into multiple languages.
|
||||||
|
The setup includes modularized translation files (namespaces), language detection, context and runtime language switching.
|
||||||
|
|
||||||
|
Files with translation are in `src/i18n/locales/<locale>` folder.
|
||||||
|
|
||||||
|
`core` namespace is already present and active.
|
||||||
37
eslint.config.js
Normal file
37
eslint.config.js
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import js from '@eslint/js';
|
||||||
|
import globals from 'globals';
|
||||||
|
import reactHooks from 'eslint-plugin-react-hooks';
|
||||||
|
import reactRefresh from 'eslint-plugin-react-refresh';
|
||||||
|
import tseslint from 'typescript-eslint';
|
||||||
|
import prettierPlugin from 'eslint-plugin-prettier';
|
||||||
|
import prettierConfig from 'eslint-config-prettier';
|
||||||
|
|
||||||
|
export default tseslint.config(
|
||||||
|
{ ignores: ['dist'] },
|
||||||
|
{
|
||||||
|
extends: [js.configs.recommended, ...tseslint.configs.recommended, prettier],
|
||||||
|
files: ['**/*.{ts,tsx}'],
|
||||||
|
languageOptions: {
|
||||||
|
ecmaVersion: 2020,
|
||||||
|
globals: globals.browser,
|
||||||
|
},
|
||||||
|
plugins: {
|
||||||
|
'react-hooks': reactHooks,
|
||||||
|
'react-refresh': reactRefresh,
|
||||||
|
prettier: prettierPlugin,
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
...reactHooks.configs.recommended.rules,
|
||||||
|
'react-refresh/only-export-components': [
|
||||||
|
'warn',
|
||||||
|
{ allowConstantExport: true },
|
||||||
|
],
|
||||||
|
'prettier/prettier': 'error',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// This disables ESLint rules that would conflict with Prettier
|
||||||
|
name: 'prettier-config',
|
||||||
|
rules: prettierConfig.rules,
|
||||||
|
}
|
||||||
|
);
|
||||||
13
index.html
Normal file
13
index.html
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Qortal Q-App</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
4617
package-lock.json
generated
Normal file
4617
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
45
package.json
Normal file
45
package.json
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
{
|
||||||
|
"name": "q-follow",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsc -b && vite build",
|
||||||
|
"dev": "vite",
|
||||||
|
"format:check": "prettier --check .",
|
||||||
|
"format": "prettier --write .",
|
||||||
|
"initialize": "node scripts/initialize.js",
|
||||||
|
"lint": "eslint .",
|
||||||
|
"lint:fix": "eslint . --fix",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@emotion/react": "^11.14.0",
|
||||||
|
"@emotion/styled": "^11.14.0",
|
||||||
|
"@mui/icons-material": "^7.0.1",
|
||||||
|
"@mui/material": "^7.0.1",
|
||||||
|
"i18next": "^25.1.2",
|
||||||
|
"jotai": "^2.12.4",
|
||||||
|
"qapp-core": "^1.0.36",
|
||||||
|
"react": "^19.0.0",
|
||||||
|
"react-dom": "^19.0.0",
|
||||||
|
"react-i18next": "^15.5.1",
|
||||||
|
"react-router-dom": "^7.3.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@eslint/js": "^9.21.0",
|
||||||
|
"@types/react": "^19.0.10",
|
||||||
|
"@types/react-dom": "^19.0.4",
|
||||||
|
"@vitejs/plugin-react": "^4.3.4",
|
||||||
|
"eslint": "^9.21.0",
|
||||||
|
"eslint-config-prettier": "^10.1.5",
|
||||||
|
"eslint-plugin-prettier": "^5.4.0",
|
||||||
|
"eslint-plugin-react-hooks": "^5.1.0",
|
||||||
|
"eslint-plugin-react-refresh": "^0.4.19",
|
||||||
|
"globals": "^15.15.0",
|
||||||
|
"prettier": "^3.5.3",
|
||||||
|
"typescript": "~5.7.2",
|
||||||
|
"typescript-eslint": "^8.24.1",
|
||||||
|
"vite": "^6.3.5"
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
public/favicon.ico
Normal file
BIN
public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
30
scripts/initialize.js
Normal file
30
scripts/initialize.js
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import { writeFile, access } from 'fs/promises';
|
||||||
|
import { constants } from 'fs';
|
||||||
|
import { randomBytes } from 'crypto';
|
||||||
|
import { join, dirname } from 'path';
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
|
||||||
|
// Resolve __dirname in ES Modules
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = dirname(__filename);
|
||||||
|
|
||||||
|
// Define the file path (adjusted for new location in scripts/)
|
||||||
|
const filePath = join(__dirname, '..', 'src', 'qapp-config.ts');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Check if file already exists
|
||||||
|
await access(filePath, constants.F_OK);
|
||||||
|
console.log('⚠️ qapp-config.ts already exists. Skipping creation.');
|
||||||
|
} catch {
|
||||||
|
// File does not exist, proceed to create it
|
||||||
|
const publicSalt = randomBytes(32).toString('base64');
|
||||||
|
const tsContent = `export const publicSalt = "${publicSalt}";\n`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await writeFile(filePath, tsContent, 'utf8');
|
||||||
|
console.log('✅ qapp-config.ts has been created with a unique public salt.');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Error writing qapp-config.ts:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
28
src/App.tsx
Normal file
28
src/App.tsx
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import { Typography } from "@mui/material";
|
||||||
|
import { useAtom } from "jotai";
|
||||||
|
import { useGlobal } from "qapp-core";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { EnumTheme, themeAtom } from "./state/global/system";
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
const { auth } = useGlobal();
|
||||||
|
const { t } = useTranslation(["core"]);
|
||||||
|
|
||||||
|
// retrieve the theme 'light' or 'dark'
|
||||||
|
const [theme] = useAtom(themeAtom);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Typography>
|
||||||
|
{t("core:welcome", { postProcess: "capitalizeAll" })} {auth?.name}
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<Typography>
|
||||||
|
{t("core:using_theme", { postProcess: "capitalizeFirstChar" })}{" "}
|
||||||
|
{theme === EnumTheme.DARK ? "Dark" : "Light"}
|
||||||
|
</Typography>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App;
|
||||||
23
src/AppWrapper.tsx
Normal file
23
src/AppWrapper.tsx
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { GlobalProvider } from "qapp-core";
|
||||||
|
import Layout from "./styles/Layout";
|
||||||
|
import { publicSalt } from "./qapp-config";
|
||||||
|
|
||||||
|
export const AppWrapper = () => {
|
||||||
|
return (
|
||||||
|
<GlobalProvider
|
||||||
|
config={{
|
||||||
|
appName: "Q-Follow",
|
||||||
|
auth: {
|
||||||
|
balanceSetting: {
|
||||||
|
interval: 180000,
|
||||||
|
onlyOnMount: false,
|
||||||
|
},
|
||||||
|
authenticateOnMount: true,
|
||||||
|
},
|
||||||
|
publicSalt: publicSalt,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Layout />
|
||||||
|
</GlobalProvider>
|
||||||
|
);
|
||||||
|
};
|
||||||
35
src/components/BlockAddressInput.tsx
Normal file
35
src/components/BlockAddressInput.tsx
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import React, { useState } from "react";
|
||||||
|
import { addListItem } from "../services/qortalAPI";
|
||||||
|
|
||||||
|
const BlockAddressInput: React.FC = () => {
|
||||||
|
const [address, setAddress] = useState("");
|
||||||
|
|
||||||
|
const handleBlock = async () => {
|
||||||
|
if (!address) return;
|
||||||
|
try {
|
||||||
|
await addListItem("blockedAddresses", address);
|
||||||
|
alert(`Address ${address} blocked successfully!`);
|
||||||
|
setAddress("");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to block address:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="block-input-container">
|
||||||
|
<h4>Block an ADDRESS Directly:</h4>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Paste address here"
|
||||||
|
value={address}
|
||||||
|
onChange={(e) => setAddress(e.target.value)}
|
||||||
|
className="block-input"
|
||||||
|
/>
|
||||||
|
<button onClick={handleBlock} className="block-button">
|
||||||
|
Block Address
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default BlockAddressInput;
|
||||||
61
src/components/SearchBar.tsx
Normal file
61
src/components/SearchBar.tsx
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
import React, { useState } from "react";
|
||||||
|
import { searchNames } from "../services/qortalAPI";
|
||||||
|
import UserActions from "./UserActions";
|
||||||
|
|
||||||
|
interface SearchBarProps {
|
||||||
|
followedNames: string[];
|
||||||
|
blockedNames: string[];
|
||||||
|
refreshLists: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SearchBar: React.FC<SearchBarProps> = ({
|
||||||
|
followedNames,
|
||||||
|
blockedNames,
|
||||||
|
refreshLists,
|
||||||
|
}) => {
|
||||||
|
const [query, setQuery] = useState("");
|
||||||
|
const [results, setResults] = useState<any[]>([]);
|
||||||
|
|
||||||
|
const handleSearch = async (value: string) => {
|
||||||
|
setQuery(value);
|
||||||
|
if (value.length < 2) {
|
||||||
|
setResults([]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const names = await searchNames(value);
|
||||||
|
setResults(names);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Search failed:", error);
|
||||||
|
setResults([]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="search-input-container">
|
||||||
|
<h4>Search for a NAME to Follow/Block:</h4>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Search names..."
|
||||||
|
value={query}
|
||||||
|
onChange={(e) => handleSearch(e.target.value)}
|
||||||
|
className="search-input"
|
||||||
|
/>
|
||||||
|
<ul className="universal-name-list">
|
||||||
|
{results.map((result) => (
|
||||||
|
<li key={result.name}>
|
||||||
|
{result.name}{" "}
|
||||||
|
<UserActions
|
||||||
|
name={result.name}
|
||||||
|
followedNames={followedNames}
|
||||||
|
blockedNames={blockedNames}
|
||||||
|
refreshLists={refreshLists}
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SearchBar;
|
||||||
101
src/components/UserActions.tsx
Normal file
101
src/components/UserActions.tsx
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
import React, { useState } from "react";
|
||||||
|
import { addListItem, removeListItem } from "../services/qortalAPI";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
name: string;
|
||||||
|
followedNames: string[];
|
||||||
|
blockedNames: string[];
|
||||||
|
refreshLists: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const UserActions: React.FC<Props> = ({
|
||||||
|
name,
|
||||||
|
followedNames,
|
||||||
|
blockedNames,
|
||||||
|
refreshLists,
|
||||||
|
}) => {
|
||||||
|
const [showConfirm, setShowConfirm] = useState(false);
|
||||||
|
|
||||||
|
const isFollowed = followedNames.includes(name);
|
||||||
|
const isBlocked = blockedNames.includes(name);
|
||||||
|
|
||||||
|
const follow = async () => {
|
||||||
|
await addListItem("followedNames", name);
|
||||||
|
refreshLists();
|
||||||
|
};
|
||||||
|
|
||||||
|
const unfollow = async () => {
|
||||||
|
await removeListItem("followedNames", name);
|
||||||
|
refreshLists();
|
||||||
|
};
|
||||||
|
|
||||||
|
const unblock = async () => {
|
||||||
|
await removeListItem("blockedNames", name);
|
||||||
|
refreshLists();
|
||||||
|
};
|
||||||
|
|
||||||
|
const blockName = () => setShowConfirm(true);
|
||||||
|
|
||||||
|
const confirmBlock = async (blockBoth: boolean) => {
|
||||||
|
try {
|
||||||
|
await addListItem("blockedNames", name);
|
||||||
|
if (blockBoth) {
|
||||||
|
const nameData = await qortalRequest({
|
||||||
|
action: "GET_NAME_DATA",
|
||||||
|
name,
|
||||||
|
});
|
||||||
|
if (nameData?.owner) {
|
||||||
|
await addListItem("blockedAddresses", nameData.owner);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
refreshLists();
|
||||||
|
setShowConfirm(false);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to block:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{/* Priority: Blocked > Followed > Neutral */}
|
||||||
|
{isBlocked ? (
|
||||||
|
<button className="remove-btn" onClick={unblock}>
|
||||||
|
Unblock
|
||||||
|
</button>
|
||||||
|
) : isFollowed ? (
|
||||||
|
<button className="remove-btn" onClick={unfollow}>
|
||||||
|
Unfollow
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<button className="follow-btn" onClick={follow}>
|
||||||
|
Follow
|
||||||
|
</button>
|
||||||
|
<button className="block-btn" onClick={blockName}>
|
||||||
|
Block Name
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{showConfirm && (
|
||||||
|
<div className="block-confirm">
|
||||||
|
<p>
|
||||||
|
Blocking a name blocks data published by that name (QDN, shops,
|
||||||
|
etc.).
|
||||||
|
<br />
|
||||||
|
Blocking the address blocks ALL transactions from that address
|
||||||
|
(trades, chats, etc.).
|
||||||
|
</p>
|
||||||
|
<button className="remove-btn" onClick={() => confirmBlock(false)}>
|
||||||
|
Block Only Name
|
||||||
|
</button>
|
||||||
|
<button className="remove-btn" onClick={() => confirmBlock(true)}>
|
||||||
|
Block Name + Address
|
||||||
|
</button>
|
||||||
|
<button onClick={() => setShowConfirm(false)}>Cancel</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default UserActions;
|
||||||
85
src/components/UserList.tsx
Normal file
85
src/components/UserList.tsx
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { removeListItem } from "../services/qortalAPI";
|
||||||
|
import { useTheme } from "@mui/material";
|
||||||
|
|
||||||
|
interface UserListProps {
|
||||||
|
followedNames: string[];
|
||||||
|
blockedNames: string[];
|
||||||
|
blockedAddresses: string[];
|
||||||
|
refreshLists: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const UserList: React.FC<UserListProps> = ({
|
||||||
|
followedNames,
|
||||||
|
blockedNames,
|
||||||
|
blockedAddresses,
|
||||||
|
refreshLists,
|
||||||
|
}) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
document.body.dataset.theme = theme.palette.mode;
|
||||||
|
}, [theme.palette.mode]);
|
||||||
|
|
||||||
|
const remove = async (listName: string, item: string) => {
|
||||||
|
await removeListItem(listName as any, item);
|
||||||
|
refreshLists();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="list-grid">
|
||||||
|
<div className="list-section followed-section">
|
||||||
|
<h3>Followed Names</h3>
|
||||||
|
<ul className="universal-name-list">
|
||||||
|
{followedNames.map((name) => (
|
||||||
|
<li key={name}>
|
||||||
|
{name}{" "}
|
||||||
|
<button
|
||||||
|
className="remove-btn"
|
||||||
|
onClick={() => remove("followedNames", name)}
|
||||||
|
>
|
||||||
|
Unfollow
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="list-section blocked-names-section">
|
||||||
|
<h3>Blocked Names</h3>
|
||||||
|
<ul className="universal-name-list">
|
||||||
|
{blockedNames.map((name) => (
|
||||||
|
<li key={name}>
|
||||||
|
{name}{" "}
|
||||||
|
<button
|
||||||
|
className="remove-btn"
|
||||||
|
onClick={() => remove("blockedNames", name)}
|
||||||
|
>
|
||||||
|
Unblock
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="list-section blocked-addresses-section">
|
||||||
|
<h3>Blocked Addresses</h3>
|
||||||
|
<ul className="universal-name-list">
|
||||||
|
{blockedAddresses.map((address) => (
|
||||||
|
<li key={address}>
|
||||||
|
{address}{" "}
|
||||||
|
<button
|
||||||
|
className="remove-btn"
|
||||||
|
onClick={() => remove("blockedAddresses", address)}
|
||||||
|
>
|
||||||
|
Unblock
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default UserList;
|
||||||
75
src/hooks/useIframeListener.tsx
Normal file
75
src/hooks/useIframeListener.tsx
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
import { useEffect } from 'react';
|
||||||
|
import { To, useNavigate } from 'react-router-dom';
|
||||||
|
import { EnumTheme, themeAtom } from '../state/global/system';
|
||||||
|
import { useSetAtom } from 'jotai';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { supportedLanguages } from '../i18n/i18n';
|
||||||
|
|
||||||
|
type Language = 'de' | 'en' | 'es' | 'fr' | 'it' | 'ja' | 'ru' | 'zh';
|
||||||
|
type Theme = 'dark' | 'light';
|
||||||
|
|
||||||
|
interface CustomWindow extends Window {
|
||||||
|
_qdnTheme: Theme;
|
||||||
|
_qdnLang: Language;
|
||||||
|
}
|
||||||
|
const customWindow = window as unknown as CustomWindow;
|
||||||
|
|
||||||
|
export const useIframe = () => {
|
||||||
|
const setTheme = useSetAtom(themeAtom);
|
||||||
|
const { i18n } = useTranslation();
|
||||||
|
|
||||||
|
const navigate = useNavigate();
|
||||||
|
useEffect(() => {
|
||||||
|
const themeColorDefault = customWindow?._qdnTheme;
|
||||||
|
if (themeColorDefault === 'dark') {
|
||||||
|
setTheme(EnumTheme.DARK);
|
||||||
|
} else if (themeColorDefault === 'light') {
|
||||||
|
setTheme(EnumTheme.LIGHT);
|
||||||
|
}
|
||||||
|
|
||||||
|
const languageDefault = customWindow?._qdnLang;
|
||||||
|
|
||||||
|
if (supportedLanguages?.includes(languageDefault)) {
|
||||||
|
i18n.changeLanguage(languageDefault);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleNavigation(event: {
|
||||||
|
data: {
|
||||||
|
action: string;
|
||||||
|
path: To;
|
||||||
|
theme: Theme;
|
||||||
|
language: Language;
|
||||||
|
};
|
||||||
|
}) {
|
||||||
|
if (event.data?.action === 'NAVIGATE_TO_PATH' && event.data.path) {
|
||||||
|
navigate(event.data.path); // Navigate directly to the specified path
|
||||||
|
|
||||||
|
// Send a response back to the parent window after navigation is handled
|
||||||
|
window.parent.postMessage(
|
||||||
|
{ action: 'NAVIGATION_SUCCESS', path: event.data.path },
|
||||||
|
'*'
|
||||||
|
);
|
||||||
|
} else if (event.data?.action === 'THEME_CHANGED' && event.data.theme) {
|
||||||
|
const themeColor = event.data.theme;
|
||||||
|
if (themeColor === 'dark') {
|
||||||
|
setTheme(EnumTheme.DARK);
|
||||||
|
} else if (themeColor === 'light') {
|
||||||
|
setTheme(EnumTheme.LIGHT);
|
||||||
|
}
|
||||||
|
} else if (
|
||||||
|
event.data?.action === 'LANGUAGE_CHANGED' &&
|
||||||
|
event.data.language
|
||||||
|
) {
|
||||||
|
if (!supportedLanguages?.includes(event.data.language)) return;
|
||||||
|
i18n.changeLanguage(event.data.language);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener('message', handleNavigation);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('message', handleNavigation);
|
||||||
|
};
|
||||||
|
}, [navigate, setTheme]);
|
||||||
|
return { navigate };
|
||||||
|
};
|
||||||
56
src/i18n/i18n.ts
Normal file
56
src/i18n/i18n.ts
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import i18n from 'i18next';
|
||||||
|
import { initReactI18next } from 'react-i18next';
|
||||||
|
import {
|
||||||
|
capitalizeAll,
|
||||||
|
capitalizeFirstChar,
|
||||||
|
capitalizeFirstWord,
|
||||||
|
} from './processors';
|
||||||
|
|
||||||
|
// Load all locale JSON files
|
||||||
|
const modules = import.meta.glob('./locales/**/*.json', {
|
||||||
|
eager: true,
|
||||||
|
}) as Record<string, any>;
|
||||||
|
|
||||||
|
// Dynamically detect unique language codes
|
||||||
|
export const supportedLanguages: string[] = Array.from(
|
||||||
|
new Set(
|
||||||
|
Object.keys(modules)
|
||||||
|
.map((path) => {
|
||||||
|
const match = path.match(/\.\/locales\/([^/]+)\//);
|
||||||
|
return match ? match[1] : null;
|
||||||
|
})
|
||||||
|
.filter((lang): lang is string => typeof lang === 'string')
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Construct i18n resources object
|
||||||
|
const resources: Record<string, Record<string, any>> = {};
|
||||||
|
|
||||||
|
for (const path in modules) {
|
||||||
|
// Path format: './locales/en/core.json'
|
||||||
|
const match = path.match(/\.\/locales\/([^/]+)\/([^/]+)\.json$/);
|
||||||
|
if (!match) continue;
|
||||||
|
|
||||||
|
const [, lang, ns] = match;
|
||||||
|
resources[lang] = resources[lang] || {};
|
||||||
|
resources[lang][ns] = modules[path].default;
|
||||||
|
}
|
||||||
|
|
||||||
|
i18n
|
||||||
|
.use(initReactI18next)
|
||||||
|
.use(capitalizeAll as any)
|
||||||
|
.use(capitalizeFirstChar as any)
|
||||||
|
.use(capitalizeFirstWord as any)
|
||||||
|
.init({
|
||||||
|
resources,
|
||||||
|
fallbackLng: 'en',
|
||||||
|
lng: navigator.language,
|
||||||
|
supportedLngs: supportedLanguages,
|
||||||
|
ns: ['core'],
|
||||||
|
defaultNS: 'core',
|
||||||
|
interpolation: { escapeValue: false },
|
||||||
|
react: { useSuspense: false },
|
||||||
|
debug: import.meta.env.MODE === 'development',
|
||||||
|
});
|
||||||
|
|
||||||
|
export default i18n;
|
||||||
4
src/i18n/locales/en/core.json
Normal file
4
src/i18n/locales/en/core.json
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"using_theme": "this application is using the theme:",
|
||||||
|
"welcome": "welcome to Qortal"
|
||||||
|
}
|
||||||
54
src/i18n/processors.ts
Normal file
54
src/i18n/processors.ts
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
export const capitalizeAll = {
|
||||||
|
type: 'postProcessor',
|
||||||
|
name: 'capitalizeAll',
|
||||||
|
process: (value: string) => value.toUpperCase(),
|
||||||
|
};
|
||||||
|
|
||||||
|
export const capitalizeEachFirstChar = {
|
||||||
|
type: 'postProcessor',
|
||||||
|
name: 'capitalizeEachFirstChar',
|
||||||
|
process: (value: string) => {
|
||||||
|
if (!value?.trim()) return value;
|
||||||
|
|
||||||
|
const leadingSpaces = value.match(/^\s*/)?.[0] || '';
|
||||||
|
const trailingSpaces = value.match(/\s*$/)?.[0] || '';
|
||||||
|
|
||||||
|
const core = value
|
||||||
|
.trim()
|
||||||
|
.split(/\s+/)
|
||||||
|
.map(
|
||||||
|
(word) =>
|
||||||
|
word.charAt(0).toLocaleUpperCase() + word.slice(1).toLocaleLowerCase()
|
||||||
|
)
|
||||||
|
.join(' ');
|
||||||
|
|
||||||
|
return leadingSpaces + core + trailingSpaces;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const capitalizeFirstChar = {
|
||||||
|
type: 'postProcessor',
|
||||||
|
name: 'capitalizeFirstChar',
|
||||||
|
process: (value: string) => value.charAt(0).toUpperCase() + value.slice(1),
|
||||||
|
};
|
||||||
|
|
||||||
|
export const capitalizeFirstWord = {
|
||||||
|
type: 'postProcessor',
|
||||||
|
name: 'capitalizeFirstWord',
|
||||||
|
process: (value: string) => {
|
||||||
|
if (!value?.trim()) return value;
|
||||||
|
|
||||||
|
const trimmed = value.trimStart();
|
||||||
|
const firstSpaceIndex = trimmed.indexOf(' ');
|
||||||
|
|
||||||
|
if (firstSpaceIndex === -1) {
|
||||||
|
return trimmed.charAt(0).toUpperCase() + trimmed.slice(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const firstWord = trimmed.slice(0, firstSpaceIndex);
|
||||||
|
const restOfString = trimmed.slice(firstSpaceIndex);
|
||||||
|
const trailingSpaces = value.slice(trimmed.length);
|
||||||
|
|
||||||
|
return firstWord.toUpperCase() + restOfString + trailingSpaces;
|
||||||
|
},
|
||||||
|
};
|
||||||
395
src/index.css
Normal file
395
src/index.css
Normal file
@@ -0,0 +1,395 @@
|
|||||||
|
@font-face {
|
||||||
|
font-family: 'Inter';
|
||||||
|
src: url('./styles/fonts/Inter-SemiBold.ttf') format('truetype');
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Inter';
|
||||||
|
src: url('./styles/fonts/Inter-ExtraBold.ttf') format('truetype');
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Inter';
|
||||||
|
src: url('./styles/fonts/Inter-Bold.ttf') format('truetype');
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Inter';
|
||||||
|
src: url('./styles/fonts/Inter-Regular.ttf') format('truetype');
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root {
|
||||||
|
line-height: 1.2;
|
||||||
|
padding: 0px;
|
||||||
|
margin: 0px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
font-family: 'Inter';
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-section {
|
||||||
|
padding: 1rem;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: var(--background-color, #1c1c1c);
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-section h3 {
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-section ul {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-section li {
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-section button {
|
||||||
|
margin-left: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.block-confirm {
|
||||||
|
padding: 1rem;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
border: 1px solid #f39c12;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: #2c2c2c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.block-confirm p {
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.block-confirm button {
|
||||||
|
margin-right: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Orbitron';
|
||||||
|
src: url('/fonts/Orbitron-VariableFont_wght.ttf') format('truetype');
|
||||||
|
font-weight: normal;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.follow-input,
|
||||||
|
.block-input {
|
||||||
|
font-family: 'Orbitron', sans-serif;
|
||||||
|
padding: 1rem;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
width: 80%;
|
||||||
|
border: 2px solid;
|
||||||
|
border-radius: 8px;
|
||||||
|
outline: none;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.follow-input {
|
||||||
|
border-color: #00c853;
|
||||||
|
/* Green */
|
||||||
|
}
|
||||||
|
|
||||||
|
.block-input {
|
||||||
|
border-color: #d50000;
|
||||||
|
/* Red */
|
||||||
|
}
|
||||||
|
|
||||||
|
.follow-button,
|
||||||
|
.block-button {
|
||||||
|
font-family: 'Orbitron', sans-serif;
|
||||||
|
padding: 0.75rem 1.5rem;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.follow-btn {
|
||||||
|
border-color: #00c853;
|
||||||
|
color: #00c853;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.follow-btn:hover {
|
||||||
|
background-color: #00c853;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.block-btn {
|
||||||
|
border-color: #d50000;
|
||||||
|
color: #d50000;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.block-btn:hover {
|
||||||
|
background-color: #d50000;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-input-container {
|
||||||
|
text-align: center;
|
||||||
|
margin: 1rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-10%);
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-input::placeholder {
|
||||||
|
color: #00c853;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-10%);
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Universal List Styling for ALL Names */
|
||||||
|
.universal-name-list {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 1rem 0;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.universal-name-list li {
|
||||||
|
font-family: 'Orbitron', sans-serif;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.75rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
animation: fadeIn 0.4s ease both;
|
||||||
|
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.universal-name-list li:hover {
|
||||||
|
transform: scale(1.03);
|
||||||
|
box-shadow: 0 0 12px rgba(0, 200, 83, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.universal-name-list li button {
|
||||||
|
padding: 0.4rem 0.75rem;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-input-container,
|
||||||
|
.block-input-container {
|
||||||
|
text-align: center;
|
||||||
|
margin: 1rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-input,
|
||||||
|
.block-input {
|
||||||
|
font-family: 'Orbitron', sans-serif;
|
||||||
|
padding: 1rem;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
width: 80%;
|
||||||
|
border: 2px solid;
|
||||||
|
border-radius: 8px;
|
||||||
|
outline: none;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
background: var(--input-bg, #ffffff);
|
||||||
|
color: var(--input-color, #000000);
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-input {
|
||||||
|
border-color: #00c853;
|
||||||
|
}
|
||||||
|
|
||||||
|
.block-input {
|
||||||
|
border-color: #d50000;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
font-family: 'Orbitron', sans-serif;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
border: 2px solid transparent;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: none;
|
||||||
|
font-size: 1rem;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.follow-btn {
|
||||||
|
border-color: #00c853;
|
||||||
|
color: #00c853;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.follow-btn:hover {
|
||||||
|
background-color: #00c853;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.block-btn {
|
||||||
|
border-color: #d50000;
|
||||||
|
color: #d50000;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.block-btn:hover {
|
||||||
|
background-color: #d50000;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.block-button {
|
||||||
|
font-family: 'Orbitron', sans-serif;
|
||||||
|
border: 2px solid #d50000;
|
||||||
|
color: #d50000;
|
||||||
|
background: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 0.75rem 1.5rem;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.block-button:hover {
|
||||||
|
background-color: #d50000;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-panel-wrapper {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1.5rem;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-section {
|
||||||
|
padding: 1rem;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: var(--section-bg, #f5f5f5);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Light/Dark Theme Adaptive */
|
||||||
|
|
||||||
|
|
||||||
|
/* Welcome Header */
|
||||||
|
.welcome-header {
|
||||||
|
font-family: 'Orbitron', sans-serif;
|
||||||
|
font-size: 2rem;
|
||||||
|
text-align: center;
|
||||||
|
margin: 1.5rem 0;
|
||||||
|
color: #04aee7;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Section Headers */
|
||||||
|
.list-section h3 {
|
||||||
|
font-family: 'Orbitron', sans-serif;
|
||||||
|
font-size: 1.4rem;
|
||||||
|
text-align: center;
|
||||||
|
color: #888;
|
||||||
|
/* Dark grey */
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.block-input-container h4{
|
||||||
|
font-family: 'Orbitron', sans-serif;
|
||||||
|
font-size: 1.4rem;
|
||||||
|
text-align: center;
|
||||||
|
color: #888;
|
||||||
|
/* Dark grey */
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-input-container h4 {
|
||||||
|
font-family: 'Orbitron', sans-serif;
|
||||||
|
font-size: 1.4rem;
|
||||||
|
text-align: center;
|
||||||
|
color: #888;
|
||||||
|
/* Dark grey */
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--input-bg: #ffffff;
|
||||||
|
--input-color: #000000;
|
||||||
|
--section-bg: #04080e;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Section Backgrounds */
|
||||||
|
.followed-section {
|
||||||
|
background: var(--section-bg);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blocked-names-section {
|
||||||
|
background: #271608;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blocked-addresses-section {
|
||||||
|
background: #110404;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/* List Item Buttons (Add/Remove) */
|
||||||
|
button.add-btn {
|
||||||
|
font-family: 'Orbitron', sans-serif;
|
||||||
|
border: 2px solid #00c853;
|
||||||
|
color: #00c853;
|
||||||
|
background: none;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.add-btn:hover {
|
||||||
|
background-color: #00c853;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.remove-btn {
|
||||||
|
font-family: 'Orbitron', sans-serif;
|
||||||
|
border: 2px solid #d50000;
|
||||||
|
color: #d50000;
|
||||||
|
background: none;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.remove-btn:hover {
|
||||||
|
background-color: #d50000;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
14
src/main.tsx
Normal file
14
src/main.tsx
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { StrictMode } from "react";
|
||||||
|
import { createRoot } from "react-dom/client";
|
||||||
|
import ThemeProviderWrapper from "./styles/theme/theme-provider.tsx";
|
||||||
|
import "./index.css";
|
||||||
|
import "./i18n/i18n.ts";
|
||||||
|
import { Routes } from "./routes/Routes.tsx";
|
||||||
|
|
||||||
|
createRoot(document.getElementById("root")!).render(
|
||||||
|
<StrictMode>
|
||||||
|
<ThemeProviderWrapper>
|
||||||
|
<Routes />
|
||||||
|
</ThemeProviderWrapper>
|
||||||
|
</StrictMode>
|
||||||
|
);
|
||||||
49
src/pages/FollowBlock.tsx
Normal file
49
src/pages/FollowBlock.tsx
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
import SearchBar from "../components/SearchBar";
|
||||||
|
import UserList from "../components/UserList";
|
||||||
|
import BlockAddressInput from "../components/BlockAddressInput";
|
||||||
|
import { getListItems } from "../services/qortalAPI";
|
||||||
|
|
||||||
|
const FollowBlock: React.FC = () => {
|
||||||
|
const [followed, setFollowed] = useState<string[]>([]);
|
||||||
|
const [blockedNames, setBlockedNames] = useState<string[]>([]);
|
||||||
|
const [blockedAddresses, setBlockedAddresses] = useState<string[]>([]);
|
||||||
|
|
||||||
|
const loadLists = async () => {
|
||||||
|
const followed = await getListItems("followedNames");
|
||||||
|
const blockedNames = await getListItems("blockedNames");
|
||||||
|
const blockedAddresses = await getListItems("blockedAddresses");
|
||||||
|
|
||||||
|
// Sort alphabetically
|
||||||
|
setFollowed([...followed].sort((a, b) => a.localeCompare(b)));
|
||||||
|
setBlockedNames([...blockedNames].sort((a, b) => a.localeCompare(b)));
|
||||||
|
setBlockedAddresses(
|
||||||
|
[...blockedAddresses].sort((a, b) => a.localeCompare(b))
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadLists();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="input-panel-wrapper">
|
||||||
|
<SearchBar
|
||||||
|
followedNames={followed}
|
||||||
|
blockedNames={blockedNames}
|
||||||
|
refreshLists={loadLists}
|
||||||
|
/>
|
||||||
|
<BlockAddressInput />
|
||||||
|
</div>
|
||||||
|
<UserList
|
||||||
|
followedNames={followed}
|
||||||
|
blockedNames={blockedNames}
|
||||||
|
blockedAddresses={blockedAddresses}
|
||||||
|
refreshLists={loadLists}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FollowBlock;
|
||||||
1
src/qapp-config.ts
Normal file
1
src/qapp-config.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export const publicSalt = "5MZU9rlee/S86/OQ/bzHmLIvdS9zOu/1U7QSatUy/7w=";
|
||||||
36
src/routes/Routes.tsx
Normal file
36
src/routes/Routes.tsx
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import { createBrowserRouter, RouterProvider } from "react-router-dom";
|
||||||
|
// import App from "../App";
|
||||||
|
import { AppWrapper } from "../AppWrapper";
|
||||||
|
import FollowBlock from "../pages/FollowBlock";
|
||||||
|
|
||||||
|
interface CustomWindow extends Window {
|
||||||
|
_qdnBase: string;
|
||||||
|
}
|
||||||
|
const customWindow = window as unknown as CustomWindow;
|
||||||
|
const baseUrl = customWindow?._qdnBase || "";
|
||||||
|
|
||||||
|
export function Routes() {
|
||||||
|
const router = createBrowserRouter(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
path: "/",
|
||||||
|
element: <AppWrapper />,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
index: true,
|
||||||
|
element: <FollowBlock />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "follow-block",
|
||||||
|
element: <FollowBlock />, // New page route
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
{
|
||||||
|
basename: baseUrl,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return <RouterProvider router={router} />;
|
||||||
|
}
|
||||||
39
src/services/qortalAPI.ts
Normal file
39
src/services/qortalAPI.ts
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
export async function searchNames(query: string, limit = 20) {
|
||||||
|
return await qortalRequest({
|
||||||
|
action: "SEARCH_NAMES",
|
||||||
|
query,
|
||||||
|
limit,
|
||||||
|
offset: 0,
|
||||||
|
reverse: false,
|
||||||
|
prefix: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getListItems(listName: "blockedNames" | "blockedAddresses" | "followedNames") {
|
||||||
|
return await qortalRequest({
|
||||||
|
action: "GET_LIST_ITEMS",
|
||||||
|
list_name: listName,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function addListItem(
|
||||||
|
listName: "blockedNames" | "blockedAddresses" | "followedNames",
|
||||||
|
name: string,
|
||||||
|
) {
|
||||||
|
return await qortalRequest({
|
||||||
|
action: "ADD_LIST_ITEMS",
|
||||||
|
list_name: listName,
|
||||||
|
items: [name],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function removeListItem(
|
||||||
|
listName: "blockedNames" | "blockedAddresses" | "followedNames",
|
||||||
|
name: string,
|
||||||
|
) {
|
||||||
|
return await qortalRequest({
|
||||||
|
action: "DELETE_LIST_ITEM",
|
||||||
|
list_name: listName,
|
||||||
|
items: [name],
|
||||||
|
});
|
||||||
|
}
|
||||||
9
src/state/global/system.ts
Normal file
9
src/state/global/system.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import { atom } from 'jotai';
|
||||||
|
|
||||||
|
export enum EnumTheme {
|
||||||
|
LIGHT = 1,
|
||||||
|
DARK = 2,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Atom to hold the current theme
|
||||||
|
export const themeAtom = atom<EnumTheme>(EnumTheme.DARK);
|
||||||
26
src/styles/Layout.tsx
Normal file
26
src/styles/Layout.tsx
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import { Outlet } from "react-router-dom";
|
||||||
|
import { useIframe } from "../hooks/useIframeListener";
|
||||||
|
import { useGlobal } from "qapp-core";
|
||||||
|
|
||||||
|
const Layout = () => {
|
||||||
|
const { auth } = useGlobal();
|
||||||
|
useIframe();
|
||||||
|
return (
|
||||||
|
<div className="app-layout">
|
||||||
|
{/* Your header/sidebar/etc */}
|
||||||
|
<header>
|
||||||
|
<h1 className="welcome-header">
|
||||||
|
Welcome to Q-Follow,{" "}
|
||||||
|
<span style={{ color: "red" }}>{auth?.name}</span>
|
||||||
|
</h1>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
{/* Main content */}
|
||||||
|
<main>
|
||||||
|
<Outlet /> {/* ← Nested routes will render here */}
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Layout;
|
||||||
135
src/styles/StyledComponents.ts
Normal file
135
src/styles/StyledComponents.ts
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
// Full MUI conversion for theme //todo
|
||||||
|
|
||||||
|
import { styled } from '@mui/material/styles';
|
||||||
|
import { Box, Button, Typography } from '@mui/material';
|
||||||
|
|
||||||
|
// Universal List Grid Wrapper
|
||||||
|
export const ListGrid = styled(Box)(({ theme }) => ({
|
||||||
|
display: 'grid',
|
||||||
|
gridTemplateColumns: 'repeat(auto-fit, minmax(250px, 1fr))',
|
||||||
|
gap: theme.spacing(2),
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Shared Section Wrapper
|
||||||
|
export const ListSection = styled(Box)(({ theme }) => ({
|
||||||
|
padding: theme.spacing(2),
|
||||||
|
border: `1px solid ${theme.palette.divider}`,
|
||||||
|
borderRadius: theme.shape.borderRadius,
|
||||||
|
backgroundColor: theme.palette.background.paper,
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Dynamic Section Backgrounds
|
||||||
|
export const FollowedSection = styled(ListSection)``;
|
||||||
|
export const BlockedNamesSection = styled(ListSection)(({ theme }) => ({
|
||||||
|
backgroundColor: theme.palette.mode === 'light' ? '#fef5f0' : '#271608',
|
||||||
|
}));
|
||||||
|
export const BlockedAddressesSection = styled(ListSection)(({ theme }) => ({
|
||||||
|
backgroundColor: theme.palette.mode === 'light' ? '#fdeeee' : '#110404',
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Universal List Items
|
||||||
|
export const UniversalListItem = styled('li')(({ theme }) => ({
|
||||||
|
fontFamily: 'Orbitron, sans-serif',
|
||||||
|
fontSize: '1.1rem',
|
||||||
|
marginBottom: theme.spacing(1),
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: theme.spacing(1),
|
||||||
|
flexWrap: 'wrap',
|
||||||
|
transition: 'transform 0.2s ease, box-shadow 0.2s ease',
|
||||||
|
'&:hover': {
|
||||||
|
transform: 'scale(1.03)',
|
||||||
|
boxShadow: `0 0 12px ${theme.palette.mode === 'dark' ? 'rgba(0, 200, 83, 0.4)' : 'rgba(63, 81, 181, 0.4)'}`,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Buttons
|
||||||
|
export const FollowButton = styled(Button)({
|
||||||
|
borderColor: '#00c853',
|
||||||
|
color: '#00c853',
|
||||||
|
backgroundColor: 'transparent',
|
||||||
|
'&:hover': {
|
||||||
|
backgroundColor: '#00c853',
|
||||||
|
color: '#fff',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const BlockButton = styled(Button)({
|
||||||
|
borderColor: '#d50000',
|
||||||
|
color: '#d50000',
|
||||||
|
backgroundColor: 'transparent',
|
||||||
|
'&:hover': {
|
||||||
|
backgroundColor: '#d50000',
|
||||||
|
color: '#fff',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const AddButton = styled(Button)({
|
||||||
|
borderColor: '#00c853',
|
||||||
|
color: '#00c853',
|
||||||
|
backgroundColor: 'transparent',
|
||||||
|
'&:hover': {
|
||||||
|
backgroundColor: '#00c853',
|
||||||
|
color: '#fff',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const RemoveButton = styled(Button)({
|
||||||
|
borderColor: '#d50000',
|
||||||
|
color: '#d50000',
|
||||||
|
backgroundColor: 'transparent',
|
||||||
|
'&:hover': {
|
||||||
|
backgroundColor: '#d50000',
|
||||||
|
color: '#fff',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Typography Components
|
||||||
|
export const WelcomeHeader = styled(Typography)(({ theme }) => ({
|
||||||
|
fontFamily: 'Orbitron, sans-serif',
|
||||||
|
fontSize: '2rem',
|
||||||
|
textAlign: 'center',
|
||||||
|
margin: theme.spacing(3, 0),
|
||||||
|
color: '#00c853',
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const SectionHeader = styled(Typography)(({ theme }) => ({
|
||||||
|
fontFamily: 'Orbitron, sans-serif',
|
||||||
|
fontSize: '1.4rem',
|
||||||
|
textAlign: 'center',
|
||||||
|
color: '#888',
|
||||||
|
marginBottom: theme.spacing(1),
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Input Styles (search, block inputs)
|
||||||
|
export const InputField = styled('input')(({ theme, }) => ({
|
||||||
|
fontFamily: 'Orbitron, sans-serif',
|
||||||
|
padding: theme.spacing(1),
|
||||||
|
fontSize: '1.2rem',
|
||||||
|
width: '80%',
|
||||||
|
border: `2px solidrgb(15, 73, 34)`,
|
||||||
|
borderRadius: theme.shape.borderRadius,
|
||||||
|
outline: 'none',
|
||||||
|
marginBottom: theme.spacing(1),
|
||||||
|
backgroundColor: theme.palette.background.paper,
|
||||||
|
color: theme.palette.text.primary,
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Input Container Wrapper
|
||||||
|
export const InputPanelWrapper = styled(Box)(({ theme }) => ({
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: theme.spacing(2),
|
||||||
|
marginBottom: theme.spacing(4),
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Block Confirm Wrapper
|
||||||
|
export const BlockConfirm = styled(Box)(({ theme }) => ({
|
||||||
|
padding: theme.spacing(2),
|
||||||
|
marginTop: theme.spacing(1),
|
||||||
|
border: `1px solid #f39c12`,
|
||||||
|
borderRadius: theme.shape.borderRadius,
|
||||||
|
backgroundColor: theme.palette.mode === 'dark' ? '#2c2c2c' : '#fafafa',
|
||||||
|
}));
|
||||||
BIN
src/styles/fonts/Inter-Black.ttf
Normal file
BIN
src/styles/fonts/Inter-Black.ttf
Normal file
Binary file not shown.
BIN
src/styles/fonts/Inter-Bold.ttf
Normal file
BIN
src/styles/fonts/Inter-Bold.ttf
Normal file
Binary file not shown.
BIN
src/styles/fonts/Inter-ExtraBold.ttf
Normal file
BIN
src/styles/fonts/Inter-ExtraBold.ttf
Normal file
Binary file not shown.
BIN
src/styles/fonts/Inter-ExtraLight.ttf
Normal file
BIN
src/styles/fonts/Inter-ExtraLight.ttf
Normal file
Binary file not shown.
BIN
src/styles/fonts/Inter-Light.ttf
Normal file
BIN
src/styles/fonts/Inter-Light.ttf
Normal file
Binary file not shown.
BIN
src/styles/fonts/Inter-Medium.ttf
Normal file
BIN
src/styles/fonts/Inter-Medium.ttf
Normal file
Binary file not shown.
BIN
src/styles/fonts/Inter-Regular.ttf
Normal file
BIN
src/styles/fonts/Inter-Regular.ttf
Normal file
Binary file not shown.
BIN
src/styles/fonts/Inter-SemiBold.ttf
Normal file
BIN
src/styles/fonts/Inter-SemiBold.ttf
Normal file
Binary file not shown.
BIN
src/styles/fonts/Inter-Thin.ttf
Normal file
BIN
src/styles/fonts/Inter-Thin.ttf
Normal file
Binary file not shown.
BIN
src/styles/fonts/Orbitron-VariableFont_wght.ttf
Normal file
BIN
src/styles/fonts/Orbitron-VariableFont_wght.ttf
Normal file
Binary file not shown.
23
src/styles/theme/theme-provider.tsx
Normal file
23
src/styles/theme/theme-provider.tsx
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import React, { FC } from 'react';
|
||||||
|
import { ThemeProvider } from '@emotion/react';
|
||||||
|
import { lightTheme, darkTheme } from './theme';
|
||||||
|
import { CssBaseline } from '@mui/material';
|
||||||
|
import { EnumTheme, themeAtom } from '../../state/global/system';
|
||||||
|
import { useAtom } from 'jotai';
|
||||||
|
|
||||||
|
interface ThemeProviderWrapperProps {
|
||||||
|
children: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ThemeProviderWrapper: FC<ThemeProviderWrapperProps> = ({ children }) => {
|
||||||
|
const [theme] = useAtom(themeAtom);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ThemeProvider theme={theme === EnumTheme.LIGHT ? lightTheme : darkTheme}>
|
||||||
|
<CssBaseline />
|
||||||
|
{children}
|
||||||
|
</ThemeProvider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ThemeProviderWrapper;
|
||||||
120
src/styles/theme/theme.ts
Normal file
120
src/styles/theme/theme.ts
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
import { createTheme } from '@mui/material/styles';
|
||||||
|
|
||||||
|
const commonThemeOptions = {
|
||||||
|
typography: {
|
||||||
|
fontFamily: ['Inter'].join(','),
|
||||||
|
h1: {
|
||||||
|
fontSize: '2rem',
|
||||||
|
fontWeight: 600,
|
||||||
|
},
|
||||||
|
h2: {
|
||||||
|
fontSize: '1.75rem',
|
||||||
|
fontWeight: 500,
|
||||||
|
},
|
||||||
|
h3: {
|
||||||
|
fontSize: '1.5rem',
|
||||||
|
fontWeight: 500,
|
||||||
|
},
|
||||||
|
h4: {
|
||||||
|
fontSize: '1.25rem',
|
||||||
|
fontWeight: 500,
|
||||||
|
},
|
||||||
|
h5: {
|
||||||
|
fontSize: '1rem',
|
||||||
|
fontWeight: 500,
|
||||||
|
},
|
||||||
|
h6: {
|
||||||
|
fontSize: '0.875rem',
|
||||||
|
fontWeight: 500,
|
||||||
|
},
|
||||||
|
body1: {
|
||||||
|
fontSize: '1rem',
|
||||||
|
fontWeight: 400,
|
||||||
|
lineHeight: 1.5,
|
||||||
|
letterSpacing: '0.5px',
|
||||||
|
},
|
||||||
|
|
||||||
|
body2: {
|
||||||
|
fontSize: '0.875rem',
|
||||||
|
fontWeight: 400,
|
||||||
|
lineHeight: 1.4,
|
||||||
|
letterSpacing: '0.2px',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
spacing: 8,
|
||||||
|
shape: {
|
||||||
|
borderRadius: 4,
|
||||||
|
},
|
||||||
|
breakpoints: {
|
||||||
|
values: {
|
||||||
|
xs: 0,
|
||||||
|
sm: 600,
|
||||||
|
md: 900,
|
||||||
|
lg: 1200,
|
||||||
|
xl: 1536,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
MuiDialog: {
|
||||||
|
styleOverrides: {
|
||||||
|
paper: {
|
||||||
|
backgroundImage: 'none',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
MuiPopover: {
|
||||||
|
styleOverrides: {
|
||||||
|
paper: {
|
||||||
|
backgroundImage: 'none',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const lightTheme = createTheme({
|
||||||
|
...commonThemeOptions,
|
||||||
|
palette: {
|
||||||
|
mode: 'light',
|
||||||
|
primary: {
|
||||||
|
main: 'rgb(63, 81, 181)',
|
||||||
|
dark: 'rgb(113, 198, 212)',
|
||||||
|
light: 'rgb(180, 200, 235)',
|
||||||
|
},
|
||||||
|
secondary: {
|
||||||
|
main: 'rgba(194, 222, 236, 1)',
|
||||||
|
|
||||||
|
},
|
||||||
|
background: {
|
||||||
|
default: 'rgba(250, 250, 250, 1)',
|
||||||
|
paper: 'rgb(220, 220, 220)', // darker card background
|
||||||
|
},
|
||||||
|
text: {
|
||||||
|
primary: 'rgba(0, 0, 0, 0.87)', // 87% black (slightly softened)
|
||||||
|
secondary: 'rgba(0, 0, 0, 0.6)', // 60% black
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const darkTheme = createTheme({
|
||||||
|
...commonThemeOptions,
|
||||||
|
palette: {
|
||||||
|
mode: 'dark',
|
||||||
|
primary: {
|
||||||
|
main: 'rgb(100, 155, 240)',
|
||||||
|
dark: 'rgb(45, 92, 201)',
|
||||||
|
light: 'rgb(130, 185, 255)',
|
||||||
|
},
|
||||||
|
secondary: {
|
||||||
|
main: 'rgb(69, 173, 255)',
|
||||||
|
},
|
||||||
|
background: {
|
||||||
|
default: 'rgb(49, 51, 56)',
|
||||||
|
paper: 'rgb(62, 64, 68)',
|
||||||
|
},
|
||||||
|
text: {
|
||||||
|
primary: 'rgb(255, 255, 255)',
|
||||||
|
secondary: 'rgb(179, 179, 179)',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export { lightTheme, darkTheme };
|
||||||
1
src/vite-env.d.ts
vendored
Normal file
1
src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
/// <reference types="vite/client" />
|
||||||
27
tsconfig.app.json
Normal file
27
tsconfig.app.json
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||||
|
"target": "ES2020",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
|
||||||
|
/* Bundler mode */
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"noEmit": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
|
||||||
|
/* Linting */
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"noUncheckedSideEffectImports": true,
|
||||||
|
"types": ["qapp-core/global"]
|
||||||
|
},
|
||||||
|
"include": ["src"]
|
||||||
|
}
|
||||||
7
tsconfig.json
Normal file
7
tsconfig.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"files": [],
|
||||||
|
"references": [
|
||||||
|
{ "path": "./tsconfig.app.json" },
|
||||||
|
{ "path": "./tsconfig.node.json" }
|
||||||
|
]
|
||||||
|
}
|
||||||
25
tsconfig.node.json
Normal file
25
tsconfig.node.json
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||||
|
"target": "ES2022",
|
||||||
|
"lib": ["ES2023"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
|
||||||
|
/* Bundler mode */
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"noEmit": true,
|
||||||
|
|
||||||
|
/* Linting */
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"noUncheckedSideEffectImports": true,
|
||||||
|
"types": ["qapp-core/global"]
|
||||||
|
},
|
||||||
|
"include": ["vite.config.ts"]
|
||||||
|
}
|
||||||
11
vite.config.ts
Normal file
11
vite.config.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { defineConfig } from 'vite';
|
||||||
|
import react from '@vitejs/plugin-react';
|
||||||
|
|
||||||
|
// https://vite.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
base: '',
|
||||||
|
optimizeDeps: {
|
||||||
|
include: ['@mui/material', '@mui/styled-engine', '@mui/system'],
|
||||||
|
},
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user