feat: basic ui layout
This commit is contained in:
parent
0c4713bd3a
commit
37f6667e50
@ -4,3 +4,9 @@
|
||||
pnpm i
|
||||
pnpm start
|
||||
```
|
||||
|
||||
## TODO
|
||||
|
||||
- 文件拖放 (利用 `react-dropzone`?)
|
||||
- 各类算法
|
||||
- 简易元数据编辑器
|
||||
|
@ -2,12 +2,11 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Vite + React + TS</title>
|
||||
<title>音乐解锁 - Unlock Music</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<main id="root"></main>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
@ -14,9 +14,11 @@
|
||||
"@chakra-ui/react": "^2.6.1",
|
||||
"@emotion/react": "^11.11.0",
|
||||
"@emotion/styled": "^11.11.0",
|
||||
"@reduxjs/toolkit": "^1.9.5",
|
||||
"framer-motion": "^10.12.8",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0"
|
||||
"react-dom": "^18.2.0",
|
||||
"react-redux": "^8.0.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.0.28",
|
||||
|
105
pnpm-lock.yaml
105
pnpm-lock.yaml
@ -13,6 +13,9 @@ dependencies:
|
||||
'@emotion/styled':
|
||||
specifier: ^11.11.0
|
||||
version: 11.11.0(@emotion/react@11.11.0)(@types/react@18.0.28)(react@18.2.0)
|
||||
'@reduxjs/toolkit':
|
||||
specifier: ^1.9.5
|
||||
version: 1.9.5(react-redux@8.0.5)(react@18.2.0)
|
||||
framer-motion:
|
||||
specifier: ^10.12.8
|
||||
version: 10.12.8(react-dom@18.2.0)(react@18.2.0)
|
||||
@ -22,6 +25,9 @@ dependencies:
|
||||
react-dom:
|
||||
specifier: ^18.2.0
|
||||
version: 18.2.0(react@18.2.0)
|
||||
react-redux:
|
||||
specifier: ^8.0.5
|
||||
version: 8.0.5(@types/react-dom@18.0.11)(@types/react@18.0.28)(react-dom@18.2.0)(react@18.2.0)(redux@4.2.1)
|
||||
|
||||
devDependencies:
|
||||
'@types/react':
|
||||
@ -1818,6 +1824,32 @@ packages:
|
||||
resolution: {integrity: sha512-Cr4OjIkipTtcXKjAsm8agyleBuDHvxzeBoa1v543lbv1YaIwQjESsVcmjiWiPEbC1FIeHOG/Op9kdCmAmiS3Kw==}
|
||||
dev: false
|
||||
|
||||
/@reduxjs/toolkit@1.9.5(react-redux@8.0.5)(react@18.2.0):
|
||||
resolution: {integrity: sha512-Rt97jHmfTeaxL4swLRNPD/zV4OxTes4la07Xc4hetpUW/vc75t5m1ANyxG6ymnEQ2FsLQsoMlYB2vV1sO3m8tQ==}
|
||||
peerDependencies:
|
||||
react: ^16.9.0 || ^17.0.0 || ^18
|
||||
react-redux: ^7.2.1 || ^8.0.2
|
||||
peerDependenciesMeta:
|
||||
react:
|
||||
optional: true
|
||||
react-redux:
|
||||
optional: true
|
||||
dependencies:
|
||||
immer: 9.0.21
|
||||
react: 18.2.0
|
||||
react-redux: 8.0.5(@types/react-dom@18.0.11)(@types/react@18.0.28)(react-dom@18.2.0)(react@18.2.0)(redux@4.2.1)
|
||||
redux: 4.2.1
|
||||
redux-thunk: 2.4.2(redux@4.2.1)
|
||||
reselect: 4.1.8
|
||||
dev: false
|
||||
|
||||
/@types/hoist-non-react-statics@3.3.1:
|
||||
resolution: {integrity: sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA==}
|
||||
dependencies:
|
||||
'@types/react': 18.0.28
|
||||
hoist-non-react-statics: 3.3.2
|
||||
dev: false
|
||||
|
||||
/@types/json-schema@7.0.11:
|
||||
resolution: {integrity: sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==}
|
||||
dev: true
|
||||
@ -1843,7 +1875,6 @@ packages:
|
||||
resolution: {integrity: sha512-O38bPbI2CWtgw/OoQoY+BRelw7uysmXbWvw3nLWO21H1HSh+GOlqPuXshJfjmpNlKiiSDG9cc1JZAaMmVdcTlw==}
|
||||
dependencies:
|
||||
'@types/react': 18.0.28
|
||||
dev: true
|
||||
|
||||
/@types/react@18.0.28:
|
||||
resolution: {integrity: sha512-RD0ivG1kEztNBdoAK7lekI9M+azSnitIn85h4iOiaLjaTrMjzslhaqCGaI4IyCJ1RljWiLCEu4jyrLLgqxBTew==}
|
||||
@ -1859,6 +1890,10 @@ packages:
|
||||
resolution: {integrity: sha512-21cFJr9z3g5dW8B0CVI9g2O9beqaThGQ6ZFBqHfwhzLDKUxaqTIy3vnfah/UPkfOiF2pLq+tGz+W8RyCskuslw==}
|
||||
dev: true
|
||||
|
||||
/@types/use-sync-external-store@0.0.3:
|
||||
resolution: {integrity: sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA==}
|
||||
dev: false
|
||||
|
||||
/@typescript-eslint/eslint-plugin@5.57.1(@typescript-eslint/parser@5.57.1)(eslint@8.38.0)(typescript@5.0.2):
|
||||
resolution: {integrity: sha512-1MeobQkQ9tztuleT3v72XmY0XuKXVXusAhryoLuU5YZ+mXoYKZP9SQ7Flulh1NX4DTjpGTc2b/eMu4u7M7dhnQ==}
|
||||
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
||||
@ -2625,6 +2660,10 @@ packages:
|
||||
engines: {node: '>= 4'}
|
||||
dev: true
|
||||
|
||||
/immer@9.0.21:
|
||||
resolution: {integrity: sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==}
|
||||
dev: false
|
||||
|
||||
/import-fresh@3.3.0:
|
||||
resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==}
|
||||
engines: {node: '>=6'}
|
||||
@ -2978,6 +3017,44 @@ packages:
|
||||
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
|
||||
dev: false
|
||||
|
||||
/react-is@18.2.0:
|
||||
resolution: {integrity: sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==}
|
||||
dev: false
|
||||
|
||||
/react-redux@8.0.5(@types/react-dom@18.0.11)(@types/react@18.0.28)(react-dom@18.2.0)(react@18.2.0)(redux@4.2.1):
|
||||
resolution: {integrity: sha512-Q2f6fCKxPFpkXt1qNRZdEDLlScsDWyrgSj0mliK59qU6W5gvBiKkdMEG2lJzhd1rCctf0hb6EtePPLZ2e0m1uw==}
|
||||
peerDependencies:
|
||||
'@types/react': ^16.8 || ^17.0 || ^18.0
|
||||
'@types/react-dom': ^16.8 || ^17.0 || ^18.0
|
||||
react: ^16.8 || ^17.0 || ^18.0
|
||||
react-dom: ^16.8 || ^17.0 || ^18.0
|
||||
react-native: '>=0.59'
|
||||
redux: ^4
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
'@types/react-dom':
|
||||
optional: true
|
||||
react-dom:
|
||||
optional: true
|
||||
react-native:
|
||||
optional: true
|
||||
redux:
|
||||
optional: true
|
||||
dependencies:
|
||||
'@babel/runtime': 7.21.5
|
||||
'@types/hoist-non-react-statics': 3.3.1
|
||||
'@types/react': 18.0.28
|
||||
'@types/react-dom': 18.0.11
|
||||
'@types/use-sync-external-store': 0.0.3
|
||||
hoist-non-react-statics: 3.3.2
|
||||
react: 18.2.0
|
||||
react-dom: 18.2.0(react@18.2.0)
|
||||
react-is: 18.2.0
|
||||
redux: 4.2.1
|
||||
use-sync-external-store: 1.2.0(react@18.2.0)
|
||||
dev: false
|
||||
|
||||
/react-refresh@0.14.0:
|
||||
resolution: {integrity: sha512-wViHqhAd8OHeLS/IRMJjTSDHF3U9eWi62F/MledQGPdJGDhodXJ9PBLNGr6WWL7qlH12Mt3TyTpbS+hGXMjCzQ==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@ -3042,10 +3119,28 @@ packages:
|
||||
loose-envify: 1.4.0
|
||||
dev: false
|
||||
|
||||
/redux-thunk@2.4.2(redux@4.2.1):
|
||||
resolution: {integrity: sha512-+P3TjtnP0k/FEjcBL5FZpoovtvrTNT/UXd4/sluaSyrURlSlhLSzEdfsTBW7WsKB6yPvgd7q/iZPICFjW4o57Q==}
|
||||
peerDependencies:
|
||||
redux: ^4
|
||||
dependencies:
|
||||
redux: 4.2.1
|
||||
dev: false
|
||||
|
||||
/redux@4.2.1:
|
||||
resolution: {integrity: sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==}
|
||||
dependencies:
|
||||
'@babel/runtime': 7.21.5
|
||||
dev: false
|
||||
|
||||
/regenerator-runtime@0.13.11:
|
||||
resolution: {integrity: sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==}
|
||||
dev: false
|
||||
|
||||
/reselect@4.1.8:
|
||||
resolution: {integrity: sha512-ab9EmR80F/zQTMNeneUr4cv+jSwPJgIlvEmVwLerwrWVbpLlBuls9XHzIeTFy4cegU2NHBp3va0LKOzU5qFEYQ==}
|
||||
dev: false
|
||||
|
||||
/resolve-from@4.0.0:
|
||||
resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==}
|
||||
engines: {node: '>=4'}
|
||||
@ -3276,6 +3371,14 @@ packages:
|
||||
tslib: 2.5.0
|
||||
dev: false
|
||||
|
||||
/use-sync-external-store@1.2.0(react@18.2.0):
|
||||
resolution: {integrity: sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==}
|
||||
peerDependencies:
|
||||
react: ^16.8.0 || ^17.0.0 || ^18.0.0
|
||||
dependencies:
|
||||
react: 18.2.0
|
||||
dev: false
|
||||
|
||||
/vite@4.3.2:
|
||||
resolution: {integrity: sha512-9R53Mf+TBoXCYejcL+qFbZde+eZveQLDYd9XgULILLC1a5ZwPaqgmdVpL8/uvw2BM/1TzetWjglwm+3RO+xTyw==}
|
||||
engines: {node: ^14.18.0 || >=16.0.0}
|
||||
|
@ -1 +0,0 @@
|
||||
/* empty file here */
|
19
src/App.tsx
19
src/App.tsx
@ -1,11 +1,22 @@
|
||||
import './App.css';
|
||||
import { Box, Center, Container } from '@chakra-ui/react';
|
||||
import { SelectFile } from './SelectFile';
|
||||
|
||||
import { FileListing } from './features/file-listing/FileListing';
|
||||
import { Footer } from './Footer';
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<main>
|
||||
<SelectFile />
|
||||
</main>
|
||||
<Box height="full" width="full" pt="4">
|
||||
<Container maxW="container.large">
|
||||
<Center>
|
||||
<SelectFile />
|
||||
</Center>
|
||||
<Box mt="8">
|
||||
<FileListing />
|
||||
</Box>
|
||||
<Footer />
|
||||
</Container>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
|
33
src/Footer.tsx
Normal file
33
src/Footer.tsx
Normal file
@ -0,0 +1,33 @@
|
||||
import { Center, Link, Text } from '@chakra-ui/react';
|
||||
|
||||
export function Footer() {
|
||||
return (
|
||||
<Center height="footer.container">
|
||||
<Center
|
||||
height="footer.content"
|
||||
fontSize="sm"
|
||||
textAlign="center"
|
||||
position="fixed"
|
||||
bottom="0"
|
||||
w="full"
|
||||
bg="gray.100"
|
||||
color="gray.800"
|
||||
left="0"
|
||||
flexDir="column"
|
||||
>
|
||||
<Text>音乐解锁 (x.x.x) - 移除已购音乐的加密保护。</Text>
|
||||
<Text>
|
||||
Copyright © 2019 - 2023{' '}
|
||||
<Link href="https://git.unlock-music.dev/um" isExternal>
|
||||
UnlockMusic 团队
|
||||
</Link>{' '}
|
||||
| 音乐解锁授权基于
|
||||
<Link href="https://git.unlock-music.dev/um/um-react/src/branch/main/LICENSE" isExternal>
|
||||
MIT许可协议
|
||||
</Link>
|
||||
。
|
||||
</Text>
|
||||
</Center>
|
||||
</Center>
|
||||
);
|
||||
}
|
@ -1,5 +0,0 @@
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
export const PointerLabel = styled.label`
|
||||
cursor: pointer;
|
||||
`;
|
@ -1,25 +1,44 @@
|
||||
import { Box, Stack, Text } from '@chakra-ui/react';
|
||||
import { UnlockIcon } from '@chakra-ui/icons';
|
||||
import { useId } from 'react';
|
||||
import { PointerLabel } from './PointerLabel';
|
||||
|
||||
import { Box, Text } from '@chakra-ui/react';
|
||||
import { UnlockIcon } from '@chakra-ui/icons';
|
||||
|
||||
export function SelectFile() {
|
||||
const id = useId();
|
||||
|
||||
return (
|
||||
<Box borderWidth="1px" borderRadius="lg" p="6">
|
||||
<Stack alignItems="center">
|
||||
<UnlockIcon />
|
||||
<Box>
|
||||
将文件拖到此处,或
|
||||
<PointerLabel htmlFor={id}>
|
||||
<Text as="span" color="teal.400">
|
||||
点击选择
|
||||
</Text>
|
||||
</PointerLabel>
|
||||
<input id={id} type="file" hidden multiple />
|
||||
</Box>
|
||||
</Stack>
|
||||
<Box
|
||||
as="label"
|
||||
htmlFor={id}
|
||||
w="100%"
|
||||
maxW={480}
|
||||
borderWidth="1px"
|
||||
borderRadius="lg"
|
||||
transitionDuration="0.5s"
|
||||
p="6"
|
||||
cursor="pointer"
|
||||
display="flex"
|
||||
flexDir="column"
|
||||
alignItems="center"
|
||||
_hover={{
|
||||
borderColor: 'gray.400',
|
||||
bg: 'gray.50',
|
||||
}}
|
||||
>
|
||||
<Box pb={3}>
|
||||
<UnlockIcon boxSize={8} />
|
||||
</Box>
|
||||
<Box textAlign="center">
|
||||
{/* 将文件拖到此处,或 */}
|
||||
<Text as="span" color="teal.400">
|
||||
点我选择
|
||||
</Text>
|
||||
需要解密的文件
|
||||
<input id={id} type="file" hidden multiple />
|
||||
<Text fontSize="sm" opacity="50%">
|
||||
仅在浏览器内对文件进行解锁,无需消耗流量
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
56
src/features/file-listing/FileListing.tsx
Normal file
56
src/features/file-listing/FileListing.tsx
Normal file
@ -0,0 +1,56 @@
|
||||
import { useEffect } from 'react';
|
||||
import { Avatar, Box, Table, TableContainer, Tbody, Td, Text, Th, Thead, Tr, Wrap, WrapItem } from '@chakra-ui/react';
|
||||
|
||||
import { addNewFile, selectFiles } from './fileListingSlice';
|
||||
import { useAppDispatch, useAppSelector } from '../../hooks';
|
||||
|
||||
export function FileListing() {
|
||||
const dispatch = useAppDispatch();
|
||||
const files = useAppSelector(selectFiles);
|
||||
|
||||
useEffect(() => {
|
||||
// FIXME: Remove test data
|
||||
if (files.length === 0) {
|
||||
dispatch(addNewFile({ id: String(Date.now()), fileName: '测试文件名.mgg', blobURI: '' }));
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<TableContainer>
|
||||
<Table variant="striped">
|
||||
<Thead>
|
||||
<Tr>
|
||||
<Th w="1%">封面</Th>
|
||||
<Th>元信息</Th>
|
||||
<Th>操作</Th>
|
||||
</Tr>
|
||||
</Thead>
|
||||
<Tbody>
|
||||
{files.map((file) => (
|
||||
<Tr key={file.id}>
|
||||
<Td>
|
||||
{file.metadata.cover && <Avatar size="sm" name="专辑封面" src={file.metadata.cover} />}
|
||||
{!file.metadata.cover && <Text>暂无封面</Text>}
|
||||
</Td>
|
||||
<Td>
|
||||
<Box as="h4" fontWeight="semibold" mt="1">
|
||||
{file.metadata.name || file.fileName}
|
||||
</Box>
|
||||
<Text>专辑: {file.metadata.album}</Text>
|
||||
<Text>艺术家: {file.metadata.artist}</Text>
|
||||
<Text>专辑艺术家: {file.metadata.albumArtist}</Text>
|
||||
</Td>
|
||||
<Td>
|
||||
<Wrap>
|
||||
<WrapItem>播放</WrapItem>
|
||||
<WrapItem>下载</WrapItem>
|
||||
<WrapItem>删除</WrapItem>
|
||||
</Wrap>
|
||||
</Td>
|
||||
</Tr>
|
||||
))}
|
||||
</Tbody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
);
|
||||
}
|
79
src/features/file-listing/fileListingSlice.ts
Normal file
79
src/features/file-listing/fileListingSlice.ts
Normal file
@ -0,0 +1,79 @@
|
||||
import { createSlice } from '@reduxjs/toolkit';
|
||||
import type { PayloadAction } from '@reduxjs/toolkit';
|
||||
import type { RootState } from '../../store';
|
||||
|
||||
export enum ProcessState {
|
||||
UNTOUCHED = 'UNTOUCHED',
|
||||
COMPLETE = 'COMPLETE',
|
||||
ERROR = 'ERROR',
|
||||
}
|
||||
|
||||
export enum ListingMode {
|
||||
LIST = 'LIST',
|
||||
CARD = 'CARD',
|
||||
}
|
||||
|
||||
export interface AudioMetadata {
|
||||
name: string;
|
||||
artist: string;
|
||||
album: string;
|
||||
albumArtist: string;
|
||||
cover: string; // blob uri
|
||||
}
|
||||
|
||||
export interface DecryptedAudioFile {
|
||||
id: string;
|
||||
fileName: string;
|
||||
raw: string; // blob uri
|
||||
decrypted: string; // blob uri
|
||||
state: ProcessState;
|
||||
errorMessage: null | string;
|
||||
metadata: AudioMetadata;
|
||||
}
|
||||
|
||||
export interface FileListingState {
|
||||
files: DecryptedAudioFile[];
|
||||
displayMode: ListingMode;
|
||||
}
|
||||
const initialState: FileListingState = {
|
||||
files: [],
|
||||
displayMode: ListingMode.LIST,
|
||||
};
|
||||
|
||||
export const fileListingSlice = createSlice({
|
||||
name: 'fileListing',
|
||||
initialState,
|
||||
reducers: {
|
||||
addNewFile: (state, { payload }: PayloadAction<{ id: string; fileName: string; blobURI: string }>) => {
|
||||
state.files.push({
|
||||
id: payload.id,
|
||||
fileName: payload.fileName,
|
||||
raw: payload.blobURI,
|
||||
decrypted: '',
|
||||
state: ProcessState.UNTOUCHED,
|
||||
errorMessage: null,
|
||||
metadata: {
|
||||
name: '',
|
||||
artist: '',
|
||||
album: '',
|
||||
albumArtist: '',
|
||||
cover: '',
|
||||
},
|
||||
});
|
||||
},
|
||||
setDecryptedContent: (state, { payload }: PayloadAction<{ id: string; decryptedBlobURI: string }>) => {
|
||||
const file = state.files.find((file) => file.id === payload.id);
|
||||
if (file) {
|
||||
file.decrypted = payload.decryptedBlobURI;
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const { addNewFile, setDecryptedContent } = fileListingSlice.actions;
|
||||
|
||||
export const selectFileCount = (state: RootState) => state.fileListing.files.length;
|
||||
export const selectFiles = (state: RootState) => state.fileListing.files;
|
||||
export const selectFileListingMode = (state: RootState) => state.fileListing.displayMode;
|
||||
|
||||
export default fileListingSlice.reducer;
|
6
src/hooks.ts
Normal file
6
src/hooks.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import type { TypedUseSelectorHook } from 'react-redux';
|
||||
import type { RootState, AppDispatch } from './store';
|
||||
|
||||
export const useAppDispatch: () => AppDispatch = useDispatch;
|
||||
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
|
@ -1,69 +0,0 @@
|
||||
:root {
|
||||
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
|
||||
line-height: 1.5;
|
||||
font-weight: 400;
|
||||
|
||||
color-scheme: light dark;
|
||||
color: rgba(255, 255, 255, 0.87);
|
||||
background-color: #242424;
|
||||
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
}
|
||||
|
||||
a {
|
||||
font-weight: 500;
|
||||
color: #646cff;
|
||||
text-decoration: inherit;
|
||||
}
|
||||
a:hover {
|
||||
color: #535bf2;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
display: flex;
|
||||
place-items: center;
|
||||
min-width: 320px;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 3.2em;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
button {
|
||||
border-radius: 8px;
|
||||
border: 1px solid transparent;
|
||||
padding: 0.6em 1.2em;
|
||||
font-size: 1em;
|
||||
font-weight: 500;
|
||||
font-family: inherit;
|
||||
background-color: #1a1a1a;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.25s;
|
||||
}
|
||||
button:hover {
|
||||
border-color: #646cff;
|
||||
}
|
||||
button:focus,
|
||||
button:focus-visible {
|
||||
outline: 4px auto -webkit-focus-ring-color;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
:root {
|
||||
color: #213547;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
a:hover {
|
||||
color: #747bff;
|
||||
}
|
||||
button {
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
}
|
12
src/main.tsx
12
src/main.tsx
@ -1,14 +1,18 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import App from './App.tsx';
|
||||
import './index.css';
|
||||
import App from './App';
|
||||
|
||||
import { ChakraProvider } from '@chakra-ui/react';
|
||||
import { Provider } from 'react-redux';
|
||||
import { store } from './store';
|
||||
import { theme } from './theme';
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
|
||||
<React.StrictMode>
|
||||
<ChakraProvider>
|
||||
<App />
|
||||
<ChakraProvider theme={theme}>
|
||||
<Provider store={store}>
|
||||
<App />
|
||||
</Provider>
|
||||
</ChakraProvider>
|
||||
</React.StrictMode>
|
||||
);
|
||||
|
11
src/store.ts
Normal file
11
src/store.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { configureStore } from '@reduxjs/toolkit';
|
||||
import fileListing from './features/file-listing/fileListingSlice';
|
||||
|
||||
export const store = configureStore({
|
||||
reducer: {
|
||||
fileListing: fileListing,
|
||||
},
|
||||
});
|
||||
|
||||
export type RootState = ReturnType<typeof store.getState>;
|
||||
export type AppDispatch = typeof store.dispatch;
|
17
src/theme.ts
Normal file
17
src/theme.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import { extendTheme } from '@chakra-ui/react';
|
||||
|
||||
export const theme = extendTheme({
|
||||
styles: {
|
||||
global: {
|
||||
body: {
|
||||
minHeight: '100vh',
|
||||
},
|
||||
},
|
||||
},
|
||||
sizes: {
|
||||
footer: {
|
||||
container: '7rem',
|
||||
content: '5rem',
|
||||
},
|
||||
},
|
||||
});
|
Loading…
Reference in New Issue
Block a user