feat: Change layout, add pages
This commit is contained in:
parent
1c972f9a33
commit
566d8be8d9
@ -16,5 +16,7 @@ module.exports = {
|
||||
'warn',
|
||||
{ allowConstantExport: true },
|
||||
],
|
||||
'react/jsx-props-no-spreading': 'off',
|
||||
'react/prop-types': 'off',
|
||||
},
|
||||
}
|
||||
};
|
||||
|
128
package-lock.json
generated
128
package-lock.json
generated
@ -12,10 +12,13 @@
|
||||
"axios": "^1.5.0",
|
||||
"chart.js": "^4.4.0",
|
||||
"react": "^18.2.0",
|
||||
"react-chartjs-2": "^5.2.0",
|
||||
"react-cookie": "^6.1.1",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-query": "^3.39.3",
|
||||
"react-router-dom": "^6.16.0",
|
||||
"styled-components": "^6.0.8"
|
||||
"styled-components": "^6.0.8",
|
||||
"zustand": "^4.4.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.2.15",
|
||||
@ -2241,6 +2244,17 @@
|
||||
"integrity": "sha512-xVRaR4u9hcYjFvcSg71Lz5Bo4//CyjAAfMxa7UsaDSYxAshflUkVJWiyVWrfxC59z2kP1IzI4/1BEpnhI9o3Mw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@swc/helpers": {
|
||||
"version": "0.5.2",
|
||||
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.2.tgz",
|
||||
"integrity": "sha512-E4KcWTpoLHqwPHLxidpOqQbcrZVgi0rsmmZXUle1jXmJfuIf/UWpczUJ7MZZ5tlxytgJXyp0w4PGkkeLiuIdZw==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/types": {
|
||||
"version": "0.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.5.tgz",
|
||||
@ -2267,17 +2281,29 @@
|
||||
"date-fns": "^2.25.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/cookie": {
|
||||
"version": "0.5.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.5.2.tgz",
|
||||
"integrity": "sha512-DBpRoJGKJZn7RY92dPrgoMew8xCWc2P71beqsjyhEI/Ds9mOyVmBwtekyfhpwFIVt1WrxTonFifiOZ62V8CnNA=="
|
||||
},
|
||||
"node_modules/@types/hoist-non-react-statics": {
|
||||
"version": "3.3.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz",
|
||||
"integrity": "sha512-YIQtIg4PKr7ZyqNPZObpxfHsHEmuB8dXCxd6qVcGuQVDK2bpsF7bYNnBJ4Nn7giuACZg+WewExgrtAJ3XnA4Xw==",
|
||||
"dependencies": {
|
||||
"@types/react": "*",
|
||||
"hoist-non-react-statics": "^3.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/prop-types": {
|
||||
"version": "15.7.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.7.tgz",
|
||||
"integrity": "sha512-FbtmBWCcSa2J4zL781Zf1p5YUBXQomPEcep9QZCfRfQgTxz3pJWiDFLebohZ9fFntX5ibzOkSsrJ0TEew8cAog==",
|
||||
"dev": true
|
||||
"integrity": "sha512-FbtmBWCcSa2J4zL781Zf1p5YUBXQomPEcep9QZCfRfQgTxz3pJWiDFLebohZ9fFntX5ibzOkSsrJ0TEew8cAog=="
|
||||
},
|
||||
"node_modules/@types/react": {
|
||||
"version": "18.2.22",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.22.tgz",
|
||||
"integrity": "sha512-60fLTOLqzarLED2O3UQImc/lsNRgG0jE/a1mPW9KjMemY0LMITWEsbS4VvZ4p6rorEHd5YKxxmMKSDK505GHpA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/prop-types": "*",
|
||||
"@types/scheduler": "*",
|
||||
@ -2296,8 +2322,7 @@
|
||||
"node_modules/@types/scheduler": {
|
||||
"version": "0.16.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.4.tgz",
|
||||
"integrity": "sha512-2L9ifAGl7wmXwP4v3pN4p2FLhD0O1qsJpvKmNin5VA8+UvNVb447UDaAEV6UdrkA+m/Xs58U1RFps44x6TFsVQ==",
|
||||
"dev": true
|
||||
"integrity": "sha512-2L9ifAGl7wmXwP4v3pN4p2FLhD0O1qsJpvKmNin5VA8+UvNVb447UDaAEV6UdrkA+m/Xs58U1RFps44x6TFsVQ=="
|
||||
},
|
||||
"node_modules/@types/stylis": {
|
||||
"version": "4.2.1",
|
||||
@ -2821,6 +2846,14 @@
|
||||
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
|
||||
"integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="
|
||||
},
|
||||
"node_modules/cookie": {
|
||||
"version": "0.5.0",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz",
|
||||
"integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/core-js-compat": {
|
||||
"version": "3.32.2",
|
||||
"resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.32.2.tgz",
|
||||
@ -3724,6 +3757,14 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/hoist-non-react-statics": {
|
||||
"version": "3.3.2",
|
||||
"resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz",
|
||||
"integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==",
|
||||
"dependencies": {
|
||||
"react-is": "^16.7.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ignore": {
|
||||
"version": "5.2.4",
|
||||
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz",
|
||||
@ -4629,9 +4670,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/postcss": {
|
||||
"version": "8.4.30",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.30.tgz",
|
||||
"integrity": "sha512-7ZEao1g4kd68l97aWG/etQKPKq07us0ieSZ2TnFDk11i0ZfDW2AwKHYU8qv4MZKqN2fdBfg+7q0ES06UA73C1g==",
|
||||
"version": "8.4.31",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",
|
||||
"integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
@ -4725,6 +4766,28 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-chartjs-2": {
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/react-chartjs-2/-/react-chartjs-2-5.2.0.tgz",
|
||||
"integrity": "sha512-98iN5aguJyVSxp5U3CblRLH67J8gkfyGNbiK3c+l1QI/G4irHMPQw44aEPmjVag+YKTyQ260NcF82GTQ3bdscA==",
|
||||
"peerDependencies": {
|
||||
"chart.js": "^4.1.1",
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-cookie": {
|
||||
"version": "6.1.1",
|
||||
"resolved": "https://registry.npmjs.org/react-cookie/-/react-cookie-6.1.1.tgz",
|
||||
"integrity": "sha512-fuFRpf8LH6SfmVMowDUIRywJF5jAUDUWrm0EI5VdXfTl5bPcJ7B0zWbuYpT0Tvikx7Gs18MlvAT+P+744dUz2g==",
|
||||
"dependencies": {
|
||||
"@types/hoist-non-react-statics": "^3.3.1",
|
||||
"hoist-non-react-statics": "^3.3.2",
|
||||
"universal-cookie": "^6.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">= 16.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-dom": {
|
||||
"version": "18.2.0",
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz",
|
||||
@ -4740,8 +4803,7 @@
|
||||
"node_modules/react-is": {
|
||||
"version": "16.13.1",
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
||||
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
|
||||
"dev": true
|
||||
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
|
||||
},
|
||||
"node_modules/react-query": {
|
||||
"version": "3.39.3",
|
||||
@ -5454,6 +5516,15 @@
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/universal-cookie": {
|
||||
"version": "6.1.1",
|
||||
"resolved": "https://registry.npmjs.org/universal-cookie/-/universal-cookie-6.1.1.tgz",
|
||||
"integrity": "sha512-33S9x3CpdUnnjwTNs2Fgc41WGve2tdLtvaK2kPSbZRc5pGpz2vQFbRWMxlATsxNNe/Cy8SzmnmbuBM85jpZPtA==",
|
||||
"dependencies": {
|
||||
"@types/cookie": "^0.5.1",
|
||||
"cookie": "^0.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/unload": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/unload/-/unload-2.2.0.tgz",
|
||||
@ -5501,6 +5572,14 @@
|
||||
"punycode": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/use-sync-external-store": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz",
|
||||
"integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==",
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "4.4.9",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-4.4.9.tgz",
|
||||
@ -5668,6 +5747,33 @@
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/zustand": {
|
||||
"version": "4.4.1",
|
||||
"resolved": "https://registry.npmjs.org/zustand/-/zustand-4.4.1.tgz",
|
||||
"integrity": "sha512-QCPfstAS4EBiTQzlaGP1gmorkh/UL1Leaj2tdj+zZCZ/9bm0WS7sI2wnfD5lpOszFqWJ1DcPnGoY8RDL61uokw==",
|
||||
"dependencies": {
|
||||
"use-sync-external-store": "1.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.7.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": ">=16.8",
|
||||
"immer": ">=9.0",
|
||||
"react": ">=16.8"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"immer": {
|
||||
"optional": true
|
||||
},
|
||||
"react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -14,10 +14,13 @@
|
||||
"axios": "^1.5.0",
|
||||
"chart.js": "^4.4.0",
|
||||
"react": "^18.2.0",
|
||||
"react-chartjs-2": "^5.2.0",
|
||||
"react-cookie": "^6.1.1",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-query": "^3.39.3",
|
||||
"react-router-dom": "^6.16.0",
|
||||
"styled-components": "^6.0.8"
|
||||
"styled-components": "^6.0.8",
|
||||
"zustand": "^4.4.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.2.15",
|
||||
|
39
src/API.js
Normal file
39
src/API.js
Normal file
@ -0,0 +1,39 @@
|
||||
import axios from 'axios';
|
||||
import { Cookies } from 'react-cookie';
|
||||
|
||||
export const API = axios.create({
|
||||
baseURL: import.meta.env.VITE_SERVER_URL ?? '/',
|
||||
withCredentials: true,
|
||||
header: {
|
||||
'Content-Type': 'application/json',
|
||||
cache: 'no-cache',
|
||||
accept: 'application/json',
|
||||
referrer: 'no-referrer',
|
||||
},
|
||||
xsrfCookieName: 'csrftoken',
|
||||
xsrfHeaderName: 'x-csrftoken',
|
||||
timeout: 1000 * 60 * 5,
|
||||
});
|
||||
|
||||
const cookies = new Cookies();
|
||||
|
||||
export const setCookie = (name, value, options) => {
|
||||
return cookies.set(name, value, options);
|
||||
};
|
||||
|
||||
export const getCookie = (name) => {
|
||||
return cookies.get(name);
|
||||
};
|
||||
|
||||
export const URL = {
|
||||
login: '/user/login/',
|
||||
logout: '/user/logout/',
|
||||
myPage: '/user/mypage/',
|
||||
user: '/user/',
|
||||
brand: '/brand/',
|
||||
product: '/product/',
|
||||
post: '/post/',
|
||||
write: '/write',
|
||||
write2: '/newPost',
|
||||
graph: '/graph/g',
|
||||
};
|
13
src/App.jsx
13
src/App.jsx
@ -1,9 +1,20 @@
|
||||
import { QueryClient, QueryClientProvider } from 'react-query';
|
||||
import Router from './Router';
|
||||
import GlobalStyle from './styles/global';
|
||||
import { ThemeProvider } from 'styled-components';
|
||||
import theme from './styles/Themes.styles';
|
||||
|
||||
const queryClient = new QueryClient();
|
||||
|
||||
const App = () => {
|
||||
return <QueryClientProvider client={queryClient}></QueryClientProvider>;
|
||||
return (
|
||||
<ThemeProvider theme={theme}>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<GlobalStyle />
|
||||
<Router />
|
||||
</QueryClientProvider>
|
||||
</ThemeProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export default App;
|
||||
|
80
src/Router.jsx
Normal file
80
src/Router.jsx
Normal file
@ -0,0 +1,80 @@
|
||||
import { BrowserRouter, Route, Routes } from 'react-router-dom';
|
||||
import PageLayout from './components/Layout/PageLayout';
|
||||
import NotFound from './pages/NotFound';
|
||||
import Products from './pages/Products';
|
||||
import ProductDetail from './pages/ProductDetail';
|
||||
import { URL } from './API';
|
||||
import Brand from './pages/Brand';
|
||||
import Posts from './pages/Posts';
|
||||
import PostDetail from './pages/PostDetail';
|
||||
import Write from './pages/Write';
|
||||
import Login from './pages/Login';
|
||||
import Logout from './pages/Logout';
|
||||
import Signup from './pages/Signup';
|
||||
import WriteSteps from './pages/WriteSteps';
|
||||
import CurrencyInput from './components/CurrencyInput';
|
||||
import { useState } from 'react';
|
||||
import MyPage from './pages/MyPage';
|
||||
|
||||
const Router = () => (
|
||||
<BrowserRouter>
|
||||
<Routes>
|
||||
<Route element={<PageLayout />}>
|
||||
<Route
|
||||
path="/"
|
||||
element={<Posts />}
|
||||
/>
|
||||
<Route
|
||||
path="/signup"
|
||||
element={<Signup />}
|
||||
/>
|
||||
<Route
|
||||
path="/login"
|
||||
element={<Login />}
|
||||
/>
|
||||
<Route
|
||||
path="/logout"
|
||||
element={<Logout />}
|
||||
/>
|
||||
<Route
|
||||
path={URL.myPage}
|
||||
element={<MyPage />}
|
||||
/>
|
||||
<Route
|
||||
path={`${URL.brand}/:id`}
|
||||
element={<Brand />}
|
||||
/>
|
||||
<Route
|
||||
path={URL.product}
|
||||
element={<Products />}
|
||||
/>
|
||||
<Route
|
||||
path={`${URL.product}:id`}
|
||||
element={<ProductDetail />}
|
||||
/>
|
||||
<Route
|
||||
path={`${URL.post}`}
|
||||
element={<Posts />}
|
||||
/>
|
||||
<Route
|
||||
path={`${URL.post}:id`}
|
||||
element={<PostDetail />}
|
||||
/>
|
||||
<Route
|
||||
path={URL.write}
|
||||
element={<Write />}
|
||||
/>
|
||||
<Route
|
||||
path={URL.write2}
|
||||
element={<WriteSteps />}
|
||||
/>
|
||||
</Route>
|
||||
<Route
|
||||
path="*"
|
||||
element={<NotFound />}
|
||||
/>
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
);
|
||||
|
||||
export default Router;
|
1
src/assets/logo.svg
Normal file
1
src/assets/logo.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg id="logo-11" width="119" height="30" viewBox="0 0 119 30" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M2.94098 27.825H28.591V2.175H2.94098V27.825ZM23.654 12.532H18.234V7.112H23.654V12.532ZM7.87698 7.112H13.3V17.468H23.654V22.888H7.87698V7.112ZM45.294 2.175C42.7574 2.175 40.2778 2.92718 38.1688 4.33641C36.0597 5.74564 34.4159 7.74863 33.4452 10.0921C32.4745 12.4356 32.2206 15.0142 32.7154 17.502C33.2103 19.9898 34.4317 22.275 36.2253 24.0686C38.0189 25.8623 40.3041 27.0837 42.7919 27.5786C45.2798 28.0734 47.8584 27.8195 50.2019 26.8488C52.5454 25.8781 54.5483 24.2343 55.9576 22.1252C57.3668 20.0161 58.119 17.5365 58.119 15C58.115 11.5998 56.7625 8.34004 54.3582 5.93575C51.9539 3.53145 48.6942 2.17897 45.294 2.175V2.175ZM45.294 22.888C43.7339 22.888 42.2088 22.4254 40.9116 21.5586C39.6145 20.6919 38.6034 19.46 38.0064 18.0186C37.4094 16.5773 37.2532 14.9913 37.5575 13.4611C37.8619 11.931 38.6132 10.5255 39.7163 9.42234C40.8195 8.31919 42.225 7.56793 43.7551 7.26357C45.2852 6.95921 46.8712 7.11542 48.3126 7.71244C49.7539 8.30946 50.9859 9.32049 51.8526 10.6177C52.7194 11.9148 53.182 13.4399 53.182 15C53.1788 17.0911 52.3467 19.0956 50.8681 20.5742C49.3895 22.0528 47.385 22.8848 45.294 22.888V22.888ZM104.013 2.175C101.476 2.17481 98.9967 2.92681 96.8875 4.33592C94.7783 5.74503 93.1344 7.74795 92.1635 10.0914C91.1927 12.4348 90.9385 15.0136 91.4333 17.5014C91.928 19.9893 93.1494 22.2746 94.943 24.0683C96.7365 25.862 99.0217 27.0836 101.51 27.5785C103.997 28.0734 106.576 27.8195 108.92 26.8489C111.263 25.8782 113.266 24.2344 114.675 22.1253C116.085 20.0162 116.837 17.5366 116.837 15C116.833 11.6 115.481 8.34036 113.077 5.9361C110.673 3.53183 107.413 2.17924 104.013 2.175V2.175ZM104.013 22.888C102.453 22.8882 100.928 22.4257 99.6304 21.5591C98.3331 20.6925 97.3219 19.4606 96.7247 18.0193C96.1275 16.578 95.9712 14.9919 96.2754 13.4617C96.5797 11.9316 97.3309 10.526 98.434 9.4227C99.5371 8.31944 100.943 7.56808 102.473 7.26364C104.003 6.9592 105.589 7.11535 107.03 7.71235C108.472 8.30934 109.704 9.32036 110.571 10.6176C111.437 11.9148 111.9 13.4399 111.9 15C111.897 17.0909 111.065 19.0952 109.586 20.5738C108.108 22.0524 106.104 22.8846 104.013 22.888V22.888ZM74.653 2.175C72.1164 2.175 69.6368 2.92718 67.5278 4.33641C65.4187 5.74564 63.7749 7.74863 62.8042 10.0921C61.8335 12.4356 61.5796 15.0142 62.0744 17.502C62.5693 19.9898 63.7907 22.275 65.5843 24.0686C67.3779 25.8623 69.6631 27.0837 72.1509 27.5786C74.6387 28.0734 77.2174 27.8195 79.5609 26.8488C81.9044 25.8781 83.9073 24.2343 85.3166 22.1252C86.7258 20.0161 87.478 17.5365 87.478 15C87.474 11.5998 86.1215 8.34004 83.7172 5.93575C81.3129 3.53145 78.0532 2.17897 74.653 2.175ZM74.653 22.888C73.1994 22.8887 71.774 22.4878 70.5339 21.7295C69.2939 20.9712 68.2874 19.8851 67.6257 18.5909C66.9641 17.2967 66.6728 15.8448 66.7842 14.3956C66.8956 12.9463 67.4052 11.556 68.2569 10.3781C69.1086 9.20026 70.2692 8.28061 71.6105 7.72071C72.9519 7.1608 74.4219 6.98242 75.8582 7.20524C77.2946 7.42807 78.6414 8.04343 79.7501 8.98342C80.8588 9.9234 81.6862 11.1515 82.141 12.532H74.653V17.468H82.141C81.6201 19.0433 80.6165 20.4147 79.2724 21.3875C77.9284 22.3604 76.3122 22.8853 74.653 22.888V22.888Z" class="ccustom" fill="#394149"></path> </svg>
|
After Width: | Height: | Size: 3.2 KiB |
1
src/assets/phone.svg
Normal file
1
src/assets/phone.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="40" viewBox="0 -960 960 960" width="40"><path d="M308.283-93.667q-22.758 0-38.854-16.095-16.096-16.096-16.096-38.823v-662.83q0-22.727 16.096-38.823 16.096-16.095 38.854-16.095h343.434q22.758 0 38.854 16.095 16.096 16.096 16.096 38.823v662.83q0 22.727-16.096 38.823-16.096 16.095-38.854 16.095H308.283ZM276.5-204.333h407v-551.334h-407v551.334Z"/></svg>
|
After Width: | Height: | Size: 399 B |
19
src/components/Brand/BrandProductList.jsx
Normal file
19
src/components/Brand/BrandProductList.jsx
Normal file
@ -0,0 +1,19 @@
|
||||
import { useBrandProduct } from '../../hooks/network/brand';
|
||||
import ProductItem from '../ProductItem';
|
||||
|
||||
const BrandProductList = ({ id }) => {
|
||||
const { data } = useBrandProduct(id);
|
||||
|
||||
return (
|
||||
<>
|
||||
{data?.map((product) => (
|
||||
<ProductItem
|
||||
key={product?.id}
|
||||
product={product}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default BrandProductList;
|
15
src/components/Brand/BrandTitle.jsx
Normal file
15
src/components/Brand/BrandTitle.jsx
Normal file
@ -0,0 +1,15 @@
|
||||
import { Suspense } from 'react';
|
||||
import { useBrand } from '../../hooks/network/brand';
|
||||
import { Title } from '../../styles/Common.styles';
|
||||
|
||||
const BrandTitle = ({ id }) => {
|
||||
const { data } = useBrand(id);
|
||||
|
||||
return (
|
||||
<Title>
|
||||
<Suspense fallback={null}>{data?.name}</Suspense>
|
||||
</Title>
|
||||
);
|
||||
};
|
||||
|
||||
export default BrandTitle;
|
27
src/components/Button/index.jsx
Normal file
27
src/components/Button/index.jsx
Normal file
@ -0,0 +1,27 @@
|
||||
import * as S from '../../styles/Button.styles';
|
||||
|
||||
function Button({
|
||||
children,
|
||||
onClick,
|
||||
className,
|
||||
disabled,
|
||||
secondary,
|
||||
type = 'button',
|
||||
...rest
|
||||
}) {
|
||||
const ButtonComponent = secondary ? S.Button.secondary : S.Button.primary;
|
||||
|
||||
return (
|
||||
<ButtonComponent
|
||||
type={type}
|
||||
className={className}
|
||||
onClick={onClick}
|
||||
disabled={disabled}
|
||||
{...rest}
|
||||
>
|
||||
{children}
|
||||
</ButtonComponent>
|
||||
);
|
||||
}
|
||||
|
||||
export default Button;
|
11
src/components/ButtonArea/index.jsx
Normal file
11
src/components/ButtonArea/index.jsx
Normal file
@ -0,0 +1,11 @@
|
||||
import * as S from '../../styles/ButtonArea.styles';
|
||||
|
||||
function ButtonArea({ children }) {
|
||||
return (
|
||||
<S.ButtonArea>
|
||||
<S.ButtonWrapper>{children}</S.ButtonWrapper>
|
||||
</S.ButtonArea>
|
||||
);
|
||||
}
|
||||
|
||||
export default ButtonArea;
|
13
src/components/CSRFToken/CSRFToken.jsx
Normal file
13
src/components/CSRFToken/CSRFToken.jsx
Normal file
@ -0,0 +1,13 @@
|
||||
import { getCookie } from '../../API';
|
||||
|
||||
const CSRFToken = () => {
|
||||
return (
|
||||
<input
|
||||
type="hidden"
|
||||
name="csrfmiddlewaretoken"
|
||||
value={getCookie('csrftoken')}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default CSRFToken;
|
21
src/components/CheckBox/index.jsx
Normal file
21
src/components/CheckBox/index.jsx
Normal file
@ -0,0 +1,21 @@
|
||||
import * as S from '../../styles/CheckBox.styles';
|
||||
|
||||
function CheckBox({ name, id, text, checked, setChecked }) {
|
||||
return (
|
||||
<S.CheckBox htmlFor={id}>
|
||||
<S.CheckInput
|
||||
type="checkbox"
|
||||
name={name}
|
||||
id={id}
|
||||
checked={checked}
|
||||
onChange={() => setChecked(!checked)}
|
||||
/>
|
||||
<S.Info>
|
||||
<S.Name>{name}</S.Name>
|
||||
<S.Text>{text}</S.Text>
|
||||
</S.Info>
|
||||
</S.CheckBox>
|
||||
);
|
||||
}
|
||||
|
||||
export default CheckBox;
|
36
src/components/CurrencyInput/index.jsx
Normal file
36
src/components/CurrencyInput/index.jsx
Normal file
@ -0,0 +1,36 @@
|
||||
import * as S from '../../styles/CurrencyInput.styles';
|
||||
|
||||
function CurrencyInput({ value, setValue }) {
|
||||
const getRenderValue = () => {
|
||||
if (value === '') {
|
||||
return '';
|
||||
}
|
||||
const numberValue = Number(value);
|
||||
return `${numberValue.toLocaleString()}원`;
|
||||
};
|
||||
const handleOnChange = (event) => {
|
||||
if (event.nativeEvent.inputType === 'deleteContentBackward') {
|
||||
const newValue = value.slice(0, -1);
|
||||
newValue === 0 ? setValue('') : setValue(newValue);
|
||||
return;
|
||||
}
|
||||
const { value: newValue } = event.target;
|
||||
const onlyNumber = newValue.replace(/[^0-9]/g, '');
|
||||
if (onlyNumber > 9999999) {
|
||||
return;
|
||||
}
|
||||
onlyNumber === '0' ? setValue('') : setValue(onlyNumber);
|
||||
};
|
||||
|
||||
return (
|
||||
<S.CurrencyInput>
|
||||
<S.CurrencyInputField
|
||||
value={getRenderValue()}
|
||||
onChange={handleOnChange}
|
||||
placeholder="가격을 입력해주세요"
|
||||
/>
|
||||
</S.CurrencyInput>
|
||||
);
|
||||
}
|
||||
|
||||
export default CurrencyInput;
|
86
src/components/Graph/ProductPriceGraph.jsx
Normal file
86
src/components/Graph/ProductPriceGraph.jsx
Normal file
@ -0,0 +1,86 @@
|
||||
import * as S from '../../styles/ProductPriceGraph.styles';
|
||||
import {
|
||||
Chart as ChartJS,
|
||||
CategoryScale,
|
||||
LinearScale,
|
||||
PointElement,
|
||||
LineElement,
|
||||
// Title,
|
||||
Tooltip,
|
||||
// Legend,
|
||||
} from 'chart.js';
|
||||
import { Line } from 'react-chartjs-2';
|
||||
import { useGraph } from '../../hooks/network/graph';
|
||||
import theme from '../../styles/Themes.styles';
|
||||
|
||||
ChartJS.register(
|
||||
CategoryScale,
|
||||
LinearScale,
|
||||
PointElement,
|
||||
LineElement,
|
||||
// Title,
|
||||
Tooltip
|
||||
);
|
||||
|
||||
const options = {
|
||||
maintainAspectRatio: false,
|
||||
responsive: true,
|
||||
scales: {
|
||||
y: {
|
||||
ticks: {
|
||||
stepSize: 50000,
|
||||
},
|
||||
display: false,
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
tooltip: {
|
||||
padding: 10,
|
||||
backgroundColor: 'white',
|
||||
titleColor: theme.colors.gray900,
|
||||
bodyColor: theme.colors.gray900,
|
||||
borderWidth: 1,
|
||||
borderColor: theme.colors.primary,
|
||||
callbacks: {
|
||||
label: (context) => `${context.dataset.data[context.dataIndex]}원`,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const ProductPriceGraph = ({ id }) => {
|
||||
const { data: graphData } = useGraph(id);
|
||||
const primaryColor = theme.colors.primary;
|
||||
const graphStyleData = {
|
||||
labels: graphData?.map((data) => `${data.year}.${data.month}`),
|
||||
datasets: [
|
||||
{
|
||||
label: '가격',
|
||||
data: graphData?.map((data) => data.price),
|
||||
backgroundColor: primaryColor,
|
||||
borderColor: primaryColor,
|
||||
cubicInterpolationMode: 'monotone',
|
||||
},
|
||||
],
|
||||
};
|
||||
const minPrice = graphData?.reduce((min, data) => {
|
||||
if (min > data.price) {
|
||||
return data.price;
|
||||
}
|
||||
return min;
|
||||
}, graphData[0]?.price);
|
||||
|
||||
return (
|
||||
<S.ProductPriceGraph className="priceGraph">
|
||||
<S.GraphArea>
|
||||
<Line
|
||||
options={options}
|
||||
data={graphStyleData}
|
||||
></Line>
|
||||
</S.GraphArea>
|
||||
<S.Text>최근 6개월 최저가: {minPrice}원</S.Text>
|
||||
</S.ProductPriceGraph>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProductPriceGraph;
|
33
src/components/ImageViewer/index.jsx
Normal file
33
src/components/ImageViewer/index.jsx
Normal file
@ -0,0 +1,33 @@
|
||||
import { useState } from 'react';
|
||||
import * as S from '../../styles/ImageViewer.style';
|
||||
import startViewTransition from '../../methods/startViewTransition';
|
||||
import { SubTitle } from '../../styles/Common.styles';
|
||||
|
||||
function ImageViewer({ images, title }) {
|
||||
const [currentImage, setCurrentImage] = useState(images[0]);
|
||||
const handleClick = (image) => () => {
|
||||
startViewTransition(() => setCurrentImage(image));
|
||||
};
|
||||
|
||||
return (
|
||||
<S.ImageViewer>
|
||||
{title && <SubTitle>{title}</SubTitle>}
|
||||
<S.MainImage
|
||||
src={currentImage}
|
||||
alt="이미지"
|
||||
/>
|
||||
<S.Images>
|
||||
{images.map((image) => (
|
||||
<img
|
||||
key={image}
|
||||
src={image}
|
||||
alt="이미지"
|
||||
onClick={handleClick(image)}
|
||||
/>
|
||||
))}
|
||||
</S.Images>
|
||||
</S.ImageViewer>
|
||||
);
|
||||
}
|
||||
|
||||
export default ImageViewer;
|
24
src/components/Layout/AuthMenu.jsx
Normal file
24
src/components/Layout/AuthMenu.jsx
Normal file
@ -0,0 +1,24 @@
|
||||
import * as S from '../../styles/Layout.styles';
|
||||
import { URL } from '../../API';
|
||||
import useIsLogged from '../../hooks/network/isLogged';
|
||||
|
||||
const AuthMenu = () => {
|
||||
const isLogged = useIsLogged((state) => state.isLogged);
|
||||
|
||||
if (!isLogged) {
|
||||
return (
|
||||
<div className="right">
|
||||
<S.NavItem to={'/login'}>로그인</S.NavItem>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="right">
|
||||
<S.NavItem to={URL.write}>글쓰기</S.NavItem>
|
||||
<S.NavItem to={URL.myPage}>마이페이지</S.NavItem>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AuthMenu;
|
13
src/components/Layout/Footer.jsx
Normal file
13
src/components/Layout/Footer.jsx
Normal file
@ -0,0 +1,13 @@
|
||||
import * as S from '../../styles/Layout.styles';
|
||||
|
||||
const Footer = () => {
|
||||
return (
|
||||
<S.Footer>
|
||||
<S.FooterWrapper>
|
||||
<p>© {new Date().getFullYear()} App. All rights reserved.</p>
|
||||
</S.FooterWrapper>
|
||||
</S.Footer>
|
||||
);
|
||||
};
|
||||
|
||||
export default Footer;
|
28
src/components/Layout/Header.jsx
Normal file
28
src/components/Layout/Header.jsx
Normal file
@ -0,0 +1,28 @@
|
||||
import * as S from '../../styles/Layout.styles';
|
||||
import { URL } from '../../API';
|
||||
import LogoImage from '../../assets/logo.svg';
|
||||
import AuthMenu from './AuthMenu';
|
||||
|
||||
function Header() {
|
||||
return (
|
||||
<S.Header>
|
||||
<div className="content">
|
||||
<S.Logo to="/">
|
||||
<img
|
||||
src={LogoImage}
|
||||
alt=""
|
||||
/>
|
||||
</S.Logo>
|
||||
<S.Nav>
|
||||
<div className="left">
|
||||
<S.NavItem to={URL.product}>기기 목록</S.NavItem>
|
||||
<S.NavItem to={URL.post}>최신글</S.NavItem>
|
||||
</div>
|
||||
<AuthMenu />
|
||||
</S.Nav>
|
||||
</div>
|
||||
</S.Header>
|
||||
);
|
||||
}
|
||||
|
||||
export default Header;
|
22
src/components/Layout/HeaderLinks.jsx
Normal file
22
src/components/Layout/HeaderLinks.jsx
Normal file
@ -0,0 +1,22 @@
|
||||
import * as S from '../../styles/Layout.styles';
|
||||
import { URL } from '../../API';
|
||||
import { useBrands } from '../../hooks/network/brand';
|
||||
|
||||
const HeaderLinks = () => {
|
||||
const { data } = useBrands();
|
||||
|
||||
return (
|
||||
<>
|
||||
{data?.map((brand) => (
|
||||
<S.NavItem
|
||||
key={brand?.id}
|
||||
to={`${URL.brand}/${brand?.id}`}
|
||||
>
|
||||
<div>{brand?.name}</div>
|
||||
</S.NavItem>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default HeaderLinks;
|
5
src/components/Layout/Nav.jsx
Normal file
5
src/components/Layout/Nav.jsx
Normal file
@ -0,0 +1,5 @@
|
||||
function Nav() {
|
||||
return ( );
|
||||
}
|
||||
|
||||
export default Nav;
|
24
src/components/Layout/PageLayout.jsx
Normal file
24
src/components/Layout/PageLayout.jsx
Normal file
@ -0,0 +1,24 @@
|
||||
import * as S from '../../styles/Layout.styles';
|
||||
import { Outlet } from 'react-router';
|
||||
import Header from './Header';
|
||||
import Footer from './Footer';
|
||||
import { ErrorBoundary } from '@toss/error-boundary';
|
||||
import { Suspense } from 'react';
|
||||
|
||||
const PageLayout = () => {
|
||||
return (
|
||||
<>
|
||||
<Header />
|
||||
<S.Main>
|
||||
<ErrorBoundary renderFallback={() => <></>}>
|
||||
<Suspense fallback={<></>}>
|
||||
<Outlet />
|
||||
</Suspense>
|
||||
</ErrorBoundary>
|
||||
</S.Main>
|
||||
<Footer />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default PageLayout;
|
10
src/components/Loading/index.jsx
Normal file
10
src/components/Loading/index.jsx
Normal file
@ -0,0 +1,10 @@
|
||||
const Loading = () => {
|
||||
return (
|
||||
<div className="loading">
|
||||
<div className="loading__spinner"></div>
|
||||
<div className="loading__text">Loading...</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Loading;
|
72
src/components/Login/LoginForm.jsx
Normal file
72
src/components/Login/LoginForm.jsx
Normal file
@ -0,0 +1,72 @@
|
||||
import { API, URL, getCookie } from '../../API';
|
||||
import * as S from '../../styles/Form.styles';
|
||||
import { useState } from 'react';
|
||||
import CSRFToken from '../CSRFToken/CSRFToken';
|
||||
import { Navigate, useNavigate } from 'react-router-dom';
|
||||
import useIsLogged from '../../hooks/network/isLogged';
|
||||
|
||||
const LoginForm = () => {
|
||||
const navigate = useNavigate();
|
||||
const setIsLogged = useIsLogged((state) => state.setIsLogged);
|
||||
const [username, setUsername] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const onSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
API.post(URL.login, {
|
||||
username,
|
||||
password,
|
||||
})
|
||||
.then((res) => {
|
||||
if (res.status === 200) {
|
||||
setIsLogged(true);
|
||||
navigate('/');
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<S.Form
|
||||
method="post"
|
||||
onSubmit={onSubmit}
|
||||
>
|
||||
<CSRFToken />
|
||||
<div className="formInput">
|
||||
<div className="helpText">아이디</div>
|
||||
<input
|
||||
className="input"
|
||||
type="text"
|
||||
placeholder="아이디"
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="formInput">
|
||||
<div className="helpText">비밀번호</div>
|
||||
<input
|
||||
className="input"
|
||||
type="password"
|
||||
placeholder="비밀번호"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<S.Buttons>
|
||||
<input
|
||||
type="button"
|
||||
value="회원가입"
|
||||
onClick={() => navigate('/signup')}
|
||||
/>
|
||||
<input
|
||||
type="submit"
|
||||
value="로그인"
|
||||
className="submitButton"
|
||||
/>
|
||||
</S.Buttons>
|
||||
</S.Form>
|
||||
);
|
||||
};
|
||||
|
||||
export default LoginForm;
|
15
src/components/PanelLayout/index.jsx
Normal file
15
src/components/PanelLayout/index.jsx
Normal file
@ -0,0 +1,15 @@
|
||||
import * as S from '../../styles/PanelLayout.styles';
|
||||
|
||||
function PanelLayout({ reverse = false, children }) {
|
||||
return <S.PanelLayout $reverse={reverse}>{children}</S.PanelLayout>;
|
||||
}
|
||||
|
||||
export function LeftPanel({ children }) {
|
||||
return <S.LeftPanel>{children}</S.LeftPanel>;
|
||||
}
|
||||
|
||||
export function RightPanel({ children }) {
|
||||
return <S.RightPanel>{children}</S.RightPanel>;
|
||||
}
|
||||
|
||||
export default PanelLayout;
|
19
src/components/Post/PostList.jsx
Normal file
19
src/components/Post/PostList.jsx
Normal file
@ -0,0 +1,19 @@
|
||||
import { usePosts } from '../../hooks/network/post';
|
||||
import PostItem from '../PostItem';
|
||||
|
||||
const PostList = ({ page }) => {
|
||||
const { data } = usePosts({ page });
|
||||
|
||||
return (
|
||||
<>
|
||||
{data?.results?.map((post) => (
|
||||
<PostItem
|
||||
key={post?.id}
|
||||
post={post}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default PostList;
|
25
src/components/PostItem/index.jsx
Normal file
25
src/components/PostItem/index.jsx
Normal file
@ -0,0 +1,25 @@
|
||||
import PhoneImage from '../../assets/phone.svg';
|
||||
import { URL } from '../../API';
|
||||
import * as S from '../../styles/PostItem.styles';
|
||||
|
||||
const PostItem = ({ post }) => {
|
||||
const image = post?.image || PhoneImage;
|
||||
|
||||
return (
|
||||
<S.PostItem to={`${URL.post}${post?.id}`}>
|
||||
<S.PostImage>
|
||||
<img
|
||||
src={image}
|
||||
alt={post?.product?.name}
|
||||
/>
|
||||
</S.PostImage>
|
||||
<S.ProductInfo>
|
||||
<S.ProductName>{post?.product?.name}</S.ProductName>
|
||||
<S.ProductDate>{post?.written_at}</S.ProductDate>
|
||||
<S.ProductPrice>{post?.price}원</S.ProductPrice>
|
||||
</S.ProductInfo>
|
||||
</S.PostItem>
|
||||
);
|
||||
};
|
||||
|
||||
export default PostItem;
|
14
src/components/ProductItem/index.jsx
Normal file
14
src/components/ProductItem/index.jsx
Normal file
@ -0,0 +1,14 @@
|
||||
import { URL } from '../../API';
|
||||
import * as S from '../../styles/Products.styles';
|
||||
|
||||
const ProductItem = ({ product }) => {
|
||||
const { id, name } = product;
|
||||
|
||||
return (
|
||||
<S.ProductItem to={`${URL.product}${id}`}>
|
||||
<div>{name}</div>
|
||||
</S.ProductItem>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProductItem;
|
19
src/components/ProductList/index.jsx
Normal file
19
src/components/ProductList/index.jsx
Normal file
@ -0,0 +1,19 @@
|
||||
import { useProducts } from '../../hooks/network/product';
|
||||
import ProductItem from '../ProductItem';
|
||||
|
||||
const ProductList = () => {
|
||||
const { data } = useProducts();
|
||||
|
||||
return (
|
||||
<>
|
||||
{data?.map((product) => (
|
||||
<ProductItem
|
||||
key={product?.id}
|
||||
product={product}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProductList;
|
19
src/components/ProductPostList/index.jsx
Normal file
19
src/components/ProductPostList/index.jsx
Normal file
@ -0,0 +1,19 @@
|
||||
import { useProductPost } from '../../hooks/network/product';
|
||||
import PostItem from '../PostItem';
|
||||
|
||||
const ProductPostList = ({ modelId, page }) => {
|
||||
const { data } = useProductPost({ id: modelId, page });
|
||||
|
||||
return (
|
||||
<>
|
||||
{data?.results?.map((post) => (
|
||||
<PostItem
|
||||
key={post?.id}
|
||||
post={post}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProductPostList;
|
89
src/components/Signup/SignupForm.jsx
Normal file
89
src/components/Signup/SignupForm.jsx
Normal file
@ -0,0 +1,89 @@
|
||||
import * as S from '../../styles/Form.styles';
|
||||
import { useState } from 'react';
|
||||
import CSRFToken from '../CSRFToken/CSRFToken';
|
||||
import { API, URL } from '../../API';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import useIsLogged from '../../hooks/network/isLogged';
|
||||
|
||||
const SignupForm = () => {
|
||||
const navigate = useNavigate();
|
||||
const setIsLogged = useIsLogged((state) => state.setIsLogged);
|
||||
const [username, setUsername] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [password2, setPassword2] = useState('');
|
||||
const [nickname, setNickname] = useState('');
|
||||
const onSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
if (password !== password2) return;
|
||||
API.post(URL.user, {
|
||||
username,
|
||||
password,
|
||||
nickname,
|
||||
})
|
||||
.then((res) => {
|
||||
if (res.status === 201) {
|
||||
setIsLogged(true);
|
||||
navigate('/');
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<S.Form
|
||||
method="post"
|
||||
onSubmit={onSubmit}
|
||||
>
|
||||
<CSRFToken />
|
||||
<div className="formInput">
|
||||
<div className="helpText">아이디</div>
|
||||
<input
|
||||
className="input"
|
||||
type="text"
|
||||
placeholder="아이디"
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="formInput">
|
||||
<div className="helpText">비밀번호</div>
|
||||
<input
|
||||
className="input"
|
||||
type="password"
|
||||
placeholder="비밀번호"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="formInput">
|
||||
<div className="helpText">비밀번호 확인</div>
|
||||
<input
|
||||
className="input"
|
||||
type="password"
|
||||
placeholder="비밀번호 확인"
|
||||
value={password2}
|
||||
onChange={(e) => setPassword2(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="formInput">
|
||||
<div className="helpText">닉네임</div>
|
||||
<input
|
||||
className="input"
|
||||
type="text"
|
||||
placeholder="닉네임"
|
||||
value={nickname}
|
||||
onChange={(e) => setNickname(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<input
|
||||
type="submit"
|
||||
value="회원가입"
|
||||
className="submitButton"
|
||||
/>
|
||||
</S.Form>
|
||||
);
|
||||
};
|
||||
|
||||
export default SignupForm;
|
7
src/components/Title/index.jsx
Normal file
7
src/components/Title/index.jsx
Normal file
@ -0,0 +1,7 @@
|
||||
import * as S from '../../styles/Common.styles';
|
||||
|
||||
function Title({ children }) {
|
||||
return <S.Title>{children}</S.Title>;
|
||||
}
|
||||
|
||||
export default Title;
|
56
src/components/Write/BrandModelSelect.jsx
Normal file
56
src/components/Write/BrandModelSelect.jsx
Normal file
@ -0,0 +1,56 @@
|
||||
import { Suspense, useState } from 'react';
|
||||
import BrandSelect from './BrandSelect';
|
||||
import ModelSelect from './ModelSelect';
|
||||
|
||||
const BrandModelSelect = ({ setFormData }) => {
|
||||
const [models, setModels] = useState([]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="formInput">
|
||||
<label
|
||||
htmlFor="brand"
|
||||
className="helpText"
|
||||
>
|
||||
브랜드
|
||||
</label>
|
||||
<Suspense
|
||||
fallback={
|
||||
<select
|
||||
name="brand"
|
||||
id="brand"
|
||||
className="input"
|
||||
></select>
|
||||
}
|
||||
>
|
||||
<BrandSelect setModels={setModels} />
|
||||
</Suspense>
|
||||
</div>
|
||||
<div className="formInput">
|
||||
<label
|
||||
htmlFor="model"
|
||||
className="helpText"
|
||||
>
|
||||
모델
|
||||
</label>
|
||||
<Suspense
|
||||
fallback={
|
||||
<select
|
||||
name="model"
|
||||
id="model"
|
||||
className="input"
|
||||
disabled
|
||||
></select>
|
||||
}
|
||||
>
|
||||
<ModelSelect
|
||||
models={models}
|
||||
setFormData={setFormData}
|
||||
/>
|
||||
</Suspense>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default BrandModelSelect;
|
41
src/components/Write/BrandSelect.jsx
Normal file
41
src/components/Write/BrandSelect.jsx
Normal file
@ -0,0 +1,41 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useBrands } from '../../hooks/network/brand';
|
||||
|
||||
const BrandSelect = ({ setModels }) => {
|
||||
const [brand, setBrand] = useState('');
|
||||
const allModels = {};
|
||||
const { data } = useBrands();
|
||||
data?.map((brand) => {
|
||||
allModels[brand?.id] = brand?.products;
|
||||
});
|
||||
useEffect(() => {
|
||||
setModels(allModels[brand]);
|
||||
}, [brand]);
|
||||
|
||||
return (
|
||||
<select
|
||||
name="brand"
|
||||
id="brand"
|
||||
className="input"
|
||||
value={brand}
|
||||
onChange={(e) => setBrand(e.target.value)}
|
||||
>
|
||||
<option
|
||||
value={''}
|
||||
disabled
|
||||
>
|
||||
선택
|
||||
</option>
|
||||
{data?.map((brand) => (
|
||||
<option
|
||||
key={brand?.id}
|
||||
value={brand?.id}
|
||||
>
|
||||
{brand?.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
);
|
||||
};
|
||||
|
||||
export default BrandSelect;
|
42
src/components/Write/ModelSelect.jsx
Normal file
42
src/components/Write/ModelSelect.jsx
Normal file
@ -0,0 +1,42 @@
|
||||
import { useState } from 'react';
|
||||
|
||||
const ModelSelect = ({ models, setFormData }) => {
|
||||
const [model, setModel] = useState('');
|
||||
const setModelData = (model) => {
|
||||
setModel(model);
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
product: model,
|
||||
}));
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<select
|
||||
name="model"
|
||||
id="model"
|
||||
className="input"
|
||||
value={model}
|
||||
onChange={(e) => setModelData(e.target.value)}
|
||||
disabled={!models?.length}
|
||||
>
|
||||
<option
|
||||
value={''}
|
||||
disabled
|
||||
>
|
||||
선택
|
||||
</option>
|
||||
{models?.map((model) => (
|
||||
<option
|
||||
key={model?.id}
|
||||
value={model?.id}
|
||||
>
|
||||
{model?.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ModelSelect;
|
30
src/components/Write/PriceInput.jsx
Normal file
30
src/components/Write/PriceInput.jsx
Normal file
@ -0,0 +1,30 @@
|
||||
import { useState } from 'react';
|
||||
|
||||
const PriceInput = ({ setFormData }) => {
|
||||
const [price, setPrice] = useState(0);
|
||||
const setPriceData = (e) => {
|
||||
const price = e.target.value;
|
||||
|
||||
setPrice(price);
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
price,
|
||||
}));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="formInput">
|
||||
<div className="helpText">가격</div>
|
||||
<input
|
||||
type="number"
|
||||
name="price"
|
||||
id="price"
|
||||
className="input"
|
||||
value={price}
|
||||
onChange={setPriceData}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PriceInput;
|
47
src/components/Write/TextInput.jsx
Normal file
47
src/components/Write/TextInput.jsx
Normal file
@ -0,0 +1,47 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
|
||||
const useTextAreaSize = (textAreaRef, value) => {
|
||||
useEffect(() => {
|
||||
if (textAreaRef) {
|
||||
textAreaRef.style.height = '100px';
|
||||
const scrollHeight = textAreaRef.scrollHeight;
|
||||
textAreaRef.style.height = scrollHeight + 'px';
|
||||
}
|
||||
}, [textAreaRef, value]);
|
||||
};
|
||||
|
||||
const TextInput = ({ setFormData }) => {
|
||||
const [text, setText] = useState('');
|
||||
const textRef = useRef();
|
||||
const setTextData = (text) => {
|
||||
setText(text);
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
text,
|
||||
}));
|
||||
};
|
||||
|
||||
useTextAreaSize(textRef.current, text);
|
||||
|
||||
return (
|
||||
<div className="formInput">
|
||||
<label
|
||||
htmlFor="text"
|
||||
className="helpText"
|
||||
>
|
||||
내용
|
||||
</label>
|
||||
<textarea
|
||||
id="text"
|
||||
name="text"
|
||||
className="input"
|
||||
ref={textRef}
|
||||
placeholder="내용을 입력해주세요."
|
||||
value={text}
|
||||
onInput={(e) => setTextData(e.target?.value)}
|
||||
></textarea>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TextInput;
|
45
src/components/Write/WriteForm.jsx
Normal file
45
src/components/Write/WriteForm.jsx
Normal file
@ -0,0 +1,45 @@
|
||||
import * as S from '../../styles/Form.styles';
|
||||
import { useState } from 'react';
|
||||
import BrandModelSelect from './BrandModelSelect';
|
||||
import PriceInput from './PriceInput';
|
||||
import TextInput from './TextInput';
|
||||
import { API, URL, getCookie } from '../../API';
|
||||
import CSRFToken from '../CSRFToken/CSRFToken';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
const WriteForm = () => {
|
||||
const navigate = useNavigate();
|
||||
const [formData, setFormData] = useState({
|
||||
product: 0,
|
||||
price: 0,
|
||||
text: '',
|
||||
});
|
||||
const onSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
API.post(URL.post, formData).then((res) => {
|
||||
navigate(-1);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<S.Form
|
||||
method="post"
|
||||
onSubmit={onSubmit}
|
||||
>
|
||||
<CSRFToken />
|
||||
<BrandModelSelect setFormData={setFormData} />
|
||||
<PriceInput setFormData={setFormData} />
|
||||
<TextInput setFormData={setFormData} />
|
||||
<input
|
||||
type="submit"
|
||||
value="글쓰기"
|
||||
className="submitButton"
|
||||
disabled={
|
||||
formData.product === 0 || formData.price === 0 || formData.text === ''
|
||||
}
|
||||
/>
|
||||
</S.Form>
|
||||
);
|
||||
};
|
||||
|
||||
export default WriteForm;
|
0
src/constants.js
Normal file
0
src/constants.js
Normal file
41
src/hooks/network/brand.js
Normal file
41
src/hooks/network/brand.js
Normal file
@ -0,0 +1,41 @@
|
||||
import { useQuery } from 'react-query';
|
||||
import { API, URL } from '../../API';
|
||||
|
||||
const getBrands = async () => {
|
||||
const { data } = await API.get(`${URL.brand}/`);
|
||||
return data;
|
||||
};
|
||||
|
||||
export const useBrands = () => {
|
||||
const data = useQuery('brands', () => getBrands(), {
|
||||
suspense: true,
|
||||
});
|
||||
|
||||
return data;
|
||||
};
|
||||
|
||||
const getBrand = async (id) => {
|
||||
const { data } = await API.get(`${URL.brand}/${id}`);
|
||||
return data;
|
||||
};
|
||||
|
||||
export const useBrand = (id) => {
|
||||
const data = useQuery(['products', id], () => getBrand(id), {
|
||||
suspense: true,
|
||||
});
|
||||
|
||||
return data;
|
||||
};
|
||||
|
||||
const getBrandProduct = async (id) => {
|
||||
const { data } = await API.get(`${URL.brand}/${id}/product`);
|
||||
return data;
|
||||
};
|
||||
|
||||
export const useBrandProduct = (id) => {
|
||||
const data = useQuery(['brandproduct', id], () => getBrandProduct(id), {
|
||||
suspense: true,
|
||||
});
|
||||
|
||||
return data;
|
||||
};
|
15
src/hooks/network/graph.js
Normal file
15
src/hooks/network/graph.js
Normal file
@ -0,0 +1,15 @@
|
||||
import { useQuery } from 'react-query';
|
||||
import { API, URL } from '../../API';
|
||||
|
||||
const getGraph = async (id) => {
|
||||
const { data } = await API.get(`${URL.graph}/${id}`);
|
||||
return data;
|
||||
};
|
||||
|
||||
export const useGraph = (id) => {
|
||||
const data = useQuery(['post', id], () => getGraph(id), {
|
||||
suspense: true,
|
||||
});
|
||||
|
||||
return data;
|
||||
};
|
9
src/hooks/network/isLogged.js
Normal file
9
src/hooks/network/isLogged.js
Normal file
@ -0,0 +1,9 @@
|
||||
import { create } from 'zustand';
|
||||
import { getCookie } from '../../API';
|
||||
|
||||
const useIsLogged = create((set) => ({
|
||||
isLogged: !!getCookie('sessionid'),
|
||||
setIsLogged: (isLogged) => set({ isLogged }),
|
||||
}));
|
||||
|
||||
export default useIsLogged;
|
29
src/hooks/network/post.js
Normal file
29
src/hooks/network/post.js
Normal file
@ -0,0 +1,29 @@
|
||||
import { API, URL } from '../../API';
|
||||
import { useQuery } from 'react-query';
|
||||
|
||||
const getPosts = async ({ page }) => {
|
||||
const pageString = page ? `?page=${page}` : '';
|
||||
const { data } = await API.get(`${URL.post}${pageString}`);
|
||||
return data;
|
||||
};
|
||||
|
||||
export const usePosts = ({ page }) => {
|
||||
const data = useQuery('posts', () => getPosts({ page }), {
|
||||
suspense: true,
|
||||
});
|
||||
|
||||
return data;
|
||||
};
|
||||
|
||||
const getPost = async (id) => {
|
||||
const { data } = await API.get(`${URL.post}${id}`);
|
||||
return data;
|
||||
};
|
||||
|
||||
export const usePost = (id) => {
|
||||
const data = useQuery(['post', id], () => getPost(id), {
|
||||
suspense: true,
|
||||
});
|
||||
|
||||
return data;
|
||||
};
|
46
src/hooks/network/product.js
Normal file
46
src/hooks/network/product.js
Normal file
@ -0,0 +1,46 @@
|
||||
import { API, URL } from '../../API';
|
||||
import { useQuery } from 'react-query';
|
||||
|
||||
const getProducts = async () => {
|
||||
const { data } = await API.get(URL.product);
|
||||
return data;
|
||||
};
|
||||
|
||||
export const useProducts = () => {
|
||||
const data = useQuery('products', () => getProducts(), {
|
||||
suspense: true,
|
||||
});
|
||||
|
||||
return data;
|
||||
};
|
||||
|
||||
const getProduct = async (id) => {
|
||||
const { data } = await API.get(`${URL.product}${id}`);
|
||||
return data;
|
||||
};
|
||||
|
||||
export const useProduct = (id) => {
|
||||
const data = useQuery(['product', id], () => getProduct(id), {
|
||||
suspense: true,
|
||||
});
|
||||
|
||||
return data;
|
||||
};
|
||||
|
||||
const getProductPost = async ({ id, page }) => {
|
||||
const pageString = page ? `?page=${page}` : '';
|
||||
const { data } = await API.get(`${URL.product}${id}/posts${pageString}`);
|
||||
return data;
|
||||
};
|
||||
|
||||
export const useProductPost = ({ id, page }) => {
|
||||
const data = useQuery(
|
||||
['productpost', id],
|
||||
() => getProductPost({ id, page }),
|
||||
{
|
||||
suspense: true,
|
||||
}
|
||||
);
|
||||
|
||||
return data;
|
||||
};
|
5
src/methods/startViewTransition.js
Normal file
5
src/methods/startViewTransition.js
Normal file
@ -0,0 +1,5 @@
|
||||
function startViewTransition(callback) {
|
||||
document.startViewTransition?.(callback) ?? callback();
|
||||
}
|
||||
|
||||
export default startViewTransition;
|
16
src/pages/Brand/index.jsx
Normal file
16
src/pages/Brand/index.jsx
Normal file
@ -0,0 +1,16 @@
|
||||
import { useParams } from 'react-router-dom';
|
||||
import BrandProductList from '../../components/Brand/BrandProductList';
|
||||
import BrandTitle from '../../components/Brand/BrandTitle';
|
||||
|
||||
const Brand = () => {
|
||||
const { id } = useParams();
|
||||
|
||||
return (
|
||||
<>
|
||||
<BrandTitle id={id} />
|
||||
<BrandProductList id={id} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Brand;
|
24
src/pages/Login/index.jsx
Normal file
24
src/pages/Login/index.jsx
Normal file
@ -0,0 +1,24 @@
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import LoginForm from '../../components/Login/LoginForm';
|
||||
import { Title } from '../../styles/Common.styles';
|
||||
import { useEffect } from 'react';
|
||||
import { getCookie } from '../../API';
|
||||
|
||||
const Login = () => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
if (getCookie('sessionid')) {
|
||||
navigate(-1);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Title>로그인</Title>
|
||||
<LoginForm />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Login;
|
29
src/pages/Logout/index.jsx
Normal file
29
src/pages/Logout/index.jsx
Normal file
@ -0,0 +1,29 @@
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { API, URL, getCookie } from '../../API';
|
||||
import useIsLogged from '../../hooks/network/isLogged';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
const Logout = () => {
|
||||
const navigate = useNavigate();
|
||||
const setIsLogged = useIsLogged((state) => state.setIsLogged);
|
||||
|
||||
useEffect(() => {
|
||||
API.post(URL.logout, {
|
||||
csrftoken: getCookie('csrftoken'),
|
||||
})
|
||||
// API.post(URL.logout)
|
||||
.then((res) => {
|
||||
if (res.status === 204) {
|
||||
setIsLogged(false);
|
||||
}
|
||||
navigate('/');
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
navigate('/');
|
||||
});
|
||||
}, []);
|
||||
return <></>;
|
||||
};
|
||||
|
||||
export default Logout;
|
26
src/pages/MyPage/index.jsx
Normal file
26
src/pages/MyPage/index.jsx
Normal file
@ -0,0 +1,26 @@
|
||||
import Title from '../../components/Title';
|
||||
import { SubTitle } from '../../styles/Common.styles';
|
||||
import * as S from '../../styles/MyPage.styles';
|
||||
|
||||
function MyPage() {
|
||||
return (
|
||||
<>
|
||||
{/* TODO: Edit Link */}
|
||||
<Title>마이페이지</Title>
|
||||
<S.MyPage>
|
||||
<SubTitle>거래</SubTitle>
|
||||
<S.Section>
|
||||
<S.LinkItem to={'/'}>내가 쓴 판매글</S.LinkItem>
|
||||
</S.Section>
|
||||
<SubTitle>계정</SubTitle>
|
||||
<S.Section>
|
||||
<S.LinkItem to={'/'}>내가 쓴 판매글</S.LinkItem>
|
||||
<S.LinkItem to={'/'}>비밀번호 바꾸기</S.LinkItem>
|
||||
<S.LinkItem to={'/'}>회원 탈퇴</S.LinkItem>
|
||||
</S.Section>
|
||||
</S.MyPage>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default MyPage;
|
9
src/pages/NotFound/index.jsx
Normal file
9
src/pages/NotFound/index.jsx
Normal file
@ -0,0 +1,9 @@
|
||||
const NotFound = () => {
|
||||
return (
|
||||
<>
|
||||
<h1>Not Found</h1>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default NotFound;
|
46
src/pages/PostDetail/index.jsx
Normal file
46
src/pages/PostDetail/index.jsx
Normal file
@ -0,0 +1,46 @@
|
||||
import * as S from '../../styles/PostDetail.styles';
|
||||
import { Link, useParams } from 'react-router-dom';
|
||||
import { usePost } from '../../hooks/network/post';
|
||||
import { URL } from '../../API';
|
||||
import ImageViewer from '../../components/ImageViewer';
|
||||
import PanelLayout, {
|
||||
LeftPanel,
|
||||
RightPanel,
|
||||
} from '../../components/PanelLayout';
|
||||
import ProductPriceGraph from '../../components/Graph/ProductPriceGraph';
|
||||
import { SubTitle } from '../../styles/Common.styles';
|
||||
|
||||
const PostDetail = () => {
|
||||
const { id } = useParams();
|
||||
const { data } = usePost(id);
|
||||
|
||||
return (
|
||||
<S.PostDetail>
|
||||
<PanelLayout>
|
||||
<LeftPanel>
|
||||
<Link
|
||||
className="product"
|
||||
to={`${URL.product}/${data.product.id}`}
|
||||
>
|
||||
{data.product.name}
|
||||
</Link>
|
||||
<time>{data.written_at}</time>
|
||||
<div className="storage">{data.storage}</div>
|
||||
<div className="price">가격 {data.price.toLocaleString()}원</div>
|
||||
<p>{data.text}</p>
|
||||
</LeftPanel>
|
||||
<RightPanel>
|
||||
<SubTitle>제품 사진</SubTitle>
|
||||
<ImageViewer
|
||||
images={data?.images?.map((image) => image.image)}
|
||||
// title="제품 사진"
|
||||
/>
|
||||
<SubTitle>최근 거래 가격</SubTitle>
|
||||
<ProductPriceGraph id={data.product.id} />
|
||||
</RightPanel>
|
||||
</PanelLayout>
|
||||
</S.PostDetail>
|
||||
);
|
||||
};
|
||||
|
||||
export default PostDetail;
|
15
src/pages/Posts/index.jsx
Normal file
15
src/pages/Posts/index.jsx
Normal file
@ -0,0 +1,15 @@
|
||||
import * as S from '../../styles/Posts.styles';
|
||||
// import { Title } from '../../styles/Common.styles';
|
||||
import PostList from '../../components/Post/PostList';
|
||||
import Title from '../../components/Title';
|
||||
|
||||
const Posts = () => {
|
||||
return (
|
||||
<S.Posts>
|
||||
<Title>최신글</Title>
|
||||
<PostList />
|
||||
</S.Posts>
|
||||
);
|
||||
};
|
||||
|
||||
export default Posts;
|
36
src/pages/ProductDetail/index.jsx
Normal file
36
src/pages/ProductDetail/index.jsx
Normal file
@ -0,0 +1,36 @@
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { useProduct } from '../../hooks/network/product';
|
||||
import { SubTitle, Title } from '../../styles/Common.styles';
|
||||
import ProductPriceGraph from '../../components/Graph/ProductPriceGraph';
|
||||
import PanelLayout, {
|
||||
LeftPanel,
|
||||
RightPanel,
|
||||
} from '../../components/PanelLayout';
|
||||
import ProductPostList from '../../components/ProductPostList';
|
||||
|
||||
const ProductDetail = () => {
|
||||
const { id } = useParams();
|
||||
const page = new URLSearchParams(window.location.search).get('page') ?? 1;
|
||||
const { data } = useProduct(id);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Title>{data?.name}</Title>
|
||||
<PanelLayout reverse>
|
||||
<LeftPanel>
|
||||
<SubTitle>판매글 목록</SubTitle>
|
||||
<ProductPostList
|
||||
page={page}
|
||||
modelId={id}
|
||||
/>
|
||||
</LeftPanel>
|
||||
<RightPanel>
|
||||
<SubTitle>최근 거래 가격</SubTitle>
|
||||
<ProductPriceGraph id={id} />
|
||||
</RightPanel>
|
||||
</PanelLayout>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProductDetail;
|
13
src/pages/Products/index.jsx
Normal file
13
src/pages/Products/index.jsx
Normal file
@ -0,0 +1,13 @@
|
||||
import { Title } from '../../styles/Common.styles';
|
||||
import ProductList from '../../components/ProductList';
|
||||
|
||||
const Products = () => {
|
||||
return (
|
||||
<>
|
||||
<Title>기기 목록</Title>
|
||||
<ProductList />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Products;
|
13
src/pages/Signup/index.jsx
Normal file
13
src/pages/Signup/index.jsx
Normal file
@ -0,0 +1,13 @@
|
||||
import SignupForm from '../../components/Signup/SignupForm';
|
||||
import { Title } from '../../styles/Common.styles';
|
||||
|
||||
const Signup = () => {
|
||||
return (
|
||||
<>
|
||||
<Title>회원가입</Title>
|
||||
<SignupForm />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Signup;
|
14
src/pages/Write/index.jsx
Normal file
14
src/pages/Write/index.jsx
Normal file
@ -0,0 +1,14 @@
|
||||
import * as S from '../../styles/Form.styles';
|
||||
import WriteForm from '../../components/Write/WriteForm';
|
||||
import { Title } from '../../styles/Common.styles';
|
||||
|
||||
const Write = () => {
|
||||
return (
|
||||
<>
|
||||
<Title>글쓰기</Title>
|
||||
<WriteForm />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Write;
|
38
src/pages/WriteSteps/PhotoStep.jsx
Normal file
38
src/pages/WriteSteps/PhotoStep.jsx
Normal file
@ -0,0 +1,38 @@
|
||||
import Button from '../../components/Button';
|
||||
import ButtonArea from '../../components/ButtonArea';
|
||||
import ImageViewer from '../../components/ImageViewer';
|
||||
import * as S from '../../styles/WriteSteps.styles';
|
||||
|
||||
function PhotoStep({ gotoNextStep, gotoPrevStep, photos, setPhotos }) {
|
||||
return (
|
||||
<S.Step>
|
||||
<S.Title>휴대폰 사진을 올려주세요</S.Title>
|
||||
<S.HelpText>사진</S.HelpText>
|
||||
{/* TODO: upload photo */}
|
||||
<input
|
||||
type="file"
|
||||
accept="image/jpg, image/png, image/jpeg"
|
||||
onChange={(e) => {
|
||||
const file = e.target.files[0];
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => {
|
||||
setPhotos([...photos, reader.result]);
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
}}
|
||||
/>
|
||||
{photos && <ImageViewer images={photos} />}
|
||||
<ButtonArea>
|
||||
<Button
|
||||
secondary
|
||||
onClick={gotoPrevStep}
|
||||
>
|
||||
이전
|
||||
</Button>
|
||||
<Button onClick={gotoNextStep}>다음</Button>
|
||||
</ButtonArea>
|
||||
</S.Step>
|
||||
);
|
||||
}
|
||||
|
||||
export default PhotoStep;
|
33
src/pages/WriteSteps/PriceStep.jsx
Normal file
33
src/pages/WriteSteps/PriceStep.jsx
Normal file
@ -0,0 +1,33 @@
|
||||
import Button from '../../components/Button';
|
||||
import ButtonArea from '../../components/ButtonArea';
|
||||
import CurrencyInput from '../../components/CurrencyInput';
|
||||
import * as S from '../../styles/WriteSteps.styles';
|
||||
|
||||
function PriceStep({ gotoNextStep, gotoPrevStep, price, setPrice }) {
|
||||
return (
|
||||
<S.Step>
|
||||
<S.Title>얼마에 팔고 싶나요?</S.Title>
|
||||
<S.HelpText>가격</S.HelpText>
|
||||
<CurrencyInput
|
||||
value={price}
|
||||
setValue={setPrice}
|
||||
/>
|
||||
<ButtonArea>
|
||||
<Button
|
||||
secondary
|
||||
onClick={gotoPrevStep}
|
||||
>
|
||||
이전
|
||||
</Button>
|
||||
<Button
|
||||
onClick={gotoNextStep}
|
||||
disabled={price === ''}
|
||||
>
|
||||
다음
|
||||
</Button>
|
||||
</ButtonArea>
|
||||
</S.Step>
|
||||
);
|
||||
}
|
||||
|
||||
export default PriceStep;
|
45
src/pages/WriteSteps/ProductSelectStep.jsx
Normal file
45
src/pages/WriteSteps/ProductSelectStep.jsx
Normal file
@ -0,0 +1,45 @@
|
||||
import * as S from '../../styles/WriteSteps.styles';
|
||||
import { useProducts } from '../../hooks/network/product';
|
||||
import Button from '../../components/Button';
|
||||
import ButtonArea from '../../components/ButtonArea';
|
||||
|
||||
function ProductSelectStep({ gotoNextStep, product, setProduct }) {
|
||||
const { data: products } = useProducts();
|
||||
|
||||
return (
|
||||
<S.Step>
|
||||
<S.Title>판매할 휴대폰을 골라주세요</S.Title>
|
||||
<S.HelpText htmlFor="product">모델</S.HelpText>
|
||||
<S.Select
|
||||
id="product"
|
||||
value={product}
|
||||
onChange={(e) => setProduct(e.target.value)}
|
||||
>
|
||||
<option
|
||||
value=""
|
||||
disabled
|
||||
>
|
||||
선택해 주세요
|
||||
</option>
|
||||
{products.map(({ id, name }) => (
|
||||
<option
|
||||
key={id}
|
||||
value={id}
|
||||
>
|
||||
{name}
|
||||
</option>
|
||||
))}
|
||||
</S.Select>
|
||||
<ButtonArea>
|
||||
<Button
|
||||
onClick={gotoNextStep}
|
||||
disabled={product === ''}
|
||||
>
|
||||
다음
|
||||
</Button>
|
||||
</ButtonArea>
|
||||
</S.Step>
|
||||
);
|
||||
}
|
||||
|
||||
export default ProductSelectStep;
|
70
src/pages/WriteSteps/StatusStep.jsx
Normal file
70
src/pages/WriteSteps/StatusStep.jsx
Normal file
@ -0,0 +1,70 @@
|
||||
import Button from '../../components/Button';
|
||||
import ButtonArea from '../../components/ButtonArea';
|
||||
import CheckBox from '../../components/CheckBox';
|
||||
import * as S from '../../styles/WriteSteps.styles';
|
||||
|
||||
function StatusStep({ gotoNextStep, gotoPrevStep, status, setStatus }) {
|
||||
return (
|
||||
<S.Step>
|
||||
<S.Title>휴대폰 상태에 대해 알려주세요</S.Title>
|
||||
<S.Grid>
|
||||
<CheckBox
|
||||
id="화면 깨짐"
|
||||
name="화면 깨짐"
|
||||
text="화면이 깨져있어요"
|
||||
value={status.display}
|
||||
setChecked={(value) =>
|
||||
setStatus((prev) => ({ ...prev, display: value }))
|
||||
}
|
||||
/>
|
||||
<CheckBox
|
||||
id="뒷면/옆면 파손"
|
||||
name="뒷면/옆면 파손"
|
||||
text="화면이 아닌 부분에 파손이 있어요"
|
||||
value={status.back}
|
||||
setChecked={(value) =>
|
||||
setStatus((prev) => ({ ...prev, back: value }))
|
||||
}
|
||||
/>
|
||||
<CheckBox
|
||||
id="버튼 고장"
|
||||
name="버튼 고장"
|
||||
text="고장난 버튼이 있어요"
|
||||
value={status.button}
|
||||
setChecked={(value) =>
|
||||
setStatus((prev) => ({ ...prev, button: value }))
|
||||
}
|
||||
/>
|
||||
<CheckBox
|
||||
id="생체 인식 고장"
|
||||
name="생체 인식 고장"
|
||||
text="지문이나 얼굴인식이 작동하지 않아요"
|
||||
value={status.biometrics}
|
||||
setChecked={(value) =>
|
||||
setStatus((prev) => ({ ...prev, biometrics: value }))
|
||||
}
|
||||
/>
|
||||
<CheckBox
|
||||
id="기능 고장"
|
||||
name="기능 고장"
|
||||
text="그 밖에 작동하지 않는 기능이 있어요"
|
||||
value={status.others}
|
||||
setChecked={(value) =>
|
||||
setStatus((prev) => ({ ...prev, others: value }))
|
||||
}
|
||||
/>
|
||||
</S.Grid>
|
||||
<ButtonArea>
|
||||
<Button
|
||||
secondary
|
||||
onClick={gotoPrevStep}
|
||||
>
|
||||
이전
|
||||
</Button>
|
||||
<Button onClick={gotoNextStep}>다음</Button>
|
||||
</ButtonArea>
|
||||
</S.Step>
|
||||
);
|
||||
}
|
||||
|
||||
export default StatusStep;
|
40
src/pages/WriteSteps/TextStep.jsx
Normal file
40
src/pages/WriteSteps/TextStep.jsx
Normal file
@ -0,0 +1,40 @@
|
||||
import { useState } from 'react';
|
||||
import Button from '../../components/Button';
|
||||
import ButtonArea from '../../components/ButtonArea';
|
||||
import * as S from '../../styles/WriteSteps.styles';
|
||||
|
||||
function TextStep({ text, setText, gotoNextStep, gotoPrevStep }) {
|
||||
const [height, setHeight] = useState(0);
|
||||
const handleChange = (event) => {
|
||||
console.log(event);
|
||||
setText(event.target.value);
|
||||
setHeight(event.target.scrollHeight);
|
||||
};
|
||||
return (
|
||||
<S.Step>
|
||||
<S.Title>자세한 내용을 적어주세요</S.Title>
|
||||
<S.TextArea
|
||||
placeholder="내용을 적어주세요"
|
||||
value={text}
|
||||
onChange={handleChange}
|
||||
$height={height}
|
||||
/>
|
||||
<ButtonArea>
|
||||
<Button
|
||||
secondary
|
||||
onClick={gotoPrevStep}
|
||||
>
|
||||
이전
|
||||
</Button>
|
||||
<Button
|
||||
onClick={gotoNextStep}
|
||||
disabled={text === ''}
|
||||
>
|
||||
다음
|
||||
</Button>
|
||||
</ButtonArea>
|
||||
</S.Step>
|
||||
);
|
||||
}
|
||||
|
||||
export default TextStep;
|
85
src/pages/WriteSteps/index.jsx
Normal file
85
src/pages/WriteSteps/index.jsx
Normal file
@ -0,0 +1,85 @@
|
||||
import { useState } from 'react';
|
||||
import ProductSelectStep from './ProductSelectStep';
|
||||
import StatusStep from './StatusStep';
|
||||
import PhotoStep from './PhotoStep';
|
||||
import PriceStep from './PriceStep';
|
||||
import TextStep from './TextStep';
|
||||
|
||||
function WriteSteps() {
|
||||
const [formData, setFormData] = useState({
|
||||
product: '',
|
||||
status: {
|
||||
display: false,
|
||||
back: false,
|
||||
button: false,
|
||||
biometrics: false,
|
||||
others: false,
|
||||
},
|
||||
price: '',
|
||||
photos: [],
|
||||
text: '',
|
||||
});
|
||||
const STEP_INFO = ['product', 'status', 'price', 'photo', 'text', 'step6'];
|
||||
const setProduct = (product) => {
|
||||
setFormData((prev) => ({ ...prev, product }));
|
||||
};
|
||||
const setStatus = (status) => {
|
||||
setFormData((prev) => ({ ...prev, status }));
|
||||
};
|
||||
const setPrice = (price) => {
|
||||
setFormData((prev) => ({ ...prev, price }));
|
||||
};
|
||||
const setPhotos = (photos) => {
|
||||
setFormData((prev) => ({ ...prev, photos }));
|
||||
};
|
||||
const setText = (text) => {
|
||||
setFormData((prev) => ({ ...prev, text }));
|
||||
};
|
||||
const [step, setStep] = useState(STEP_INFO[0]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{step === 'product' && (
|
||||
<ProductSelectStep
|
||||
gotoNextStep={() => setStep('status')}
|
||||
product={formData.product}
|
||||
setProduct={setProduct}
|
||||
/>
|
||||
)}
|
||||
{step === 'status' && (
|
||||
<StatusStep
|
||||
status={formData.status}
|
||||
setStatus={setStatus}
|
||||
gotoNextStep={() => setStep('price')}
|
||||
gotoPrevStep={() => setStep('product')}
|
||||
/>
|
||||
)}
|
||||
{step === 'price' && (
|
||||
<PriceStep
|
||||
price={formData.price}
|
||||
setPrice={setPrice}
|
||||
gotoNextStep={() => setStep('photo')}
|
||||
gotoPrevStep={() => setStep('status')}
|
||||
/>
|
||||
)}
|
||||
{step === 'photo' && (
|
||||
<PhotoStep
|
||||
photos={formData.photos}
|
||||
setPhotos={setPhotos}
|
||||
gotoNextStep={() => setStep('text')}
|
||||
gotoPrevStep={() => setStep('price')}
|
||||
/>
|
||||
)}
|
||||
{step === 'text' && (
|
||||
<TextStep
|
||||
text={formData.text}
|
||||
setText={setText}
|
||||
gotoNextStep={() => setStep('step6')}
|
||||
gotoPrevStep={() => setStep('photo')}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default WriteSteps;
|
27
src/styles/Brand.styles.js
Normal file
27
src/styles/Brand.styles.js
Normal file
@ -0,0 +1,27 @@
|
||||
import { Link } from 'react-router-dom';
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const BrandList = styled.div`
|
||||
${({ theme }) => theme.maxWidth};
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 0 20px;
|
||||
`;
|
||||
|
||||
export const BrandItem = styled(Link)`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
margin: 20px;
|
||||
text-decoration: none;
|
||||
color: #000;
|
||||
background-color: #eee;
|
||||
border-radius: 20px;
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
justify-content: center;
|
||||
`;
|
32
src/styles/Button.styles.js
Normal file
32
src/styles/Button.styles.js
Normal file
@ -0,0 +1,32 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const BaseButton = styled.button`
|
||||
width: 100%;
|
||||
padding: 20px;
|
||||
border-radius: 10px;
|
||||
border: none;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
color: ${({ theme }) => theme.colors.gray50};
|
||||
transition: all 0.3s ease;
|
||||
cursor: pointer;
|
||||
|
||||
&:active {
|
||||
transform: scale(0.99);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
background-color: ${({ theme }) => theme.colors.gray200};
|
||||
color: ${({ theme }) => theme.colors.gray500};
|
||||
cursor: not-allowed;
|
||||
}
|
||||
`;
|
||||
|
||||
export const Button = {
|
||||
primary: styled(BaseButton)`
|
||||
background-color: ${({ theme }) => theme.colors.primary};
|
||||
`,
|
||||
secondary: styled(BaseButton)`
|
||||
background-color: ${({ theme }) => theme.colors.gray400};
|
||||
`,
|
||||
};
|
19
src/styles/ButtonArea.styles.js
Normal file
19
src/styles/ButtonArea.styles.js
Normal file
@ -0,0 +1,19 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const ButtonArea = styled.div`
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
padding: 20px;
|
||||
border-top: 1px solid ${({ theme }) => theme.colors.gray100};
|
||||
background-color: ${({ theme }) => theme.colors.background};
|
||||
box-sizing: border-box;
|
||||
`;
|
||||
|
||||
export const ButtonWrapper = styled.div`
|
||||
${({ theme }) => theme.maxWidth};
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 20px;
|
||||
`;
|
46
src/styles/CheckBox.styles.js
Normal file
46
src/styles/CheckBox.styles.js
Normal file
@ -0,0 +1,46 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const CheckBox = styled.label`
|
||||
${({ theme }) => theme.boxShadow};
|
||||
${({ theme }) => theme.hoverBorder};
|
||||
${({ theme }) => theme.activeTransform};
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
padding: 16px;
|
||||
user-select: none;
|
||||
cursor: pointer;
|
||||
`;
|
||||
|
||||
export const CheckInput = styled.input.attrs({ type: 'checkbox' })`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border: 1px solid ${({ theme }) => theme.colors.gray300};
|
||||
border-radius: 4px;
|
||||
transition: all 0.1s ease;
|
||||
|
||||
&:checked {
|
||||
background-color: ${({ theme }) => theme.colors.primary};
|
||||
}
|
||||
`;
|
||||
|
||||
export const Info = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
`;
|
||||
|
||||
export const Name = styled.div`
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: ${({ theme }) => theme.colors.gray900};
|
||||
`;
|
||||
|
||||
export const Text = styled.div`
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
color: ${({ theme }) => theme.colors.gray500};
|
||||
`;
|
19
src/styles/Common.styles.js
Normal file
19
src/styles/Common.styles.js
Normal file
@ -0,0 +1,19 @@
|
||||
import { styled } from 'styled-components';
|
||||
|
||||
export const Title = styled.h1`
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
padding: 32px 16px;
|
||||
margin: 0;
|
||||
user-select: none;
|
||||
box-sizing: border-box;
|
||||
`;
|
||||
|
||||
export const SubTitle = styled.h2`
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
padding: 16px;
|
||||
margin: 0;
|
||||
user-select: none;
|
||||
box-sizing: border-box;
|
||||
`;
|
30
src/styles/CurrencyInput.styles.js
Normal file
30
src/styles/CurrencyInput.styles.js
Normal file
@ -0,0 +1,30 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const CurrencyInput = styled.div`
|
||||
${({ theme }) => theme.boxShadow};
|
||||
${({ theme }) => theme.hoverBorder};
|
||||
${({ theme }) => theme.activeTransform};
|
||||
padding: 0;
|
||||
`;
|
||||
|
||||
export const CurrencyInputField = styled.input.attrs({ type: 'text' })`
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 20px;
|
||||
border-radius: 10px;
|
||||
box-sizing: border-box;
|
||||
color: ${({ theme }) => theme.colors.gray900};
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
caret-color: transparent;
|
||||
border: none;
|
||||
|
||||
&:focus,
|
||||
&:active {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
&:placeholder-shown {
|
||||
caret-color: ${({ theme }) => theme.colors.primary};
|
||||
}
|
||||
`;
|
79
src/styles/Form.styles.js
Normal file
79
src/styles/Form.styles.js
Normal file
@ -0,0 +1,79 @@
|
||||
import { styled } from 'styled-components';
|
||||
|
||||
export const Form = styled.form`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
align-items: start;
|
||||
gap: 16px;
|
||||
width: 100%;
|
||||
.formInput {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
min-height: 40px;
|
||||
.helpText {
|
||||
width: 20%;
|
||||
min-width: 80px;
|
||||
height: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 16px;
|
||||
}
|
||||
.formInput textarea {
|
||||
text-align: left;
|
||||
}
|
||||
.input {
|
||||
width: 100%;
|
||||
min-height: 40px;
|
||||
margin: 0;
|
||||
padding: 8px 16px;
|
||||
box-sizing: border-box;
|
||||
border-radius: 10px;
|
||||
border: none;
|
||||
background-color: ${({ theme }) => theme.colors.gray100};
|
||||
}
|
||||
.input:focus,
|
||||
.input:focus-visible,
|
||||
.input:focus-within {
|
||||
outline: 0;
|
||||
}
|
||||
.input:disabled {
|
||||
}
|
||||
}
|
||||
input[type='button'],
|
||||
input[type='submit'] {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
padding: 16px;
|
||||
width: 100%;
|
||||
min-height: 40px;
|
||||
border-radius: 10px;
|
||||
background-color: ${({ theme }) => theme.colors.gray400};
|
||||
color: white;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:disabled {
|
||||
background-color: ${({ theme }) => theme.colors.gray300};
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: scale(0.99);
|
||||
}
|
||||
}
|
||||
input[type='submit'] {
|
||||
background-color: ${({ theme }) => theme.colors.primary};
|
||||
}
|
||||
`;
|
||||
|
||||
export const Buttons = styled.div`
|
||||
margin-top: 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: stretch;
|
||||
gap: 16px;
|
||||
width: 100%;
|
||||
`;
|
23
src/styles/Global.js
Normal file
23
src/styles/Global.js
Normal file
@ -0,0 +1,23 @@
|
||||
import { createGlobalStyle } from 'styled-components';
|
||||
|
||||
const GlobalStyle = createGlobalStyle`
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
font-family: 'Noto Sans', Arial, sans-serif;
|
||||
letter-spacing: -0.6px;
|
||||
color: ${({ theme }) => theme.colors.gray900};
|
||||
}
|
||||
input, textarea, button, select {
|
||||
appearance: none;
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
}
|
||||
img {
|
||||
user-select: none;
|
||||
}
|
||||
`;
|
||||
|
||||
export default GlobalStyle;
|
29
src/styles/ImageViewer.style.js
Normal file
29
src/styles/ImageViewer.style.js
Normal file
@ -0,0 +1,29 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const ImageViewer = styled.div`
|
||||
${({ theme }) => theme.boxShadow};
|
||||
`;
|
||||
|
||||
export const MainImage = styled.img`
|
||||
max-width: 100%;
|
||||
width: 100%;
|
||||
height: 400px;
|
||||
object-fit: cover;
|
||||
border-radius: 8px;
|
||||
`;
|
||||
|
||||
export const Images = styled.div`
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
overflow-x: scroll;
|
||||
margin-top: 20px;
|
||||
/* justify-content: center; */
|
||||
|
||||
img {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
object-fit: cover;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
}
|
||||
`;
|
12
src/styles/KeyFrames.styles.js
Normal file
12
src/styles/KeyFrames.styles.js
Normal file
@ -0,0 +1,12 @@
|
||||
import { keyframes } from 'styled-components';
|
||||
|
||||
export const FadeInSlideFromRightAnimation = keyframes`
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateX(10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
`
|
92
src/styles/Layout.styles.js
Normal file
92
src/styles/Layout.styles.js
Normal file
@ -0,0 +1,92 @@
|
||||
// import LogoImage from '../assets/logo.svg';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { styled } from 'styled-components';
|
||||
|
||||
export const Header = styled.header`
|
||||
position: sticky;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 60px;
|
||||
background-color: ${({ theme }) => theme.colors.background};
|
||||
border-bottom: ${({ theme }) => `1px solid ${theme.colors.gray200}`};
|
||||
text-align: center;
|
||||
user-select: none;
|
||||
z-index: 9999;
|
||||
|
||||
.content {
|
||||
${({ theme }) => theme.maxWidth};
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 32px;
|
||||
align-items: center;
|
||||
padding: 0 32px;
|
||||
height: 100%;
|
||||
}
|
||||
`;
|
||||
|
||||
export const Logo = styled(Link)`
|
||||
max-width: 100px;
|
||||
height: 40px;
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
`;
|
||||
|
||||
export const Nav = styled.nav`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: #fff;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 9998;
|
||||
|
||||
> .left,
|
||||
> .right {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
`;
|
||||
|
||||
export const NavItem = styled(Link)`
|
||||
color: ${({ theme }) => theme.colors.gray700};
|
||||
font-size: 16px;
|
||||
text-decoration: none;
|
||||
border-radius: 8px;
|
||||
padding: 8px;
|
||||
box-sizing: border-box;
|
||||
transition: background-color 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
background-color: ${({ theme }) => theme.colors.gray200};
|
||||
}
|
||||
`;
|
||||
|
||||
export const Main = styled.main`
|
||||
${({ theme }) => theme.maxWidth};
|
||||
min-height: calc(100vh - 120px);
|
||||
padding: 32px;
|
||||
`;
|
||||
|
||||
export const Footer = styled.footer`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: start;
|
||||
justify-content: center;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
height: 60px;
|
||||
color: ${({ theme }) => theme.colors.gray500};
|
||||
font-size: 12px;
|
||||
`;
|
||||
|
||||
export const FooterWrapper = styled.div`
|
||||
${({ theme }) => theme.maxWidth};
|
||||
padding: 0 32px;
|
||||
`;
|
32
src/styles/MyPage.styles.js
Normal file
32
src/styles/MyPage.styles.js
Normal file
@ -0,0 +1,32 @@
|
||||
import { Link } from 'react-router-dom';
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const MyPage = styled.div`
|
||||
${({ theme }) => theme.maxWidth};
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
`;
|
||||
|
||||
export const Section = styled.section`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
padding: 0 16px 16px;
|
||||
`;
|
||||
|
||||
export const SectionContents = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
`;
|
||||
|
||||
export const LinkItem = styled(Link)`
|
||||
${({ theme }) => theme.boxShadow};
|
||||
${({ theme }) => theme.hoverBorder};
|
||||
${({ theme }) => theme.activeTransform};
|
||||
display: block;
|
||||
font-size: 16px;
|
||||
padding: 16px 20px;
|
||||
color: ${({ theme }) => theme.colors.gray900};
|
||||
text-decoration: none;
|
||||
`;
|
34
src/styles/PanelLayout.styles.js
Normal file
34
src/styles/PanelLayout.styles.js
Normal file
@ -0,0 +1,34 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const PanelLayout = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 32px;
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
flex-direction: ${({ $reverse }) =>
|
||||
$reverse ? 'column-reverse' : 'column'};
|
||||
gap: 10px;
|
||||
}
|
||||
`;
|
||||
|
||||
export const LeftPanel = styled.div`
|
||||
width: 60%;
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
width: 100%;
|
||||
}
|
||||
`;
|
||||
|
||||
export const RightPanel = styled.div`
|
||||
width: 40%;
|
||||
/* height: 100%; */
|
||||
height: fit-content;
|
||||
position: sticky;
|
||||
top: 100px;
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
position: static;
|
||||
width: 100%;
|
||||
}
|
||||
`;
|
42
src/styles/PostDetail.styles.js
Normal file
42
src/styles/PostDetail.styles.js
Normal file
@ -0,0 +1,42 @@
|
||||
import { styled } from 'styled-components';
|
||||
|
||||
export const PostDetail = styled.div`
|
||||
${({ theme }) => theme.maxWidth};
|
||||
|
||||
.product {
|
||||
display: inline-block;
|
||||
color: ${({ theme }) => theme.colors.gray900};
|
||||
text-decoration: none;
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
time {
|
||||
display: block;
|
||||
margin-top: 4px;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
letter-spacing: 0;
|
||||
color: ${({ theme }) => theme.colors.gray500};
|
||||
}
|
||||
|
||||
.price {
|
||||
margin-top: 8px;
|
||||
font-size: 18px;
|
||||
line-height: 1.7;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 16px;
|
||||
}
|
||||
`;
|
||||
|
||||
export const WrittenTime = styled.time`
|
||||
display: block;
|
||||
margin-top: 4px;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
letter-spacing: 0;
|
||||
color: ${({ theme }) => theme.colors.gray500};
|
||||
`;
|
50
src/styles/PostItem.styles.js
Normal file
50
src/styles/PostItem.styles.js
Normal file
@ -0,0 +1,50 @@
|
||||
import { Link } from 'react-router-dom';
|
||||
import { styled } from 'styled-components';
|
||||
|
||||
export const PostItem = styled(Link)`
|
||||
${({ theme }) => theme.boxShadow};
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
padding: 16px;
|
||||
margin-bottom: 16px;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
text-decoration: none;
|
||||
color: ${({ theme }) => theme.colors.gray900};
|
||||
transition: all 0.3s ease;
|
||||
`;
|
||||
|
||||
export const ProductInfo = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
`;
|
||||
|
||||
export const ProductName = styled.div`
|
||||
font-size: 19.2px;
|
||||
font-weight: 600;
|
||||
`;
|
||||
|
||||
export const ProductDate = styled.time`
|
||||
font-size: 12.8px;
|
||||
color: ${({ theme }) => theme.colors.gray400};
|
||||
margin-bottom: 12px;
|
||||
`;
|
||||
|
||||
export const ProductPrice = styled.div`
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
`;
|
||||
|
||||
export const PostImage = styled.div`
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
border-radius: 10px;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
border-radius: 10px;
|
||||
}
|
||||
`;
|
5
src/styles/Posts.styles.js
Normal file
5
src/styles/Posts.styles.js
Normal file
@ -0,0 +1,5 @@
|
||||
import { styled } from 'styled-components';
|
||||
|
||||
export const Posts = styled.div`
|
||||
margin: 0 auto;
|
||||
`;
|
26
src/styles/ProductPriceGraph.styles.js
Normal file
26
src/styles/ProductPriceGraph.styles.js
Normal file
@ -0,0 +1,26 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const ProductPriceGraph = styled.div`
|
||||
${({ theme }) => theme.boxShadow};
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
|
||||
&:hover,
|
||||
&:active {
|
||||
border-color: ${({ theme }) => theme.colors.gray200};
|
||||
transform: none;
|
||||
}
|
||||
`;
|
||||
|
||||
export const GraphArea = styled.div`
|
||||
width: 100%;
|
||||
height: 200px;
|
||||
`;
|
||||
|
||||
export const Text = styled.div`
|
||||
font-size: 16px;
|
||||
margin-left: 16px;
|
||||
`;
|
16
src/styles/Products.styles.js
Normal file
16
src/styles/Products.styles.js
Normal file
@ -0,0 +1,16 @@
|
||||
import { Link } from 'react-router-dom';
|
||||
import { styled } from 'styled-components';
|
||||
|
||||
export const ProductItem = styled(Link)`
|
||||
${({ theme }) => theme.boxShadow};
|
||||
display: block;
|
||||
padding: 16px;
|
||||
margin-bottom: 16px;
|
||||
height: 100%;
|
||||
text-decoration: none;
|
||||
color: ${({ theme }) => theme.colors.gray900};
|
||||
transition: all 0.2s ease;
|
||||
user-select: none;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
`;
|
55
src/styles/Themes.styles.js
Normal file
55
src/styles/Themes.styles.js
Normal file
@ -0,0 +1,55 @@
|
||||
import { css } from 'styled-components';
|
||||
|
||||
const colors = {
|
||||
gray50: '#f9fafb',
|
||||
gray100: '#f2f4f6',
|
||||
gray200: '#e5e8eb',
|
||||
gray300: '#d1d6db',
|
||||
gray400: '#b0b8c1',
|
||||
gray500: '#8b95a1',
|
||||
gray600: '#6b7684',
|
||||
gray700: '#4e5968',
|
||||
gray800: '#333d4b',
|
||||
gray900: '#191f28',
|
||||
background: '#fff',
|
||||
grayBackground: '#f2f4f6',
|
||||
primary: '#2060ee',
|
||||
};
|
||||
|
||||
const boxShadow = css`
|
||||
padding: 20px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid ${({ theme }) => theme.colors.gray200};
|
||||
box-shadow: 0px 4px 20px 0px #2060ee1a;
|
||||
transition: all 0.3s ease;
|
||||
`;
|
||||
|
||||
const hoverBorder = css`
|
||||
&:hover,
|
||||
&:active {
|
||||
border-color: ${({ theme }) => theme.colors.primary};
|
||||
}
|
||||
`;
|
||||
|
||||
const activeTransform = css`
|
||||
&:active {
|
||||
transform: scale(0.99);
|
||||
}
|
||||
`;
|
||||
|
||||
const maxWidth = css`
|
||||
width: 100%;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
box-sizing: border-box;
|
||||
`;
|
||||
|
||||
const theme = {
|
||||
colors,
|
||||
boxShadow,
|
||||
hoverBorder,
|
||||
activeTransform,
|
||||
maxWidth,
|
||||
};
|
||||
|
||||
export default theme;
|
65
src/styles/WriteSteps.styles.js
Normal file
65
src/styles/WriteSteps.styles.js
Normal file
@ -0,0 +1,65 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const Step = styled.div`
|
||||
/* flex-grow: 1; */
|
||||
min-height: calc(100vh - 240px);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: stretch;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
export const Title = styled.h1`
|
||||
margin: 0;
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
line-height: 1.44;
|
||||
color: ${({ theme }) => theme.colors.gray900};
|
||||
word-break: keep-all;
|
||||
margin-bottom: 40px;
|
||||
`;
|
||||
|
||||
export const HelpText = styled.div`
|
||||
display: block;
|
||||
margin-bottom: 16px;
|
||||
margin-left: 8px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: ${({ theme }) => theme.colors.gray900};
|
||||
`;
|
||||
|
||||
export const Select = styled.select`
|
||||
${({ theme }) => theme.boxShadow};
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
color: ${({ theme }) => theme.colors.gray900};
|
||||
cursor: pointer;
|
||||
`;
|
||||
|
||||
export const Grid = styled.div`
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 20px;
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
grid-template-columns: repeat(1, 1fr);
|
||||
}
|
||||
`;
|
||||
|
||||
export const TextArea = styled.textarea`
|
||||
${({ theme }) => theme.boxShadow};
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
min-height: 160px;
|
||||
padding: 16px;
|
||||
font-size: 16px;
|
||||
font-weight: 400;
|
||||
color: ${({ theme }) => theme.colors.gray900};
|
||||
box-sizing: border-box;
|
||||
transition: all 0.3s ease;
|
||||
outline: none;
|
||||
|
||||
&:active {
|
||||
transform: none;
|
||||
}
|
||||
`;
|
Loading…
Reference in New Issue
Block a user