Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"next-i18next": "^13.1.4",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-dropzone": "^14.2.3",
"react-i18next": "^12.1.5",
"typescript": "4.9.5"
},
Expand Down
9 changes: 6 additions & 3 deletions src/app/providers/withTheme.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,9 @@ import { usePreferredTheme } from '@/shared/lib/theme/usePreferredTheme';
import { ThemeProvider } from '@mui/material';
import { ComponentType } from 'react';

export const withTheme =
<P extends {}>(Component: ComponentType<P>) =>
export const withTheme = <P extends {}>(Component: ComponentType<P>) => {
// eslint-disable-next-line react/display-name
(props: P) => {
const WrappedComponent = (props: P) => {
const theme = usePreferredTheme();

return (
Expand All @@ -14,3 +13,7 @@ export const withTheme =
</ThemeProvider>
);
};

Object.assign(WrappedComponent, Component);
return WrappedComponent;
};
4 changes: 4 additions & 0 deletions src/app/typings/next-auth.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ declare module 'next-auth' {
accessToken?: string;
refreshToken?: string;
}

interface Session {
accessToken?: string;
}
}

declare module 'next-auth/jwt' {
Expand Down
1 change: 1 addition & 0 deletions src/entities/file/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './ui';
105 changes: 105 additions & 0 deletions src/entities/file/ui/FileDropzone.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import {
Box,
BoxProps,
Button,
FormControl,
FormHelperText,
Typography,
} from '@mui/material';
import { DropzoneOptions, useDropzone } from 'react-dropzone';
import CloudUploadIcon from '@mui/icons-material/CloudUpload';
import { styled, alpha } from '@mui/material/styles';

interface ContainerProps extends BoxProps {
disabled?: boolean;
}

const Container = styled(Box, {
shouldForwardProp: (prop) => prop !== 'disabled',
})<ContainerProps>(({ theme, disabled }) => ({
height: '50vh',
width: '50vh',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
backgroundColor: alpha(
theme.palette.mode === 'dark'
? theme.palette.common.white
: theme.palette.common.black,
0.15
),
border: 2,
borderRadius: theme.shape.borderRadius,
borderColor: theme.palette.divider,
borderStyle: 'solid',
padding: theme.spacing(3, 1),
'&:hover': {
borderColor: disabled ? undefined : 'text.primary',
borderStyle: 'dashed',
},
'&:focus-within': {
borderColor: 'primary.main',
borderWidth: 2,
},
}));

interface FileDropzoneProps
extends Omit<DropzoneOptions, 'onDrop' | 'onDropAccepted'> {
title?: string;
buttonText?: string;
value?: File[];
onChange: (files: File[]) => void;
}

const FileDropzone = ({
value,
onChange,
title,
buttonText,
disabled,
maxSize,
...options
}: FileDropzoneProps) => {
const { fileRejections, getRootProps, getInputProps, open } = useDropzone({
...options,
disabled,
maxSize,
onDropAccepted: onChange,
noClick: true,
noKeyboard: true,
});

const isFileTooLarge =
maxSize !== undefined &&
fileRejections.length > 0 &&
fileRejections[0].file.size > maxSize;

return (
<Container {...getRootProps()}>
<FormControl error={isFileTooLarge}>
<input {...getInputProps()} />
<CloudUploadIcon
sx={{ fontSize: 72, alignSelf: 'center' }}
color={disabled ? 'disabled' : 'primary'}
/>
<Typography variant="subtitle1" textAlign="center" sx={{ paddingY: 1 }}>
{title}
</Typography>
<Button
variant="contained"
onClick={open}
disabled={disabled}
sx={{ marginBottom: 1 }}
>
{buttonText}
</Button>
<FormHelperText>
{' '}
{fileRejections[0]?.errors[0]?.message}{' '}
</FormHelperText>
</FormControl>
</Container>
);
};

export default FileDropzone;
1 change: 1 addition & 0 deletions src/entities/file/ui/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default as FileDropzone } from './FileDropzone';
17 changes: 16 additions & 1 deletion src/pages/_app.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import '@/app/styles/globals.css';
import Head from 'next/head';
import { AppProps as NextAppProps } from 'next/app';
import NextApp, { AppContext, AppProps as NextAppProps } from 'next/app';
import CssBaseline from '@mui/material/CssBaseline';
import { CacheProvider, EmotionCache } from '@emotion/react';
import { SessionProvider } from 'next-auth/react';
import createEmotionCache from '@/shared/lib/createEmotionCache';
import { withProviders } from '@/app/providers';
import { Layout } from '@/widgets/layout';
import { getToken } from 'next-auth/jwt';
import { api } from '@/shared/api/base';

