From e81c3977f64a8f05d57af02796dadd152d44e679 Mon Sep 17 00:00:00 2001
From: PortalCube <35104213+PortalCube@users.noreply.github.com>
Date: Thu, 5 Oct 2023 22:31:55 +0900
Subject: [PATCH 1/7] =?UTF-8?q?Feat:=20=EB=B3=80=EA=B2=BD=EB=90=9C=20API?=
=?UTF-8?q?=20=EC=BD=94=EB=93=9C=20=EA=B5=AC=ED=98=84?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/librarys/axios.js | 40 +++++++++++++++++++++++++++++++++++++++-
1 file changed, 39 insertions(+), 1 deletion(-)
diff --git a/src/librarys/axios.js b/src/librarys/axios.js
index e551b27..35cf2dc 100644
--- a/src/librarys/axios.js
+++ b/src/librarys/axios.js
@@ -5,7 +5,7 @@ export default axios.create({
timeout: 10000,
});
-export function getSpringAxios(token) {
+export function getSpringAxios(token = null) {
const options = {
baseURL: "http://motus.website/",
timeout: 10000,
@@ -26,3 +26,41 @@ export function getAIAxios() {
timeout: 1000 * 60 * 60 * 24,
});
}
+
+export async function createVideo(options) {
+ const axios = getSpringAxios();
+
+ const data = new FormData();
+ data.append("title", options.title);
+ data.append("description", options.description);
+ data.append("category", options.category);
+ data.append("position", options.pose);
+ data.append("frame", options.totalFrame);
+ data.append("playTime", options.duration);
+ data.append("files[0]", options.video);
+ data.append("files[1]", options.skeleton);
+
+ const response = await axios.post("/video/create", data);
+ return response.data;
+}
+
+export async function removeVideo(id) {
+ const axios = getSpringAxios();
+
+ const response = await axios.delete("/video/delete/" + id);
+ return response.data;
+}
+
+export async function listVideo() {
+ const axios = getSpringAxios();
+
+ const response = await axios.get("/video/list");
+ return response.data;
+}
+
+export async function getVideo(id) {
+ const axios = getSpringAxios();
+
+ const response = await axios.get("/video/" + id);
+ return response.data;
+}
From 4d943671a43a3acf230b8272df35fc8afb295872 Mon Sep 17 00:00:00 2001
From: PortalCube <35104213+PortalCube@users.noreply.github.com>
Date: Thu, 5 Oct 2023 22:33:19 +0900
Subject: [PATCH 2/7] =?UTF-8?q?Feat:=20=EB=8F=99=EC=98=81=EC=83=81=20?=
=?UTF-8?q?=EA=B2=8C=EC=8B=9C=20=ED=8E=98=EC=9D=B4=EC=A7=80=20API=20?=
=?UTF-8?q?=EC=B5=9C=EC=A2=85=20=EC=97=B0=EA=B2=B0?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- reducer 패턴을 이용하여 코드 구성 단순화
- 각종 dispatch 코드 및 handle 코드 리팩토링
---
src/components/SkeletonVideo.jsx | 29 ++++-----
src/components/TagSelect.jsx | 100 +++++++++++++++++++++++++++++++
src/components/VideoUploader.jsx | 100 +++++++++++++++++++++----------
src/pages/AddExercise.jsx | 87 ++++++++++++++++++++++-----
src/reducer/upload.js | 54 +++++++++++++++++
5 files changed, 308 insertions(+), 62 deletions(-)
create mode 100644 src/components/TagSelect.jsx
create mode 100644 src/reducer/upload.js
diff --git a/src/components/SkeletonVideo.jsx b/src/components/SkeletonVideo.jsx
index 14078c2..ca32058 100644
--- a/src/components/SkeletonVideo.jsx
+++ b/src/components/SkeletonVideo.jsx
@@ -30,6 +30,7 @@ const Loading = styled.div`
flex-direction: column;
align-items: center;
justify-content: center;
+ color: white;
background-color: rgba(0, 0, 0, 0.5);
&.disable {
@@ -39,13 +40,13 @@ const Loading = styled.div`
const Text = styled.p`
margin-top: 12px;
- font-size: 12px;
+ font-size: 16px;
text-align: center;
`;
const Icon = styled(ImSpinner2)`
- width: 48px;
- height: 48px;
+ width: 72px;
+ height: 72px;
animation: spin 5s infinite linear;
@keyframes spin {
@@ -124,6 +125,17 @@ function drawPoints(context, data, ratio) {
});
}
+function getContainedSize(video) {
+ const ratio = video.videoWidth / video.videoHeight;
+ let width = video.clientHeight * ratio;
+ let height = video.clientHeight;
+ if (width > video.clientWidth) {
+ width = video.clientWidth;
+ height = video.clientWidth / ratio;
+ }
+ return [width, height];
+}
+
const SkeletonVideo = ({ src, skeleton, onLoad, ...props }) => {
const containerRef = useRef(null);
const canvasRef = useRef(null);
@@ -197,17 +209,6 @@ const SkeletonVideo = ({ src, skeleton, onLoad, ...props }) => {
drawPoints(context, data[currentFrame], ratio);
}
- function getContainedSize(video) {
- const ratio = video.videoWidth / video.videoHeight;
- let width = video.clientHeight * ratio;
- let height = video.clientHeight;
- if (width > video.clientWidth) {
- width = video.clientWidth;
- height = video.clientWidth / ratio;
- }
- return [width, height];
- }
-
function getWidthAndHeight(event) {
onLoad(event);
setAspectRatio(event.target.videoWidth / event.target.videoHeight);
diff --git a/src/components/TagSelect.jsx b/src/components/TagSelect.jsx
new file mode 100644
index 0000000..c055666
--- /dev/null
+++ b/src/components/TagSelect.jsx
@@ -0,0 +1,100 @@
+import { useContext, useState } from "react";
+import styled from "styled-components";
+import PropTypes from "prop-types";
+import { CATEGORY, POSITION } from "../librarys/type";
+import { ReducerContext } from "../librarys/context";
+
+const Container = styled.div`
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ gap: 16px;
+`;
+
+const FilterSection = styled.div`
+ height: 36px;
+ display: flex;
+ align-items: center;
+ gap: 12px;
+`;
+
+const Header = styled.div`
+ width: 120px;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+
+ font-size: 20px;
+ font-weight: 700;
+ color: #5f5f5f;
+
+ &::after {
+ content: "";
+ width: 2px;
+ height: 20px;
+ background-color: #5f5f5f;
+ }
+`;
+
+const Button = styled.button`
+ width: 100px;
+ height: 100%;
+ background-color: ${(props) => (props.selected ? "#6968CC" : "#1E1E1E")};
+ color: #f2f2f2;
+ border-radius: 10px;
+ font-size: 16px;
+ border: none;
+ cursor: pointer;
+
+ &:focus {
+ outline: none;
+ }
+`;
+
+const TagSelect = () => {
+ const [state, dispatch] = useContext(ReducerContext);
+ const { category, pose } = state;
+
+ function onClick(type, payload) {
+ dispatch({
+ type,
+ payload,
+ });
+ }
+
+ return (
+
+
+
+ {CATEGORY.map(({ key, value }) => (
+ onClick("category", key)}
+ >
+ {value}
+
+ ))}
+
+
+
+
+ {POSITION.map(({ key, value }) => (
+ onClick("pose", key)}
+ >
+ {value} 자세
+
+ ))}
+
+
+ );
+};
+
+TagSelect.propTypes = {
+ onChange: PropTypes.func,
+};
+
+export default TagSelect;
diff --git a/src/components/VideoUploader.jsx b/src/components/VideoUploader.jsx
index 75617f8..ddba024 100644
--- a/src/components/VideoUploader.jsx
+++ b/src/components/VideoUploader.jsx
@@ -1,6 +1,10 @@
-import styled from 'styled-components';
-import PropTypes from 'prop-types';
-import { useState } from 'react';
+import styled from "styled-components";
+import PropTypes from "prop-types";
+import { useContext, useMemo, useRef, useState } from "react";
+import SkeletonVideo from "./SkeletonVideo";
+import { getSkeletons } from "../librarys/skeleton-api";
+import classNames from "classnames";
+import { ReducerContext } from "../librarys/context";
const VideoUploadContainer = styled.div`
width: 540px;
@@ -11,12 +15,12 @@ const VideoUploadContainer = styled.div`
display: flex;
align-items: center;
justify-content: center;
- cursor: pointer;
+ cursor: pointer;
position: relative;
- overflow: hidden;
+ overflow: hidden;
- &:hover {
- opacity: 0.8;
+ &.disable {
+ cursor: default;
}
`;
@@ -28,39 +32,75 @@ const UploadText = styled.span`
`;
const HiddenInput = styled.input`
- position: absolute;
- top: 0;
- left: 0;
- width: 100%;
- height: 100%;
- opacity: 0;
- cursor: pointer;
+ display: none;
`;
-const VideoPreview = styled.video`
+const VideoPreview = styled(SkeletonVideo)`
width: 100%;
height: 100%;
border-radius: 10px;
`;
-const VideoUploader = ({ onUpload }) => {
- const [videoPreview, setVideoPreview] = useState(null);
+const VideoUploader = () => {
+ const input = useRef(null);
+ const [state, dispatch] = useContext(ReducerContext);
+ const { skeleton } = state;
+ const url = useMemo(
+ () => (state.video ? URL.createObjectURL(state.video) : null),
+ [state.video],
+ );
- const handleFileChange = (event) => {
+ const handleVideoChange = async (event) => {
const file = event.target.files[0];
- if (file) {
- // 파일 객체에서 URL을 생성하여 미리보기에 사용
- const videoURL = URL.createObjectURL(file);
- setVideoPreview(videoURL);
- onUpload && onUpload(file);
+
+ if (!file) {
+ return;
}
+
+ // 클라이언트 단에서 영상 띄우기
+ dispatch({
+ type: "video",
+ payload: file,
+ });
+
+ // AI에게 영상 binary를 전송
+ const formData = new FormData();
+ formData.append("video_file", file);
+ const skeleton = await getSkeletons(formData);
+
+ // 스켈레톤 받아오면 등록
+ dispatch({
+ type: "skeleton",
+ payload: skeleton,
+ });
};
+ function handleMetadata(event) {
+ dispatch({
+ type: "duration",
+ payload: Number(event.target.duration),
+ });
+ }
+
+ function onClick() {
+ if (input && url === null) {
+ input.current.click();
+ }
+ }
+
return (
-
-
- {videoPreview ? (
-
+
+
+ {url ? (
+
) : (
여기를 클릭해서 동영상 업로드
@@ -70,8 +110,4 @@ const VideoUploader = ({ onUpload }) => {
);
};
-VideoUploader.propTypes = {
- onUpload: PropTypes.func,
-};
-
-export default VideoUploader;
\ No newline at end of file
+export default VideoUploader;
diff --git a/src/pages/AddExercise.jsx b/src/pages/AddExercise.jsx
index 8a3e94a..b61ac1b 100644
--- a/src/pages/AddExercise.jsx
+++ b/src/pages/AddExercise.jsx
@@ -1,9 +1,13 @@
import AddHeader from "../components/AddHeader.jsx";
import VideoUploader from "../components/VideoUploader.jsx";
import styled from "styled-components";
-import { useState } from "react";
+import { useReducer, useState } from "react";
import UploadButton from "../components/UploadButton.jsx";
-import FilterButtons from "../components/FilterButtons.jsx";
+import TagSelect from "../components/TagSelect.jsx";
+import { createVideo } from "../librarys/axios.js";
+import { useNavigate } from "react-router-dom";
+import { intialUploadState, uploadReducer } from "../reducer/upload.js";
+import { ReducerContext } from "../librarys/context.js";
const PageContainer = styled.div`
width: 1200px;
@@ -30,12 +34,13 @@ const Title = styled.h1`
width: 100%;
font-weight: bold;
font-size: 24px;
- color: #ffffff;
+ color: #f2f2f2;
`;
const StyledInput = styled.input`
width: 100%;
height: 50px;
+ color: #f2f2f2;
background-color: #242424;
border-radius: 10px;
border: 1px solid #444444;
@@ -45,6 +50,7 @@ const StyledInput = styled.input`
const StyledTextarea = styled.textarea`
width: 100%;
height: 120px;
+ color: #f2f2f2;
background-color: #242424;
border-radius: 10px;
border: 1px solid #444444;
@@ -61,24 +67,73 @@ const UploadButtonContainer = styled.div`
`;
const AddExercise = () => {
- const [title, setTitle] = useState("");
- const [description, setDesctiption] = useState("");
+ const navigate = useNavigate();
+ const [state, dispatch] = useReducer(uploadReducer, intialUploadState);
+ const { title, description, video, skeleton } = state;
- const handleTitleChange = (e) => {
- setTitle(e.target.value);
+ const handleChange = (type) => {
+ return (e) => dispatch({ type, payload: e.target.value });
};
- const handleDescriptionChange = (e) => {
- setDesctiption(e.target.value);
+ const handleTagChange = ({ category, pose }) => {
+ dispatch({
+ type: "tag",
+ payload: [category, pose],
+ });
};
+ async function upload() {
+ if (video === null) {
+ alert("동영상을 업로드해주세요.");
+ return;
+ }
+
+ // if (skeleton === null) {
+ // alert("동영상의 처리가 완료될 때까지 기다려주세요.");
+ // return;
+ // }
+
+ if (title.length < 1) {
+ alert("제목을 2자 이상 입력해주세요.");
+ return;
+ }
+
+ if (description.length < 1) {
+ alert("설명을 2자 이상 입력해주세요.");
+ return;
+ }
+
+ const dummy = {
+ error: "테스트 데이터입니다.",
+ video_length: "0",
+ };
+
+ const blob = new Blob([JSON.stringify(dummy)], {
+ type: "application/json",
+ });
+
+ const options = {
+ ...state,
+ totalFrame: parseInt(dummy.video_length),
+ skeleton: blob,
+ };
+
+ console.log(options);
+
+ const programResponse = await createVideo(options);
+ console.log(programResponse);
+
+ alert("비디오를 성공적으로 게시했습니다.");
+ navigate("/");
+ }
+
return (
-
+
- 동영상 및 스켈레톤 데이터
+ 동영상 및 AI 스켈레톤 데이터
@@ -87,24 +142,24 @@ const AddExercise = () => {
placeholder="제목을 입력하세요..."
maxLength={50}
value={title}
- onChange={handleTitleChange}
+ onChange={handleChange("title")}
/>
운동 설명
카테고리 및 태그
-
+
-
+
-
+
);
};
export default AddExercise;
diff --git a/src/reducer/upload.js b/src/reducer/upload.js
new file mode 100644
index 0000000..9bca252
--- /dev/null
+++ b/src/reducer/upload.js
@@ -0,0 +1,54 @@
+import { CATEGORY, POSITION } from "../librarys/type";
+
+export const intialUploadState = {
+ title: "",
+ description: "",
+ category: CATEGORY[0].key,
+ pose: POSITION[0].key,
+ video: null,
+ duration: null,
+ skeleton: null,
+};
+
+export function uploadReducer(state, action) {
+ switch (action.type) {
+ case "title":
+ return {
+ ...state,
+ title: action.payload,
+ };
+ case "description":
+ return {
+ ...state,
+ description: action.payload,
+ };
+ case "category":
+ return {
+ ...state,
+ category: action.payload,
+ };
+ case "pose":
+ return {
+ ...state,
+ pose: action.payload,
+ };
+ case "video":
+ return {
+ ...state,
+ video: action.payload,
+ };
+ case "duration":
+ return {
+ ...state,
+ duration: action.payload,
+ };
+ case "skeleton":
+ return {
+ ...state,
+ skeleton: action.payload,
+ };
+ default:
+ console.error("[UploadReducer] Undefined action: " + action.type);
+ return state;
+ }
+}
From c5d9da627014aa59298d9ace53c3700ba57d7a5a Mon Sep 17 00:00:00 2001
From: PortalCube <35104213+PortalCube@users.noreply.github.com>
Date: Thu, 5 Oct 2023 22:33:37 +0900
Subject: [PATCH 3/7] =?UTF-8?q?Chore:=20type=EC=9D=84=20API=EC=99=80=20?=
=?UTF-8?q?=EB=8F=99=EC=9D=BC=ED=95=98=EA=B2=8C=20=EC=88=98=EC=A0=95?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/librarys/type.js | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/src/librarys/type.js b/src/librarys/type.js
index 10430a5..f4de5b0 100644
--- a/src/librarys/type.js
+++ b/src/librarys/type.js
@@ -1,8 +1,8 @@
export const CATEGORY = [
- { key: "ARMS", value: "팔" },
- { key: "SHOULDERS", value: "어깨" },
- { key: "KNEES", value: "무릎" },
- { key: "THIGHS", value: "허벅지" },
+ { key: "ARM", value: "팔" },
+ { key: "SHOULDER", value: "어깨" },
+ { key: "KNEE", value: "무릎" },
+ { key: "THIGH", value: "허벅지" },
];
export const POSITION = [
From 459ce8dc4a2042e5467607ec3c0fec199c7bab0b Mon Sep 17 00:00:00 2001
From: PortalCube <35104213+PortalCube@users.noreply.github.com>
Date: Thu, 5 Oct 2023 22:33:52 +0900
Subject: [PATCH 4/7] =?UTF-8?q?Feat:=20reducerContext=20=EC=B6=94=EA=B0=80?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/librarys/context.js | 3 +++
1 file changed, 3 insertions(+)
create mode 100644 src/librarys/context.js
diff --git a/src/librarys/context.js b/src/librarys/context.js
new file mode 100644
index 0000000..7e7042f
--- /dev/null
+++ b/src/librarys/context.js
@@ -0,0 +1,3 @@
+import { createContext } from "react";
+
+export const ReducerContext = createContext();
From ea9e7c771a216ec95665174bbbf2b34d65c5c6a3 Mon Sep 17 00:00:00 2001
From: PortalCube <35104213+PortalCube@users.noreply.github.com>
Date: Thu, 5 Oct 2023 22:34:05 +0900
Subject: [PATCH 5/7] =?UTF-8?q?Chore:=20=EB=AF=B8=EC=82=AC=EC=9A=A9=20user?=
=?UTF-8?q?id=20=EC=A0=9C=EA=B1=B0?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/librarys/player.js | 5 -----
1 file changed, 5 deletions(-)
diff --git a/src/librarys/player.js b/src/librarys/player.js
index 6760fca..194d35b 100644
--- a/src/librarys/player.js
+++ b/src/librarys/player.js
@@ -12,7 +12,6 @@ class Player {
guideDuration = null;
name = null;
id = null;
- userId = null;
videoId = null;
status = 0;
@@ -181,10 +180,6 @@ class Player {
}
const score = response ? response.metrics : 0.01;
- console.log(this.videoId, this.userId, score);
-
- await modifyMetrics(this.videoId, this.userId, score);
-
this.onComplete(time, score);
}
}
From 0830249a43ae9abfb6a9d25de2fcf63f4374039f Mon Sep 17 00:00:00 2001
From: PortalCube <35104213+PortalCube@users.noreply.github.com>
Date: Thu, 5 Oct 2023 22:34:23 +0900
Subject: [PATCH 6/7] =?UTF-8?q?Chore:=20=EC=9D=BC=EB=B6=80=20=EC=83=9D?=
=?UTF-8?q?=EB=9E=B5=EB=90=9C=20=ED=99=95=EC=9E=A5=EC=9E=90=20=EB=AA=85?=
=?UTF-8?q?=EC=8B=9C?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/components/player/ControllerSection.jsx | 2 +-
src/components/player/GuideSection.jsx | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/components/player/ControllerSection.jsx b/src/components/player/ControllerSection.jsx
index b21443b..ebedf0d 100644
--- a/src/components/player/ControllerSection.jsx
+++ b/src/components/player/ControllerSection.jsx
@@ -5,7 +5,7 @@ import { MdClose, MdPlayArrow, MdRefresh } from "react-icons/md";
import classNames from "classnames";
import styled from "styled-components";
-import { DispatchContext, StateContext } from "../../librarys/context";
+import { DispatchContext, StateContext } from "../../librarys/context.jsx";
import Player from "../../librarys/player.js";
import { useNavigate } from "react-router-dom";
diff --git a/src/components/player/GuideSection.jsx b/src/components/player/GuideSection.jsx
index 1bf5658..af3f49d 100644
--- a/src/components/player/GuideSection.jsx
+++ b/src/components/player/GuideSection.jsx
@@ -2,7 +2,7 @@ import { useState, useEffect, useRef, useContext } from "react";
import styled from "styled-components";
import Player from "../../librarys/player";
-import { DispatchContext, StateContext } from "../../librarys/context";
+import { DispatchContext, StateContext } from "../../librarys/context.jsx";
const Container = styled.div`
max-width: 20%;
From 4e8b28929d1dbc1a3c7574e07b7a256730e7eb99 Mon Sep 17 00:00:00 2001
From: PortalCube <35104213+PortalCube@users.noreply.github.com>
Date: Thu, 5 Oct 2023 22:44:07 +0900
Subject: [PATCH 7/7] =?UTF-8?q?Fix:=20=EC=83=9D=EB=9E=B5=EB=90=9C=20?=
=?UTF-8?q?=ED=99=95=EC=9E=A5=EC=9E=90=20=EB=AA=85=EC=8B=9C=20(2)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/components/TagSelect.jsx | 2 +-
src/components/VideoUploader.jsx | 2 +-
src/components/player/BorderBox.jsx | 2 +-
src/components/player/Countdown.jsx | 2 +-
4 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/src/components/TagSelect.jsx b/src/components/TagSelect.jsx
index c055666..8b27031 100644
--- a/src/components/TagSelect.jsx
+++ b/src/components/TagSelect.jsx
@@ -2,7 +2,7 @@ import { useContext, useState } from "react";
import styled from "styled-components";
import PropTypes from "prop-types";
import { CATEGORY, POSITION } from "../librarys/type";
-import { ReducerContext } from "../librarys/context";
+import { ReducerContext } from "../librarys/context.js";
const Container = styled.div`
display: flex;
diff --git a/src/components/VideoUploader.jsx b/src/components/VideoUploader.jsx
index ddba024..b31fac3 100644
--- a/src/components/VideoUploader.jsx
+++ b/src/components/VideoUploader.jsx
@@ -4,7 +4,7 @@ import { useContext, useMemo, useRef, useState } from "react";
import SkeletonVideo from "./SkeletonVideo";
import { getSkeletons } from "../librarys/skeleton-api";
import classNames from "classnames";
-import { ReducerContext } from "../librarys/context";
+import { ReducerContext } from "../librarys/context.js";
const VideoUploadContainer = styled.div`
width: 540px;
diff --git a/src/components/player/BorderBox.jsx b/src/components/player/BorderBox.jsx
index e4eecd1..b41fea2 100644
--- a/src/components/player/BorderBox.jsx
+++ b/src/components/player/BorderBox.jsx
@@ -1,6 +1,6 @@
import { useState, useEffect, useRef, useContext } from "react";
import styled from "styled-components";
-import { StateContext } from "../../librarys/context";
+import { StateContext } from "../../librarys/context.jsx";
const Container = styled.div`
width: 100%;
diff --git a/src/components/player/Countdown.jsx b/src/components/player/Countdown.jsx
index 6cce66a..6f2260b 100644
--- a/src/components/player/Countdown.jsx
+++ b/src/components/player/Countdown.jsx
@@ -1,7 +1,7 @@
import { useState, useEffect, useRef, useContext } from "react";
import styled from "styled-components";
import PropTypes from "prop-types";
-import { StateContext } from "../../librarys/context";
+import { StateContext } from "../../librarys/context.jsx";
const Container = styled.div`
max-width: 700px;