initial commit

This commit is contained in:
2025-04-05 03:56:13 +03:00
commit bfac701b31
36 changed files with 5094 additions and 0 deletions

24
.gitignore vendored Normal file
View File

@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

View File

@@ -0,0 +1,8 @@
{
"hash": "964445a2",
"configHash": "294caa02",
"lockfileHash": "75101775",
"browserHash": "3e05e511",
"optimized": {},
"chunks": {}
}

3
.vite/deps/package.json Normal file
View File

@@ -0,0 +1,3 @@
{
"type": "module"
}

54
README.md Normal file
View File

@@ -0,0 +1,54 @@
# 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,
},
})
```

28
eslint.config.js Normal file
View File

@@ -0,0 +1,28 @@
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'
export default tseslint.config(
{ ignores: ['dist'] },
{
extends: [js.configs.recommended, ...tseslint.configs.recommended],
files: ['**/*.{ts,tsx}'],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
},
plugins: {
'react-hooks': reactHooks,
'react-refresh': reactRefresh,
},
rules: {
...reactHooks.configs.recommended.rules,
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
},
},
)

13
index.html Normal file
View 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>

26
initialize.js Normal file
View File

@@ -0,0 +1,26 @@
import { writeFile } from "fs/promises";
import { randomBytes } from "crypto";
import { join } from "path";
import { fileURLToPath } from "url";
import { dirname } from "path";
// Resolve __dirname in ES Modules
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
// Generate a unique public salt (32 bytes, Base64 encoded)
const publicSalt = randomBytes(32).toString("base64");
// Define the TypeScript file content
const tsContent = `export const publicSalt = "${publicSalt}";\n`;
// Define the file path
const filePath = join(__dirname, "src", "qapp-config.ts");
// Write the TypeScript file
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);
}

4103
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

37
package.json Normal file
View File

@@ -0,0 +1,37 @@
{
"name": "q-search",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"lint": "eslint .",
"preview": "vite preview",
"initialize": "node initialize.js"
},
"dependencies": {
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.0",
"@mui/icons-material": "^6.4.3",
"@mui/material": "^6.4.7",
"qapp-core": "^1.0.15",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-router-dom": "^7.3.0",
"zustand": "^5.0.3"
},
"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-plugin-react-hooks": "^5.1.0",
"eslint-plugin-react-refresh": "^0.4.19",
"globals": "^15.15.0",
"typescript": "~5.7.2",
"typescript-eslint": "^8.24.1",
"vite": "^6.2.0"
}
}

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

84
src/App.tsx Normal file
View File

@@ -0,0 +1,84 @@
import { Box, Typography, Button, InputBase } from '@mui/material';
import SearchIcon from '@mui/icons-material/Search';
import { useEffect, useRef, useState } from "react";
import { useNavigate } from "react-router-dom";
function App() {
const searchRef = useRef<HTMLInputElement>(null);
const [search, setSearch] = useState('');
const navigate = useNavigate();
const handleSearch = () => {
if (search.trim()) {
navigate(`/search?q=${encodeURIComponent(search.trim())}`);
}
};
useEffect(()=> {
if(searchRef.current){
searchRef.current.focus()
}
}, [])
return (
<Box
sx={{
height: '100vh',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
// bgcolor: 'white',
}}
>
{/* Logo */}
<Typography variant="h2" sx={{ fontFamily: 'sans-serif', color: '#4285F4', fontWeight: 600 }}>
Q-
<span style={{ color: '#4285F4' }}>s</span>
<span style={{ color: '#4285F4' }}>e</span>
<span style={{ color: '#4285F4' }}>a</span>
<span style={{ color: '#4285F4' }}>r</span>
<span style={{ color: '#4285F4' }}>c</span>
<span style={{ color: '#4285F4' }}>h</span>
</Typography>
{/* Search bar */}
<Box
sx={{
display: 'flex',
alignItems: 'center',
mt: 4,
width: '90%',
maxWidth: 600,
border: '1px solid #dfe1e5',
borderRadius: '24px',
px: 2,
py: 1,
boxShadow: 1,
}}
>
<SearchIcon sx={{ color: '#9aa0a6' }} />
<InputBase
inputRef={searchRef}
onChange={(e)=> setSearch(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter' && search?.trim()) {
handleSearch()
}
}}
value={search}
fullWidth
placeholder="Search Qortal"
sx={{ ml: 2 }}
/>
</Box>
{/* Buttons */}
<Box sx={{ display: 'flex', gap: 2, mt: 3 }}>
<Button onClick={handleSearch} variant="outlined">Qortal Search</Button>
</Box>
</Box>
)
}
export default App

23
src/AppWrapper.tsx Normal file
View File

@@ -0,0 +1,23 @@
import { Routes } from "./Routes";
import { GlobalProvider } from "qapp-core";
import { publicSalt } from "./qapp-config";
export const AppWrapper = () => {
return (
<GlobalProvider
config={{
auth: {
balanceSetting: {
interval: 180000,
onlyOnMount: false,
},
authenticateOnMount: true,
},
publicSalt: publicSalt,
appName: "q-search",
}}
>
<Routes />
</GlobalProvider>
);
};

43
src/Routes.tsx Normal file
View File

@@ -0,0 +1,43 @@
import {
createBrowserRouter,
RouterProvider,
} from "react-router-dom";
import App from "./App";
import Layout from "./styles/Layout";
import { Search } from "./page/Search";
// Use a custom type if you need it
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: <Layout />,
children: [
{
index: true,
element: <App />,
},
{
path: "search",
element: <Search />,
},
],
},
],
{
basename: baseUrl,
}
);
return <RouterProvider router={router} />;
}

View File

@@ -0,0 +1,29 @@
import { useEffect } from "react";
import { To, useNavigate } from "react-router-dom";
export const useIframe = () => {
const navigate = useNavigate();
useEffect(() => {
function handleNavigation(event: { data: { action: string; path: To }; }) {
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 },
"*"
);
}
}
window.addEventListener("message", handleNavigation);
return () => {
window.removeEventListener("message", handleNavigation);
};
}, [navigate]);
return { navigate };
};

30
src/index.css Normal file
View File

@@ -0,0 +1,30 @@
@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';
word-wrap: break-word;
}

14
src/main.tsx Normal file
View File

@@ -0,0 +1,14 @@
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import "./index.css";
import ThemeProviderWrapper from "./styles/theme/theme-provider.tsx";
import { AppWrapper } from "./AppWrapper.tsx";
createRoot(document.getElementById("root")!).render(
// <StrictMode>
<ThemeProviderWrapper>
<AppWrapper />
</ThemeProviderWrapper>
// </StrictMode>
);

289
src/page/Search.tsx Normal file
View File

@@ -0,0 +1,289 @@
import { useNavigate, useSearchParams } from "react-router-dom";
import {
Box,
Tabs,
Tab,
Typography,
InputBase,
Avatar,
Divider,
Breadcrumbs,
ButtonBase,
Button,
Dialog,
} from "@mui/material";
import SearchIcon from "@mui/icons-material/Search";
import { useCallback, useEffect, useState } from "react";
import {
createAvatarLink,
dismissToast,
extractComponents,
hashWordWithoutPublicSalt,
IndexCategory,
showError,
showLoading,
Spacer,
useGlobal,
usePublish,
} from "qapp-core";
import NavigateNextIcon from "@mui/icons-material/NavigateNext";
export const Search = () => {
const openPageIndexManager = useGlobal().indexOperations.openPageIndexManager;
const [searchParams] = useSearchParams();
const publish = usePublish();
const query = searchParams.get("q") || "";
const [tab, setTab] = useState(0);
const [results, setResults] = useState([]);
const handleTabChange = (event, newValue) => {
setTab(newValue);
};
const [search, setSearch] = useState("");
const navigate = useNavigate();
const handleSearch = () => {
if (search.trim()) {
navigate(
`/search?q=${encodeURIComponent(search.trim().toLocaleLowerCase())}`
);
}
};
const searchQortal = useCallback(async (searchQuery: string) => {
const loadId = showLoading("Loading...");
try {
setResults([]);
const res = await fetch(`/arbitrary/indices?terms=${searchQuery.trim()}`);
const data = await res.json();
const allResults = data.map(async (item) => {
const identifierWithoutHash = item.name + item.link;
const identifier = await hashWordWithoutPublicSalt(
identifierWithoutHash,
20
);
const rawData = await publish.fetchPublish(
{
name: item.name,
service: "METADATA",
identifier,
},
"JSON"
);
let copyItem = { ...item };
if (
rawData?.resource &&
rawData?.resource?.data?.title &&
rawData?.resource?.data?.description
) {
copyItem = {
...copyItem,
title: rawData?.resource?.data?.title,
description: rawData?.resource?.data?.description,
};
}
return copyItem;
});
const responseFromPromise = await Promise.all(allResults);
setResults(responseFromPromise);
} catch (error) {
showError(error?.message || "Failed to fetch results");
} finally {
dismissToast(loadId);
}
}, []);
useEffect(() => {
if (query.trim()) {
searchQortal(query.trim());
}
}, [searchQortal, query]);
return (
<Box
sx={{
width: "100%",
padding: '10px'
}}
>
{/* Top search bar */}
<Box
sx={{
display: "flex",
alignItems: "center",
border: "1px solid #dfe1e5",
borderRadius: "24px",
maxWidth: 600,
mx: "auto",
px: 2,
py: 1,
mb: 3,
}}
>
<SearchIcon sx={{ color: "#9aa0a6" }} />
<InputBase
onChange={(e) => setSearch(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter" && search?.trim()) {
handleSearch();
}
}}
placeholder="Search Qortal"
value={search}
fullWidth
defaultValue={query}
sx={{ ml: 2 }}
/>
</Box>
{/* Tabs */}
<Tabs
value={tab}
onChange={handleTabChange}
centered
textColor="primary"
indicatorColor="primary"
sx={{ borderBottom: 1, borderColor: "divider" }}
>
{/* <Tab label="All" /> */}
{/* <Tab disabled={true} label="Videos" />
<Tab disabled={true} label="Images" />
<Tab disabled={true} label="PDFs" />
<Tab disabled={true} label="Audio" /> */}
</Tabs>
{/* Content */}
<Box sx={{ mt: 4 }}>
<Typography variant="h6">
Showing{" "}
{
[
"all results",
"video results",
"image results",
"pdf results",
"audio results",
][tab]
}{" "}
for:
</Typography>
<Typography variant="h5" color="primary" sx={{ mt: 1 }}>
"{query}"
</Typography>
{/* You can replace this with dynamic results per tab */}
</Box>
<Spacer height="25px" />
{results?.length === 0 && (
<Typography variant="h6">No results</Typography>
)}
{results?.map((item, i) => {
const res = extractComponents(item?.link || "");
const appName = res?.name || "";
return (
<Box key={i} sx={{ mb: 3, display: "flex", gap: 2, width: "100%" }}>
<Avatar
alt={appName}
src={createAvatarLink(appName)}
variant="square"
sx={{ width: 24, height: 24, mt: 0.5 }}
/>
<Box
sx={{
width: "calc(100% - 50px)",
}}
>
<ButtonBase
sx={{
width: "100%",
justifyContent: 'flex-start'
}}
onClick={() => {
qortalRequest({
action: "OPEN_NEW_TAB",
qortalLink: item?.link,
});
}}
>
<Breadcrumbs
separator={<NavigateNextIcon fontSize="small" />}
aria-label="breadcrumb"
>
<Typography variant="body2" color="text.secondary">
{res?.service}
</Typography>
<Typography
sx={{
textAlign: "start",
}}
variant="body2"
color="text.secondary"
>
{appName}
</Typography>
<Typography
sx={{
textAlign: "start",
}}
variant="body2"
color="text.secondary"
>
{res?.path}
</Typography>
</Breadcrumbs>
</ButtonBase>
<Spacer height="10px" />
<ButtonBase
sx={{
width: "100%",
}}
onClick={() => {
qortalRequest({
action: "OPEN_NEW_TAB",
qortalLink: item?.link,
});
}}
>
<Typography
variant="h6"
sx={{
display: "block",
textDecoration: "none",
width: "100%",
textAlign: "start",
"&:hover": { textDecoration: "underline" },
}}
>
{item?.title}
</Typography>
</ButtonBase>
<Typography
sx={{
overflow: "hidden",
textOverflow: "ellipsis",
}}
variant="body2"
color="text.secondary"
>
{item?.description}
</Typography>
<Button
onClick={() => {
openPageIndexManager({
link: item.link,
name: item.name,
category: IndexCategory.PUBLIC_PAGE_VIDEO,
rootName: appName,
});
}}
>
Index
</Button>
<Divider sx={{ mt: 2 }} />
</Box>
</Box>
);
})}
</Box>
);
};

1
src/qapp-config.ts Normal file
View File

@@ -0,0 +1 @@
export const publicSalt = "pJ2qmod6Q4tyBWKMGk0BdAdB+SvnoYR+R6LLtSjEpCY=";

View File

@@ -0,0 +1,15 @@
import { create } from "zustand";
export enum EnumTheme {
LIGHT = 1,
DARK = 2
}
interface SystemState {
theme: EnumTheme;
setTheme: (theme: EnumTheme) => void;
}
export const useSystemState = create<SystemState>((set) => ({
theme: EnumTheme.DARK,
setTheme: (theme: EnumTheme) => set({ theme }),
}));

19
src/styles/Layout.tsx Normal file
View File

@@ -0,0 +1,19 @@
import { Outlet } from "react-router-dom";
import { useIframe } from "../hooks/useIframeListener";
import { Container } from "@mui/material";
const Layout = () => {
useIframe()
return (
<Container>
{/* Add Header here */}
<main>
<Outlet /> {/* This is where page content will be rendered */}
</main>
{/* Add Footer here */}
</Container>
);
};
export default Layout;

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,24 @@
import React, { FC } from "react";
import { ThemeProvider } from "@emotion/react";
import {lightTheme, darkTheme} from "./theme"
import { CssBaseline } from "@mui/material";
import { EnumTheme, useSystemState } from "../../state/global/system";
interface ThemeProviderWrapperProps {
children: React.ReactNode;
}
const ThemeProviderWrapper: FC<ThemeProviderWrapperProps> = ({ children }) => {
const theme = useSystemState().theme
return (
<ThemeProvider theme={theme === EnumTheme.LIGHT ? lightTheme : darkTheme}>
<CssBaseline />
{children}
</ThemeProvider>
);
};
export default ThemeProviderWrapper;

158
src/styles/theme/theme.ts Normal file
View File

@@ -0,0 +1,158 @@
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: "23px",
fontWeight: 400,
lineHeight: 1.5,
letterSpacing: "0.5px",
},
body2: {
fontSize: "18px",
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,
},
},
components: {
MuiModal: {
styleOverrides: {
root: {
zIndex: 50000,
},
},
},
},
};
const lightTheme = createTheme({
...commonThemeOptions,
palette: {
mode: "light",
primary: {
main: "rgb(40, 84, 76)",
light: "rgb(123, 158, 143)",
dark: "rgb(5, 10, 11)",
contrastText: "rgb(199, 210, 218)",
},
secondary: {
main: "rgb(65, 143, 75)",
dark: "rgba(69, 114, 87, 0.67)",
},
error: {
main: "rgba(148, 23, 16, 1)",
},
text: {
primary: "rgba(4,6,6,0.87)",
secondary: "rgb(113, 122, 124)",
},
info: {
main: "rgb(8, 155, 152)",
},
background: {
default: "rgb(233, 233, 233)",
paper: "rgb(186, 186, 186)",
},
action: {
hover: "rgba(58, 81, 64, 0.27)",
selected: "rgba(31, 93, 124, 0.29)",
},
},
typography: {
fontWeightMedium: 600,
h1: {
fontFamily: "Droid Sans",
},
fontSize: 16,
fontFamily: "Inter",
},
});
const darkTheme = createTheme({
...commonThemeOptions,
palette: {
mode: "dark",
primary: {
main: "rgba(70, 95, 107, 0.98)",
light: "rgba(126, 160, 170, 0.96)",
dark: "rgba(104, 108, 116, 0.98)",
contrastText: "rgba(215, 225, 222, 0.97)",
},
secondary: {
main: "rgba(133, 186, 150, 0.95)",
dark: " #33445566 "
},
error: {
main: "rgba(212, 11, 42, 0.94)",
},
text: {
primary: "rgba(255, 255, 255, 0.87)",
secondary: "rgba(116, 143, 127, 0.93)",
},
info: {
main: "rgba(33,220,243,0.87)",
},
background: {
default: "rgb(15, 16, 16)",
paper: "rgb(29, 35, 36)",
},
action: {
hover: "rgba(47, 58, 64, 0.73)", // darker than the default 0.04
selected: "rgba(80, 125, 159, 0.61)",
},
divider: "rgba(89, 89, 89, 0.76)",
},
typography: {
fontWeightMedium: 600,
h1: {
fontFamily: "Droid Sans",
},
fontSize: 16,
fontFamily: "Inter",
},
});
export { lightTheme, darkTheme };

1
src/vite-env.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
/// <reference types="vite/client" />

26
tsconfig.app.json Normal file
View File

@@ -0,0 +1,26 @@
{
"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
},
"include": ["src"]
}

7
tsconfig.json Normal file
View File

@@ -0,0 +1,7 @@
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
]
}

24
tsconfig.node.json Normal file
View File

@@ -0,0 +1,24 @@
{
"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
},
"include": ["vite.config.ts"]
}

11
vite.config.ts Normal file
View 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"],
},
})