const clientSideEmotionCache = createEmotionCache();

Expand All @@ -32,4 +34,17 @@ const App = ({
</SessionProvider>
);

App.getInitialProps = async (context: AppContext) => {
const pageProps = await NextApp.getInitialProps(context);

if (!api.defaults.headers.common.Authorization) {
// @ts-ignore
const token = await getToken({ req: context.ctx.req });

api.defaults.headers.common.Authorization = `Bearer ${token?.accessToken}`;
}

return pageProps;
};

export default withProviders(App);
4 changes: 4 additions & 0 deletions src/pages/api/auth/[...nextauth].ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,5 +26,9 @@ export default NextAuth({

return token;
},
session: ({ session, token }) => {
session.accessToken = token.accessToken;
return session;
},
},
});
33 changes: 12 additions & 21 deletions src/pages/index.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
import { Box, Container, Typography } from '@mui/material';
import { GetServerSideProps } from 'next';
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
import { useTranslation } from 'next-i18next';
import { setAuthToken } from '@/shared/api/base';
import { file } from '@/shared/api';
import { getToken } from 'next-auth/jwt';
import { FileDropzone } from '@/entities/file';

interface IndexProps {
files: unknown[];
Expand All @@ -13,31 +11,24 @@ interface IndexProps {
const Index = (_props: IndexProps) => {
const { t } = useTranslation();

const onChange = async ([fileToUpload]: File[]) => {
const response = await file.uploadFile(fileToUpload);
console.log(response);
};

return (
<Container maxWidth="lg">
<Box
sx={{
my: 4,
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
}}
>
<Typography variant="h4" component="h1" gutterBottom>
{t('a')}
</Typography>
</Box>
</Container>
<FileDropzone
multiple={false}
title={t<string>('fileDropzone.title')}
buttonText={t<string>('fileDropzone.buttonText')}
onChange={onChange}
/>
);
};

export const getServerSideProps: GetServerSideProps<IndexProps> = async ({
req,
locale,
}) => {
const token = await getToken({ req });
setAuthToken(token?.accessToken!);
const { data: files } = await file.getFiles();
return {
props: {
Expand Down
12 changes: 9 additions & 3 deletions src/shared/api/base.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
import axios from 'axios';
import { getSession } from 'next-auth/react';

export const api = axios.create({
baseURL: process.env.NEXT_PUBLIC_API_URL,
headers: { 'Content-Type': 'application/json' },
});

export const setAuthToken = (accessToken: string) => {
api.defaults.headers.common.Authorization = `Bearer ${accessToken}`;
};
api.interceptors.request.use(async (request) => {
// TODO: it`s not good calling async operation on every request
const session = await getSession();
if (session) {
request.headers.Authorization = `Bearer ${session.accessToken}`;
}
return request;
});
15 changes: 14 additions & 1 deletion src/shared/api/file.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,16 @@
import { api } from './base';

export const getFiles = () => api.get('/files');
const BASE_PATH = '/files';

export const getFiles = () => api.get(BASE_PATH);

export const uploadFile = (file: File) => {
const formData = new FormData();
formData.append('file', file);

return api.post(`${BASE_PATH}/upload`, formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
});
};
5 changes: 4 additions & 1 deletion src/shared/lib/i18n/locales/en/common.json
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
{
"a": "This is a",
"login": "Login",
"logout": "Logout",
"menu": {
"files": "Files",
"favorites": "Favorites"
},
"fileDropzone": {
"title": "Select files or drag them here",
"buttonText": "Upload files"
}
}
5 changes: 4 additions & 1 deletion src/shared/lib/i18n/locales/ru/common.json
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
{
"a": "Это а",
"login": "Войти",
"logout": "Выйти",
"menu": {
"files": "Файлы",
"favorites": "Избранное"
},
"fileDropzone": {
"title": "Выберите файлы или перетащите их сюда",
"buttonText": "Загрузить"
}
}
2 changes: 1 addition & 1 deletion src/widgets/header/ui/Profile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export const Profile = ({ user }: ProfileProps) => {
<>
<Tooltip title={user?.name}>
<IconButton sx={{ p: 0 }} onClick={handleOpenUserMenu}>
<Avatar src={user?.image ?? ''} />
<Avatar src={user?.image ?? ''} alt={user.name ?? ''} />
</IconButton>
</Tooltip>
<UserMenu anchorEl={anchorElUser} onClose={handleCloseUserMenu} />
Expand Down
13 changes: 2 additions & 11 deletions src/widgets/layout/ui/Layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { Header } from '@/widgets/header';
import { MainMenu } from '@/widgets/mainMenu';
import { PropsWithChildren, useState } from 'react';
import { styled } from '@mui/material/styles';
import { Box } from '@mui/material';
import { Container } from '@mui/material';

const menuWidth = 280;

Expand All @@ -25,16 +25,7 @@ export const Layout = ({ children }: PropsWithChildren<LayoutProps>) => {
<>
<Header menuWidth={menuWidth} onMenuOpen={() => setIsMenuOpen(true)} />
<LayoutRoot>
<Box
sx={{
display: 'flex',
flex: '1 1 auto',
flexDirection: 'column',
width: '100%',
}}
>
{children}
</Box>
<Container maxWidth="lg">{children}</Container>
</LayoutRoot>
<MainMenu
menuWidth={menuWidth}
Expand Down
21 changes: 21 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -690,6 +690,11 @@ asynckit@^0.4.0:
resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"
integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==

attr-accept@^2.2.2:
version "2.2.2"
resolved "https://registry.yarnpkg.com/attr-accept/-/attr-accept-2.2.2.tgz#646613809660110749e92f2c10833b70968d929b"
integrity sha512-7prDjvt9HmqiZ0cl5CRjtS84sEyhsHP2coDkaZKRKVfCDo9s7iw7ChVmar78Gu9pC4SoR/28wFu/G5JJhTnqEg==

available-typed-arrays@^1.0.5:
version "1.0.5"
resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz#92f95616501069d07d10edb2fc37d3e1c65123b7"
Expand Down Expand Up @@ -1410,6 +1415,13 @@ file-entry-cache@^6.0.1:
dependencies:
flat-cache "^3.0.4"

file-selector@^0.6.0:
version "0.6.0"
resolved "https://registry.yarnpkg.com/file-selector/-/file-selector-0.6.0.tgz#fa0a8d9007b829504db4d07dd4de0310b65287dc"
integrity sha512-QlZ5yJC0VxHxQQsQhXvBaC7VRJ2uaxTf+Tfpu4Z/OcVQJVpZO+DGU0rkoVW5ce2SccxugvpBJoMvUs59iILYdw==
dependencies:
tslib "^2.4.0"

fill-range@^7.0.1:
version "7.0.1"
resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40"
Expand Down Expand Up @@ -2553,6 +2565,15 @@ [email protected]:
loose-envify "^1.1.0"
scheduler "^0.23.0"

react-dropzone@^14.2.3:
version "14.2.3"
resolved "https://registry.yarnpkg.com/react-dropzone/-/react-dropzone-14.2.3.tgz#0acab68308fda2d54d1273a1e626264e13d4e84b"
integrity sha512-O3om8I+PkFKbxCukfIR3QAGftYXDZfOE2N1mr/7qebQJHs7U+/RSL/9xomJNpRg9kM5h9soQSdf0Gc7OHF5Fug==
dependencies:
attr-accept "^2.2.2"
file-selector "^0.6.0"
prop-types "^15.8.1"

react-i18next@^12.1.5:
version "12.1.5"
resolved "https://registry.yarnpkg.com/react-i18next/-/react-i18next-12.1.5.tgz#b65f5733dd2f96188a9359c009b7dbe27443f009"
Expand Down