+
+
+
+
+ }
+ />
+ ) : null}
+ >
+ );
+};
+export default ModalSheetBuilder;
+
+const Background = styled.div`
+ width: 100%;
+ height: 100%;
+ z-index: 10;
+ background: ${({ isExpanded }) =>
+ isExpanded ? "#fff" : "rgba(0, 0, 0, 0.5)"};
+ position: fixed;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ top: 0;
+ left: 0;
+`;
+
+const ModalWrapper = styled.div`
+ background: #fff;
+ color: #000;
+ display: flex;
+ z-index: 20;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ border-radius: 10px;
+ width: fit-content;
+ height: fit-content;
+ position: fixed;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+`;
diff --git a/src/presentation/components/popup/PopUp.js b/src/presentation/components/popup/PopUp.js
new file mode 100644
index 0000000..d6ad917
--- /dev/null
+++ b/src/presentation/components/popup/PopUp.js
@@ -0,0 +1,128 @@
+import React from "react";
+import styled from "styled-components";
+import SubmitBtn from "../buttons/SubmitBtn";
+import CancelBtn from "../buttons/CancelBtn";
+import ExitModalBtn from "../buttons/ExitModalBtn";
+import {
+ useDataInput,
+ useUpdateDataInput,
+} from "../../../service/providers/data_input_provider";
+
+const PopUpContainer = styled.div`
+ display: flex;
+ width: 510px;
+ height: 217px;
+ border-radius: 10px;
+ background: #fff;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+ box-shadow: 2px 2px 10px rgba(0, 0, 0, 0.3);
+`;
+const TextContainer = styled.div`
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+`;
+
+const BtnCantainer = styled.div`
+ display: flex;
+ flex-direction: row;
+ justify-content: center;
+ align-items: center;
+`;
+
+const Space = styled.div`
+ width: 10px;
+`;
+
+const FirstText = styled.p`
+ font-family: ${(props) => props.theme.fontFamily.mainfont};
+ font-weight: ${(props) => props.theme.fontWeights.bold};
+ font-size: ${(props) => props.theme.fontSizes.Body2};
+ color: ${(props) => props.theme.color.blackHigh};
+ font-style: normal;
+ flex: column;
+ text-align: center;
+ margin-bottom: 0px;
+`;
+
+const SecondText = styled.p`
+ font-family: ${(props) => props.theme.fontFamily.mainfont};
+ font-weight: ${(props) => props.theme.fontWeights.regular};
+ font-size: ${(props) => props.theme.fontSizes.Body2};
+ color: ${(props) => props.theme.color.blackHigh};
+ font-style: normal;
+ flex: column;
+ text-align: center;
+ margin-top: 8px;
+`;
+
+function PopUp({
+ imgSrc,
+ text1,
+ text2,
+ id,
+ close,
+ handleModalOpen,
+ handleSetModalType,
+}) {
+ const dataInput = useDataInput();
+ const updateDataInput = useUpdateDataInput();
+ const handleInputChange = (value) => {
+ updateDataInput("add-free", value);
+ updateDataInput("add-template-1", value);
+ updateDataInput("add-template-2", value);
+ };
+ const handleExitModal = () => {
+ handleModalOpen();
+ dataInput.isExpanded = false;
+ };
+
+ return (
+
+
+
+ {text1}
+ {text2}
+
+ {id === 1 ? (
+
+
+
+
+
+ ) : id === 3 ? (
+
+
+
+ {
+ handleSetModalType();
+ close();
+ handleInputChange(null);
+ }}
+ />
+
+ ) : id === 4 ? (
+
+
+
+ {
+ handleSetModalType();
+ close();
+ }}
+ />
+
+ ) : (
+
+ )}
+
+ );
+}
+
+export default PopUp;
diff --git a/src/presentation/components/popup/PopUpBuilder.js b/src/presentation/components/popup/PopUpBuilder.js
new file mode 100644
index 0000000..0ef44ef
--- /dev/null
+++ b/src/presentation/components/popup/PopUpBuilder.js
@@ -0,0 +1,57 @@
+import Save from "../../../assets/img/popup_save.svg";
+import Close from "../../../assets/img/popup_getout.svg";
+import Error from "../../../assets/img/popup_error.svg";
+import PopUp from "./PopUp";
+
+const popup_db = [
+ {
+ id: 0,
+ title: "경험 기록이 저장되었습니다.",
+ body: "이제 저장된 기록을 내가 원하는 형태로 볼 수 있어요!",
+ image: Save,
+ },
+ {
+ id: 1,
+ title: "기록창을 나가시겠어요?",
+ body: "작성하던 기록은 삭제되어 다시 볼 수 없어요.",
+ image: Close,
+ },
+ {
+ id: 2,
+ title: "유효하지 않은 링크입니다.",
+ body: "입력하신 링크를 다시 한 번 확인해주세요.",
+ image: Error,
+ },
+ {
+ id: 3,
+ title: "템플릿으로 입력하시겠어요?",
+ body: "작성하던 기록은 삭제되어 다시 볼 수 없어요.",
+ image: Close,
+ },
+ {
+ id: 4,
+ title: "자유로운 형식으로 입력하시겠어요?",
+ body: "작성하던 기록은 삭제되어 다시 볼 수 없어요.",
+ image: Close,
+ },
+];
+
+function PopUpBuilder({ id, close, handleModalOpen, handleSetModalType }) {
+ const popup_data = popup_db.filter((value) => value["id"] === id);
+ if (!popup_data.length > 0) return;
+
+ const data = popup_data[0];
+ return (
+
+ );
+}
+
+export default PopUpBuilder;
diff --git a/src/presentation/components/postListItem/ExplainModal.js b/src/presentation/components/postListItem/ExplainModal.js
new file mode 100644
index 0000000..7725d2c
--- /dev/null
+++ b/src/presentation/components/postListItem/ExplainModal.js
@@ -0,0 +1,63 @@
+import React, { useState } from "react";
+import styled from "styled-components";
+import LoadingImg from "../../../assets/img/loading.gif";
+
+const Container = styled.div`
+ width : 100%;
+ display : coloumn;
+ justify-content: space-between;
+ // align-items: center;
+ padding : 20px;
+ padding-top : 0px;
+`
+
+const FirstExplain = styled.div`
+ justify-content: center;
+ font-family: ${props => props.theme.fontFamily.mainfont};
+ font-weight: ${props => props.theme.fontWeights.bold};
+ font-size: ${props => props.theme.fontSizes.Header5};
+ color: ${props => props.theme.color.blackHigh};
+
+ margin-top : 27px;
+`
+const SecondExplain = styled.div`
+ justify-content: center;
+ font-family: ${props => props.theme.fontFamily.mainfont};
+ font-weight: ${props => props.theme.fontWeights.regular};
+ font-size: ${props => props.theme.fontSizes.Body1};
+ color: ${props => props.theme.color.blackHigh};
+ margin-top : 10px;
+`
+
+function ExplainModal({ userId , isLoading}) {
+
+ return (
+
+
+
+ {userId ?? "no id found"}님의 디스콰이엇 계정에서 {isLoading ? "글을 불러오는 중입니다.." : "불러온 글입니다."}
+
+
+ 아크박스 목록에 추가할 글을 선택하세요!
+
+ {isLoading && (
+
+ )}
+
+
+
+ );
+}
+
+export default ExplainModal;
\ No newline at end of file
diff --git a/src/presentation/components/postListItem/ListContainer.js b/src/presentation/components/postListItem/ListContainer.js
new file mode 100644
index 0000000..e132518
--- /dev/null
+++ b/src/presentation/components/postListItem/ListContainer.js
@@ -0,0 +1,76 @@
+import React, { useState } from "react";
+import styled from "styled-components";
+import ListItem from "./ListItem";
+import Icon_checkDefault from "../../../assets/img/Icon_checkDefault.svg";
+import Icon_checked from "../../../assets/img/Icon_checked.svg";
+
+const Container = styled.div`
+ width: 100%;
+ height: 500px;
+ overflow: auto;
+ background-color: white;
+`;
+
+const SelectAllButton = styled.button`
+ margin-left : 18px;
+ margin-bottom: 10px;
+ background: transparent;
+ border: none;
+ color: var(--primary-300, #11E3B2);
+ font-size: 16px;
+ font-family: Pretendard;
+ font-weight: 600;
+ line-height: 160%;
+ cursor: pointer;
+`;
+
+
+function ListContainer({ data, setSelectedTags, selectedTags, selectedItems, setSelectedItems }) {
+// const [selectAllIcon, setSelectAllIcon] = useState(Icon_checkDefault);
+
+ const handleSelectAll = () => {
+ if (selectedItems.length === data.items.length) {
+ setSelectedItems([]);
+ // setSelectAllIcon(Icon_checkDefault);
+ } else {
+ const itemIds = data.items.map((item) => item.id);
+ setSelectedItems(itemIds);
+ // setSelectAllIcon(Icon_checked);
+ }
+ };
+
+ const handleItemSelect = (itemId, isChecked) => {
+ if (isChecked) {
+ setSelectedItems([...selectedItems, itemId]);
+ } else {
+ setSelectedItems(selectedItems.filter((id) => id !== itemId));
+ }
+ };
+
+ console.log(data);
+
+ return (
+ <>
+
+ {/* */}
+ {selectedItems.length === data.items.length ? "전체해제" : "전체선택"}
+
+
+
+ {data.items.map((item) => (
+
+ ))}
+
+ >
+ );
+}
+
+export default ListContainer;
diff --git a/src/presentation/components/postListItem/ListItem.js b/src/presentation/components/postListItem/ListItem.js
new file mode 100644
index 0000000..fae8030
--- /dev/null
+++ b/src/presentation/components/postListItem/ListItem.js
@@ -0,0 +1,249 @@
+import React, { useState } from "react";
+import styled from "styled-components";
+import Icon_checkDefault from "../../../assets/img/Icon_checkDefault.svg";
+import Icon_checked from "../../../assets/img/Icon_checked.svg";
+
+const ListItemContainer = styled.div`
+ width: 550px;
+ height: 105px;
+ background-color: ${(props) => props.theme.color.background};
+ border-radius: 10px;
+ padding: 10px;
+ margin-bottom: 10px;
+ margin-left: 90px;
+ display: flex;
+ flex-direction: column;
+ position: relative;
+`;
+
+const TitleDateContainer = styled.div`
+ display: flex;
+ justify-content: space-between;
+ margin-bottom: 0px;
+ padding-bottom: 0px;
+`;
+
+const CheckboxIcon = styled.img`
+ width: 24px;
+ height: 24px;
+ cursor: pointer;
+`;
+
+const Title = styled.div`
+ font-family: ${(props) => props.theme.fontFamily.mainfont};
+ font-size: ${(props) => props.theme.fontSizes.Subtitle2};
+ font-weight: ${(props) => props.theme.fontWeights.bold};
+ color: ${(props) => props.theme.color.blackHigh};
+ margin-left: 14px;
+ margin-top: 20px;
+ margin-bottom: 0px;
+`;
+
+const Date = styled.div`
+ font-family: ${(props) => props.theme.fontFamily.mainfont};
+ font-size: ${(props) => props.theme.fontSizes.Body2};
+ font-weight: ${(props) => props.theme.fontWeights.semibold};
+ color: var(--disabled-1, #ababab);
+ margin-top: auto;
+`;
+
+const TagContainer = styled.div`
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+ margin-top: 20px;
+ margin-bottom: 30px;
+`;
+
+const DropdownContainer = styled.div`
+ position: relative;
+ width: 230px;
+ height: 28px;
+ flex-shrink: 0;
+ border-radius: 10px;
+ border: 1px solid var(--disabled-1, #ababab);
+ margin-left: 10px;
+`;
+
+const DropdownButton = styled.button`
+ width: 100%;
+ height: 100%;
+ background-color: ${(props) => props.theme.color.surface};
+ border-radius: 10px;
+ border: 1px solid var(--disabled-1, #ababab);
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 0 10px;
+ border: none;
+ cursor: pointer;
+ z-index: 1;
+`;
+
+const DropdownArrow = styled.span`
+ margin-left: auto;
+`;
+
+const DropdownContent = styled.div`
+ position: absolute;
+ top: 100%;
+ right: 0;
+ background-color: ${(props) => props.theme.color.surface};
+ width: 230px;
+ border-radius: 10px;
+ border: 1px solid var(--disabled-1, #ababab);
+ background: var(--surface-white, #fff);
+ padding: 5px;
+ display: "block";// 수정된 부분
+ z-index: 2;
+`;
+
+
+const TagLabel = styled.span`
+ margin-right: 5px;
+ font-family: ${(props) => props.theme.fontFamily.mainfont};
+ font-size: ${(props) => props.theme.fontSizes.Subtitle2};
+ font-weight: 450;
+ color: var(--disabled-1, #ababab);
+`;
+
+const TagListTitle = styled.h4`
+ font-size: 10px;
+ font-weight: 450;
+ color: var(--disabled-1, #ababab);
+ margin-bottom: 10px;
+ margin-left: 10px;
+ margin-top: 8px;
+`;
+
+const TagListContainer = styled.div`
+ display: flex;
+ flex-wrap: wrap;
+ padding-left: 2px;
+ margin-top: 5px;
+ padding-top: 0px;
+`;
+
+const TagItem = styled.div`
+ border: 1px solid ${(props) => props.backgroundColor};
+ background-color: ${(props) =>
+ props.isSelected ? props.backgroundColor : "transparent"};
+ color: ${(props) => (props.isSelected ? "white" : props.backgroundColor)};
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 2px 8px;
+ border-radius: 12px;
+ font-size: 12px;
+ margin-right: 5px;
+ margin-bottom: 5px;
+ cursor: pointer;
+`;
+
+const CheckIconContainer = styled.div`
+ position: absolute;
+ left: -66px;
+ top: 50%;
+ transform: translateY(-50%);
+ width: 24px;
+ height: 24px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ cursor: pointer;
+`;
+
+const CheckIcon = styled.img`
+ width: 100%;
+ height: 100%;
+`;
+
+function ListItem({ item, data, onItemSelect, isSelected, setSelectedTags, selectedTags }) {
+ const [isDropdownOpen, setIsDropdownOpen] = useState(false);
+
+ const addItemTags = (itemKey, tags) => {
+ setSelectedTags((prevTags) => ({
+ ...prevTags,
+ [itemKey]: tags,
+ }));
+ };
+
+ const handleTagClick = (tagId) => {
+ let tags;
+ if (selectedTags[item.id] !== undefined ? selectedTags[item.id].includes(tagId) : false) {
+ tags = selectedTags[item.id].filter((tag) => tag !== tagId);
+ } else {
+ if (selectedTags[item.id] !== undefined ? selectedTags[item.id].length < 2 : true) {
+ if(selectedTags[item.id] === undefined){
+ tags = [tagId];
+ }else{
+ tags = [...selectedTags[item.id], tagId];
+ }
+
+ }
+ }
+ addItemTags(item.id, tags);
+ };
+
+ const handleDropdownToggle = () => {
+ console.log("click!")
+ setIsDropdownOpen(!isDropdownOpen);
+ };
+
+
+ return (
+
+ onItemSelect(item.id, !isSelected)}>
+
+
+
+ {item.title}
+ {item.date}
+
+
+
+
+ 태그 :
+
+
+ {data.tags.map((tag) => (
+ handleTagClick(tag.id)}
+ >
+ {tag.tagName}
+
+ ))}
+
+
+ ▼
+
+ {isDropdownOpen && (
+
+ 태그선택
+
+ {data.tags.map((tag) => (
+ handleTagClick(tag.id)}
+ >
+ <>{tag.tagName}>
+
+ ))}
+
+
+ )}
+
+
+
+ );
+}
+
+export default ListItem;
diff --git a/src/presentation/components/postListItem/PostHeader.js b/src/presentation/components/postListItem/PostHeader.js
new file mode 100644
index 0000000..6b74d06
--- /dev/null
+++ b/src/presentation/components/postListItem/PostHeader.js
@@ -0,0 +1,50 @@
+import React, { useState } from "react";
+import styled from "styled-components";
+import Logo_Disquiet from "../../../assets/img/Logo_Disquiet.png";
+import CloseIcon from "../../../assets/img/close_icon.svg";
+import IconButton from "../buttons/IconBtn";
+
+const HeaderContainer = styled.div`
+ width : 100%;
+ /* height : auto; */
+ margin-top: 0px;
+ display : flex;
+ justify-content: space-between;
+ align-items: center;
+`
+
+const HeaderIcon = styled.img`
+ flex-direction: column;
+ margin-left : 10px;
+ margin-top : 5px;
+`
+const UserId = styled.div`
+ justify-content: center;
+ font-family: ${props => props.theme.fontFamily.mainfont};
+ font-weight: ${props => props.theme.fontWeights.bold};
+ font-size: ${props => props.theme.fontSizes.Subtitle1};
+ color: ${props => props.theme.color.blackHigh};
+`
+
+const CloseButton = styled(IconButton)`
+ margin-right: 100px !important;
+ margin-left: auto;
+`;
+
+function PostHeader({ userId, closeModal }) {
+
+
+ return (
+
+
+ {userId ?? "no id found"}
+
+
+ );
+}
+
+export default PostHeader;
\ No newline at end of file
diff --git a/src/presentation/components/postListItem/index.js b/src/presentation/components/postListItem/index.js
new file mode 100644
index 0000000..8bc2a3d
--- /dev/null
+++ b/src/presentation/components/postListItem/index.js
@@ -0,0 +1,205 @@
+import React, { useEffect, useState } from "react";
+import styled from "styled-components";
+import axios from "axios";
+import firebase from "firebase/compat/app";
+
+import PostHeader from "./PostHeader";
+import { Divider } from "@mui/material";
+import ExplainModal from "./ExplainModal";
+import ListContainer from "./ListContainer";
+import postService from "../../../service/firebase/PostService";
+import { tags, FetchUserTags } from "../../../constants/tags";
+import { useUser } from "../../../service/providers/auth_provider";
+import { grey } from "@mui/material/colors";
+
+const ModalContainer = styled.div`
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+ justify-content: space-between;
+ width: 690px;
+ height: 770px;
+ background-color: white;
+ border-radius: 15px;
+ padding-left: 30px;
+ padding-right: 30px;
+ padding-bottom: 30px;
+ padding-top: 10px;
+`;
+
+const SubmitButton = styled.button`
+ margin-top: 15px;
+ align-self: flex-end;
+ border-radius: 100px;
+ border: none;
+ background: ${(props) =>
+ props.length ? props.theme.color.primary300 : grey[400]};
+ width: 122.674px;
+ height: 45.196px;
+ justify-content: center;
+ align-items: center;
+ display: flex;
+ font-family: ${(props) => props.theme.fontFamily.mainfont};
+ font-weight: ${(props) => props.theme.fontWeights.semibold};
+ font-size: ${(props) => props.theme.fontSizes.Body1};
+ color: white;
+ cursor: ${(props) => (props.length ? null : "pointer")};
+ pointer-events: ${(props) => (props.length ? "auto" : "none")};
+`;
+
+const ErrorMsg = styled.div`
+ font-family: ${(props) => props.theme.fontFamily.mainfont};
+ font-weight: ${(props) => props.theme.fontWeights.bold};
+ font-size: ${(props) => props.theme.fontSizes.Header4};
+ color: ${(props) => props.theme.color.error};
+ position: fixed;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+`;
+
+function ListModal({fetchTags, disquiteId, closeModal, handleSnack }) {
+ const [isLoading, setIsLoading] = useState(true);
+ const [crawledData, setCrawledData] = useState([]);
+ const [selectedTags, setSelectedTags] = useState({});
+ const [selectedItems, setSelectedItems] = useState([]);
+ const [isCorrectId, setIsCorrectId] = useState(true);
+ const [progressMsg, setProgreeMsg] = useState(null);
+
+ const user = useUser();
+
+ console.log('fetchTags', fetchTags);
+
+
+ const addMetaData = (data) => {
+
+ const updatedData = {
+ userId: disquiteId,
+ items: [...data],
+ tags: [...fetchTags],
+ };
+ setCrawledData(updatedData);
+ };
+
+ const fetchCrawledData = async () => {
+ try {
+ if (!disquiteId) {
+ throw new Error("Disquite Id is null");
+ }
+ const response = await axios.post("http://0.0.0.0:8000/crawl", {
+ disquite_id: disquiteId,
+ });
+ addMetaData(response.data);
+ setIsLoading(false);
+ } catch (error) {
+ console.log("ErrorTest:", error);
+ if (error.message === "Disquite Id is null") {
+ //아이디가 빈값일 때
+ setIsCorrectId(false);
+ } else {
+ //아이디가 없는 아이디 일 때
+ setIsCorrectId(false);
+ }
+ } finally {
+ }
+ };
+
+ // request data to the crawler
+ useEffect(() => {
+ fetchCrawledData();
+ }, []);
+
+ const handleSubmit = () => {
+ const items = crawledData.items.filter((it) =>
+ selectedItems.includes(it.id)
+ );
+ const result = items.map((item, index) => {
+ const dateRowForm = item.date;
+ const [month, day, year] = dateRowForm.split("/");
+ const dateForm = new Date(year, month - 1, day);
+ item.date = firebase.firestore.Timestamp.fromDate(dateForm);
+
+ if (selectedTags[item.id] === undefined) {
+ return {
+ ...item,
+ "selected-tags": [{ tagName: "디스콰이엇", color: "#8560F6" }], // Add an empty "selected-tags" property to each item
+ };
+ } else {
+ const filteredTags = fetchTags.filter((tag) =>
+ selectedTags[item.id].includes(tag.id)
+ );
+ return {
+ ...item,
+ "selected-tags": [
+ ...filteredTags,
+ { tagName: "디스콰이엇", color: "#8560F6" },
+ ], // Add an empty "selected-tags" property to each item
+ };
+ }
+ });
+
+ const userId = user.uid;
+ result.forEach((element) => {
+ console.log(element);
+ postService.createPost(userId, element);
+ });
+ closeModal();
+ handleSnack();
+ };
+
+ return (
+
+
+
+
+
+ {isCorrectId ? (
+
+ ) : (
+ 유효하지 않은 ID입니다.
+ )}
+
+ {!isLoading && (
+
+ )}
+
+ 추가하기
+
+
+
+
+ );
+}
+
+export default ListModal;
+
+const Background = styled.div`
+ width: 100%;
+ height: 100%;
+ z-index: 10;
+ background: rgba(0, 0, 0, 0.5);
+ position: fixed;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ top: 0;
+ left: 0;
+`;
diff --git a/src/presentation/components/timeline/CardBuilder.js b/src/presentation/components/timeline/CardBuilder.js
new file mode 100644
index 0000000..094b3eb
--- /dev/null
+++ b/src/presentation/components/timeline/CardBuilder.js
@@ -0,0 +1,21 @@
+const lxSize = "lx";
+const largeSize = "l";
+const mediumSize = "m";
+const smallSize = "s";
+
+const CardBuilder = ({size}) => {
+ switch(size){
+ case lxSize:
+ // TODO : return extra large size card
+ case largeSize:
+ // TODO : return large size card
+ case mediumSize:
+ // TODO : return medium size card
+ case smallSize:
+ // TODO : return small size card
+ default:
+ // TODO : return Large size card
+ }
+}
+
+export {CardBuilder, lxSize, largeSize, mediumSize, smallSize};
\ No newline at end of file
diff --git a/src/presentation/components/timeline/CardWrapper.js b/src/presentation/components/timeline/CardWrapper.js
new file mode 100644
index 0000000..5416b2d
--- /dev/null
+++ b/src/presentation/components/timeline/CardWrapper.js
@@ -0,0 +1,124 @@
+import { styled } from "styled-components";
+import { lxSize, largeSize, mediumSize, smallSize } from "./CardBuilder";
+import ExperienceCardSelfSmall from "../commons/ExperienceCardSelfSmall";
+import ExperienceCardSelfMiddle from "../commons/ExperienceCardSelfMiddle";
+import ExperienceCardSelf from "../commons/ExperienceCardSelf";
+import ExperienceCardSelfExtraSmall from "../commons/ExperienceCardSelfExtraSmall";
+
+
+
+const CardWrapper = ({ setPostData, isAbove, mode, postDataList, handleDotClick }) => {
+
+ const renderCards = (dataList) => {
+ switch (mode) {
+ case lxSize:
+ return dataList.map((post) => (
+
{
+ handleDotClick();
+ setPostData(post);
+ }} >
+
+
+ ));
+ case largeSize:
+ return dataList.map((post) => (
+
{
+ handleDotClick();
+ setPostData(post);
+ }}>
+
+
+ ));
+ case mediumSize:
+ return dataList.map((post) => (
+
{
+ handleDotClick();
+ setPostData(post);
+ }}>
+
+
+ ));
+ case smallSize:
+ return dataList.map((post) => (
+
{
+ handleDotClick();
+ setPostData(post);
+ }}>
+
+
+ ));
+ default:
+ return dataList.map((post) => (
+
{
+ handleDotClick();
+ setPostData(post);
+ }}>
+
+
+ ));
+ }
+ };
+
+ if (mode === smallSize) {
+ const columns = Math.ceil(postDataList.length / 8);
+ const columnArrays = Array.from({ length: columns }, () => []);
+ postDataList.forEach((post, index) => {
+ const columnIndex = index % columns;
+ columnArrays[columnIndex].push(post);
+ });
+
+ return (
+
+ {columnArrays.map((column, index) => (
+
+ {renderCards(column)}
+
+ ))}
+
+ );
+ } else {
+ return (
+
+ {renderCards(postDataList)}
+
+ );
+ }
+};
+
+const Column = styled.div`
+ display: flex;
+ flex-direction: column;
+ flex-basis: 100%;
+ flex: 1;
+ width: calc(25% - 4px); /* Adjust the width and margin as needed */
+ margin: 2px;
+
+`;
+
+const Wrapper = styled.div`
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+
+ flex-direction: ${({ isAbove }) => (isAbove ? "column-reverse" : "column")};
+ position: absolute;
+ font-size: 12px;
+ ${({ isAbove }) => (isAbove ? "bottom: 58%;" : "top: 58%;")};
+`;
+
+const Container = styled.div`
+ display: flex;
+ justify-content: center;
+ position: absolute;
+ font-size: 12px;
+ ${({ isAbove }) => (isAbove ? "bottom: 58%;" : "top: 58%;")};
+`;
+
+const gapList = {
+ [lxSize]: "0px",
+ [largeSize]: "14px",
+ [mediumSize]: "11px",
+ [smallSize]: "6px",
+};
+
+export default CardWrapper;
\ No newline at end of file
diff --git a/src/presentation/components/timeline/grouping_functions.js b/src/presentation/components/timeline/grouping_functions.js
new file mode 100644
index 0000000..029c9a4
--- /dev/null
+++ b/src/presentation/components/timeline/grouping_functions.js
@@ -0,0 +1,96 @@
+import { format, startOfDay, getWeek} from "date-fns";
+
+function getYearMonthWeek(date) {
+ const year = date.getFullYear(); // 년도
+ const month = date.getMonth() + 1; // 월 (0부터 시작하므로 +1)
+ const firstDayOfMonth = new Date(year, date.getMonth(), 1); // 해당 월의 첫 날
+ const diff = date.getDate() + firstDayOfMonth.getDay() - 1;
+ const week = Math.ceil(diff / 7); // 해당 월의 몇 주차인지 계산
+
+ return {
+ year,
+ month,
+ week
+ };
+}
+
+// Function to convert map data to the desired format by days
+const groupDataByDay = (data) => {
+ const groupedData = {};
+
+ for (const [, value] of data.entries()) {
+ const date = startOfDay(value.date.toDate());
+
+ const formattedDate = format(date, "MM/dd/yyyy");
+ const [month, day, year] = formattedDate.split('/');
+ const dateStr = `${year}년 ${month}월 ${day}일`
+
+ if (!groupedData[dateStr]) {
+ groupedData[dateStr] = [];
+ }
+
+ groupedData[dateStr].push(value);
+ }
+
+ return groupedData;
+};
+
+// Function to convert map data to the desired format by weeks
+const groupDataByWeek = (data) => {
+ const groupedData = {};
+
+ for (const [, value] of data.entries()) {
+ const date = value.date.toDate();
+ const { year, month, week } = getYearMonthWeek(date);
+ const dateStr = `${year}년 ${month}월 ${week}주차`
+
+ if (!groupedData[dateStr]) {
+ groupedData[dateStr] = [];
+ }
+
+ groupedData[dateStr].push(value);
+ }
+
+ return groupedData;
+};
+
+// Function to convert map data to the desired format by months
+const groupDataByMonth = (data) => {
+ const groupedData = {};
+
+ for (const [, value] of data.entries()) {
+ const date = value.date.toDate();
+ const formattedMonth = format(date, "MM/yyyy");
+ const [month, year] = formattedMonth.split('/');
+
+ const dateStr = `${year}년 ${month}월`;
+ if (!groupedData[dateStr]) {
+ groupedData[dateStr] = [];
+ }
+
+ groupedData[dateStr].push(value);
+ }
+
+ return groupedData;
+};
+
+// Function to convert map data to the desired format by years
+const groupDataByYear = (data) => {
+ const groupedData = {};
+
+ for (const [, value] of data.entries()) {
+ const date = value.date.toDate();
+ const formattedYear = format(date, "yyyy");
+
+ const dateStr = `${formattedYear}년`;
+ if (!groupedData[dateStr]) {
+ groupedData[dateStr] = [];
+ }
+
+ groupedData[dateStr].push(value);
+ }
+
+ return groupedData;
+};
+
+export {groupDataByDay, groupDataByWeek, groupDataByMonth, groupDataByYear};
diff --git a/src/presentation/components/timeline/index.js b/src/presentation/components/timeline/index.js
new file mode 100644
index 0000000..5108e94
--- /dev/null
+++ b/src/presentation/components/timeline/index.js
@@ -0,0 +1,474 @@
+import React, { useEffect, useState, useRef } from "react";
+import firebase from "firebase/compat/app";
+import { keyframes, css } from "styled-components";
+import styled from "styled-components";
+import { ReactComponent as TimelineDot } from "../../../assets/img/timeline_dot.svg";
+import {
+ groupDataByDay,
+ groupDataByMonth,
+ groupDataByWeek,
+ groupDataByYear,
+} from "./grouping_functions";
+import {
+ useTimelineData,
+ useUpdateTimelineData,
+} from "../../../service/providers/timeline_data_provider";
+import CardWrapper from "./CardWrapper";
+
+import { lxSize, largeSize, mediumSize, smallSize } from "./CardBuilder";
+import GoToFirstIcon from "../../../assets/img/GoToFirstIcon.svg";
+import GotoLastIcon from "../../../assets/img/GotoLastIcon.svg";
+import GoToDateIcon from "../../../assets/img/GoToDateIcon.svg";
+import ModalView from "../modal/ModalView";
+import { useUser } from "../../../service/providers/auth_provider";
+import { useDataInput } from "../../../service/providers/data_input_provider";
+import CircularIndicator from "../loadingIndicator/CircularIndicator";
+
+const TimelineContainer = styled.div`
+ display: flex;
+ overflow-x: scroll;
+ overflow-y: hidden;
+ scroll-snap-type: x mandatory;
+ width: 100%;
+ height: 100%;
+ position: relative; /* Add relative positioning */
+ /* margin-left: 18px; 디자인 수정사항 */
+ margin-right: 18px;
+ margin-bottom: 18px;
+ font-weight: ${(props) => props.theme.fontWeights.semibold};
+ /* Remove scrollbar */
+ scrollbar-width: none;
+ -ms-overflow-style: none;
+ &::-webkit-scrollbar {
+ display: none;
+ }
+`;
+
+const TransparentButton = styled.button`
+ position: fixed;
+ z-index: 5;
+ background-color: transparent;
+ border: none;
+ cursor: pointer;
+ align-items: center;
+ display: inline-flex;
+ margin-top: 10px;
+`;
+const ButtonText = styled.span`
+ color: ${(props) => props.theme.color.primary400};
+ font-family: ${(props) => props.theme.fontFamily.mainfont};
+ font-size: ${(props) => props.theme.fontSizes.Subtitle2};
+ font-weight: ${(props) => props.theme.fontWeights.semibold};
+ margin-left: 6px;
+ margin-right: 6px;
+`;
+const FirstButton = styled(TransparentButton)`
+ right: 180px;
+`;
+const LastButton = styled(TransparentButton)`
+ right: 67px;
+`;
+const FirstIcon = styled.img`
+ width: 12px;
+ height: 12px;
+ margin-top: 1px;
+ opacity: 80%;
+`;
+const LastIcon = styled.img`
+ width: 12px;
+ height: 12px;
+ margin-bottom: 0px;
+ opacity: 80%;
+`;
+
+const HorizontalLines = styled.div`
+ position: absolute; /* Use absolute positioning */
+ top: 50%; /* Position at the vertical center */
+ left: 0;
+ width: ${({ lineWidth }) => lineWidth}px;
+ height: 2px;
+ background-color: #ccc;
+ transform: translateY(-50%); /* Adjust the vertical alignment */
+ z-index: -1;
+`;
+const DotContainer = styled.div`
+ scroll-snap-align: center;
+ display: flex;
+ flex-shrink: 0;
+ width: ${({ dotWidth }) => dotWidth}px;
+ background-color: transparent;
+`;
+
+const DotTimeWrapper = styled.div`
+ z-index: 0;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center; /* Center both vertically and horizontally */
+ margin: auto; /* Center the dot horizontally */
+`;
+
+const brightenAnimation = keyframes`
+ 0% { opacity: 0.7; }
+ 50% { opacity: 1; }
+ 100% { opacity: 0.7; }
+`;
+
+const darkenAnimation = keyframes`
+ 0% { opacity: 1; }
+ 50% { opacity: 0.7; }
+ 100% { opacity: 1; }
+`;
+
+const scaleAnimation = keyframes`
+ 0% { transform: scale(1); }
+ 50% { transform: scale(1.1); }
+ 100% { transform: scale(1); }
+`;
+
+const Dot = styled(TimelineDot)`
+ width: ${({ size }) => {
+ if (size === "day") {
+ return "40px";
+ } else if (size === "week") {
+ return "55px";
+ } else if (size === "month") {
+ return "70px";
+ } else if (size === "year") {
+ return "90px";
+ }
+ }};
+
+ height: ${({ size }) => {
+ if (size === "day") {
+ return "40px";
+ } else if (size === "week") {
+ return "55px";
+ } else if (size === "month") {
+ return "70px";
+ } else if (size === "year") {
+ return "90px";
+ }
+ }};
+
+ animation: ${({ size }) => {
+ if (
+ size === "day" ||
+ size === "week" ||
+ size === "month" ||
+ size === "year"
+ ) {
+ return css`
+ ${brightenAnimation} 2s linear infinite, ${scaleAnimation} 1.5s linear infinite
+ `;
+ }
+ }};
+
+ &:hover {
+ animation: ${({ size }) => {
+ if (
+ size === "day" ||
+ size === "week" ||
+ size === "month" ||
+ size === "year"
+ ) {
+ return css`
+ ${darkenAnimation} 2s linear infinite
+ `;
+ }
+ }};
+ }
+`;
+
+const Time = styled.div`
+ position: absolute;
+ font-size: 12px;
+ white-space: nowrap;
+ margin-top: ${({ locate, isAbove }) => {
+ let topValue;
+ if (locate === "day") {
+ topValue = isAbove ? "-60px" : "60px";
+ } else if (locate === "week") {
+ topValue = isAbove ? "-80px" : "80px";
+ } else if (locate === "month") {
+ topValue = isAbove ? "-97px" : "97px";
+ } else if (locate === "year") {
+ topValue = isAbove ? "-117px" : "117px";
+ }
+ return topValue;
+ }};
+`;
+
+function formatDate(date) {
+ const month = String(date.getMonth() + 1).padStart(2, "0");
+ const day = String(date.getDate()).padStart(2, "0");
+ const year = date.getFullYear();
+
+ return `${month}/${day}/${year}`;
+}
+
+const TimelineDataBuilder = () => {
+ const timelineData = useTimelineData();
+
+ let targetData;
+ switch (timelineData["grouping"]) {
+ case "year":
+ targetData = timelineData["postGroupByYear"];
+ break;
+ case "month":
+ targetData = timelineData["postGroupByMonth"];
+ break;
+ case "week":
+ targetData = timelineData["postGroupByWeek"];
+ break;
+ case "day":
+ targetData = timelineData["postGroupByDay"];
+ break;
+ default:
+ targetData = timelineData["postGroupByDay"];
+ break;
+ }
+ if (!targetData) return [];
+
+ targetData = Object.entries(targetData);
+
+ if (targetData.length === 0) {
+ return targetData;
+ }
+
+ if (timelineData["selected-tags"] && timelineData["selected-tags"][0]) {
+ let selectedTags = timelineData["selected-tags"];
+ if (
+ timelineData["selected-hashs"] !== undefined &&
+ timelineData["selected-hashs"] != null
+ ) {
+ selectedTags = [...selectedTags, ...timelineData["selected-hashs"]];
+ }
+ const filteredData = targetData.filter((element) => {
+ return element[1].some((item) => {
+ if (item["selected-tags"]) {
+ return selectedTags.every((tag) => {
+ return item["selected-tags"].some((itemTag) => {
+ return (
+ itemTag.tagName === tag.tagName && itemTag.color === tag.color
+ );
+ });
+ });
+ }
+ return false;
+ });
+ });
+ return filteredData;
+ }
+
+ return targetData;
+};
+
+const CardSizeBuilder = (size) => {
+ if (size <= 1) {
+ return lxSize;
+ } else if (size <= 2) {
+ return largeSize;
+ } else if (size <= 3) {
+ return mediumSize;
+ } else {
+ return smallSize;
+ }
+};
+
+const Timeline = ({ rerender, setRerender }) => {
+ const [selectedDotData, setSelectedDotData] = useState(null);
+ const [isCardCliked, setIsCardCliked] = useState(false);
+ const handleDotClick = () => {
+ setIsCardCliked(!isCardCliked);
+ };
+ const user = useUser();
+ const dataInput = useDataInput();
+
+ const timelineData = useTimelineData();
+
+ const timelineContainerRef = useRef(null);
+
+ const [isPostDeleted, setIsPostDeleted] = useState(false);
+
+ const [dotWidth, setDotWidth] = useState(0);
+
+ const [isLoading, setIsLoading] = useState(true);
+
+ const updateDataInput = useUpdateTimelineData();
+ const handleTimelineDataChange = (name, value) => {
+ updateDataInput(name, value);
+ };
+
+ const timelinePostData = TimelineDataBuilder();
+
+ useEffect(() => {
+ const fetchPosts = async () => {
+ setIsLoading(true);
+
+ try {
+ const dateStr = "06/09/2023";
+
+ const userId = user.uid;
+ const [month, day, year] = dateStr.split("/");
+ const givenDate = new Date(year, month - 1, day);
+ const collectionRef = firebase.firestore().collection("posts");
+ let query = collectionRef
+ .where("userId", "==", userId)
+ .orderBy("date", "asc");
+ const querySnapshot = await query.get();
+ const fetchedPosts = querySnapshot.docs.map((doc) => ({
+ docId: doc.id,
+ ...doc.data(),
+ }));
+ handleTimelineDataChange(
+ "postGroupByDay",
+ groupDataByDay(fetchedPosts)
+ );
+ handleTimelineDataChange(
+ "postGroupByWeek",
+ groupDataByWeek(fetchedPosts)
+ );
+ handleTimelineDataChange(
+ "postGroupByMonth",
+ groupDataByMonth(fetchedPosts)
+ );
+ handleTimelineDataChange(
+ "postGroupByYear",
+ groupDataByYear(fetchedPosts)
+ );
+ } catch (error) {
+ console.error("Error fetching posts:", error);
+ }
+
+ setIsLoading(false);
+ };
+
+ fetchPosts();
+ setIsPostDeleted(false);
+ setRerender(false);
+ }, [isPostDeleted, rerender]);
+
+ const handlePostDelete = () => {
+ setIsPostDeleted(true);
+ };
+
+ const buttonOnClick = (mode) => {
+ const timelineContainer = timelineContainerRef.current;
+ if (timelineContainer === null) return;
+ const targetDate = dataInput["date-navigate"]; // Target date to navigate to
+ console.log("got data: ", targetDate, mode);
+ let targetIndex;
+
+ switch (mode) {
+ case "end":
+ targetIndex = dataLength - 1;
+ break;
+ case "start":
+ targetIndex = 0;
+ break;
+ case "date":
+ if (targetDate === null) return;
+ targetIndex = timelinePostData.findIndex((item) => {
+ return item[0] >= targetDate;
+ });
+ console.log(targetIndex);
+ break;
+ default:
+ return; //Do nothing
+ }
+
+ // Calculate the position of the target date on the timeline
+ const targetPosition = targetIndex * dotWidth;
+
+ // Scroll to the target position on the timeline
+ timelineContainer.scrollTo({
+ left: targetPosition,
+ behavior: "smooth", // Optional, for smooth scrolling effect
+ });
+ };
+
+ useEffect(() => {
+ if (!timelinePostData || !timelineContainerRef.current) {
+ return; // Exit early if posts or timelineContainerRef.current is not available
+ }
+
+ const calculateDotWidth = () => {
+ const containerWidth =
+ timelineContainerRef.current.getBoundingClientRect().width;
+ const dotCount = Math.min(dataLength, 7);
+ const calculatedDotWidth = containerWidth / dotCount;
+ setDotWidth(calculatedDotWidth);
+ };
+
+ calculateDotWidth();
+ window.addEventListener("resize", calculateDotWidth);
+ return () => {
+ window.removeEventListener("resize", calculateDotWidth);
+ };
+ }, [timelinePostData, timelineContainerRef.current]);
+
+ useEffect(() => {
+ buttonOnClick(dataInput["date-navigate"] !== null ? "date" : "start");
+ }, [dataInput["date-navigate"]]);
+
+ if (isLoading) {
+ return
loading..
; // Render a loading state or return null while the data is being fetched
+ }
+
+ const dataLength = timelinePostData.length;
+
+ return (
+ <>
+
+ buttonOnClick("start")}>
+
+ 첫 기록
+
+ buttonOnClick("end")}>
+ 마지막 기록
+
+
+
+ {timelinePostData.length === 0 ? (
+
+ ) : (
+ <>>
+ )}
+ {timelinePostData.map((entry, index) => {
+ const cardSize = Object.entries(entry[1]).length;
+ return (
+
+
+
+
+
+ {entry[0]}
+
+
+
+
+ );
+ })}
+
+ {isCardCliked && (
+
setIsPostDeleted(true)}
+ />
+ )}
+ >
+ );
+};
+
+export default Timeline;
diff --git a/src/presentation/pages/FakeHomePage.js b/src/presentation/pages/FakeHomePage.js
new file mode 100644
index 0000000..4640b8f
--- /dev/null
+++ b/src/presentation/pages/FakeHomePage.js
@@ -0,0 +1,44 @@
+// import React, { useState } from "react";
+// import styled from "styled-components";
+// import { TestModal } from "../components/modal/TestModal";
+
+// const Container = styled.div`
+// display: flex;
+// justify-content: center;
+// align-items: center;
+// height: 100vh;
+// `;
+
+// const Button = styled.button`
+// min-width: 100px;
+// padding: 16px 32px;
+// border-radius: 4px;
+// border: none;
+// background: #141414;
+// color: #fff;
+// font-size: 24px;
+// cursor: pointer;
+// `;
+
+// function FakeHomePage() {
+// const [showModal, setShowModal] = useState(false);
+
+// const openModal = () => {
+// setShowModal((showModal) => !showModal);
+// };
+
+// return (
+// <>
+//
+// I'm a modal
+//
+//
+// >
+// );
+// }
+
+// export default FakeHomePage;
diff --git a/src/presentation/pages/HomePage.js b/src/presentation/pages/HomePage.js
index 71bea83..36d2abe 100644
--- a/src/presentation/pages/HomePage.js
+++ b/src/presentation/pages/HomePage.js
@@ -1,16 +1,159 @@
-import ModalContent2 from '../components/ModalContent2';
-import TmpSaveBtn from '../components/TmpSaveBtn';
-import SubmitBtn from '../components/SubmitBtn';
-import InputTitle from '../components/InputTitle';
+import React from "react";
+import styled from "styled-components";
+import Snackbar from "@mui/material/Snackbar";
+import Alert from "@mui/material/Alert";
+import BackgroundImg from "../../assets/img/MainBackground.png";
+
+import CategoryBuilder from "../components/catagory";
+import GlobalNavBar from "../components/Nav/GlobalNavBar";
+import Header from "../components/header/Header";
+import ExperienceCardLinkMiddle from "../components/commons/ExperienceCardLinkMiddle";
+import Timeline from "../components/timeline/index";
+import { TimelineDataProvider } from "../../service/providers/timeline_data_provider";
+
+import ListModal from "../components/postListItem";
+
+import LinkBox from "../components/modal/LinkBox";
+import LoginPage from "./LoginPage";
+import { DataInputProvider } from "../../service/providers/data_input_provider";
+
+const BackgroundContainer = styled.div`
+ width: 100%;
+ height: 100vh;
+ overflow: hidden;
+ position: relative;
+`;
+
+const Background = styled.img`
+ width: 100%;
+ height: 100%;
+ position: absolute;
+ opacity: 80%;
+ top: 0;
+ left: 0;
+ z-index: -1;
+`;
+const TestBox = styled.div`
+ width: 100%;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ background-color: gray;
+ margin-bottom: 300px;
+`;
+
+const exampleCrawledData = {
+ "add-free": null,
+ "add-link": null,
+ "tag-is": true, // 링크작성 : 테그 유무에 따라 로고 vs 테그 보여지는거 달라짐. -> "코드 추가 완료"
+ "img-is": true, // 직접작성일 때, 이미지 유무에 따라 summary 길이 달라져야함. -> "코드 추가 작성필요."
+ summary:
+ "summary 입니다. summary 입니다. summary 입니다. summary 입니다. summary 입니다. summary 입니다. summary 입니다. summary 입니다. ",
+ "crawled-website": "disquiet",
+ date: "06/25/2023",
+ "selected-tags": [
+ {
+ color: "#8560F6",
+ tagName: "리더십",
+ },
+ {
+ color: "#ED735D",
+ tagName: "협업",
+ },
+ ],
+ title: "경험card - 링크로 기록경험card - 링크로 기록경험card",
+ userId: "jshooni",
+ imgSrc:
+ "https://img.seoul.co.kr//img/upload/2023/03/19/SSC_20230319153307.jpg", //직접작성(제일큰거)일 때, img 주소
+};
function HomePage() {
- return (
-
-
-
-
-
-
- );
- }
- export default HomePage;
+ const [open, setOpen] = React.useState(false);
+ const [rerender, setRerender] = React.useState(false);
+
+const handleSetRerender = () => {
+ setRerender(true);
+ };
+
+ const handleSnack = () => {
+ setOpen(true);
+ setRerender(true);
+ };
+
+ const handleClose = (event, reason) => {
+ if (reason === "clickaway") {
+ return;
+ }
+
+ setOpen(false);
+ };
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+ 기록이 저장되었습니다!
+
+
+
+
+ );
+}
+
+export default HomePage;
diff --git a/src/presentation/pages/LoginPage.js b/src/presentation/pages/LoginPage.js
new file mode 100644
index 0000000..1ce2401
--- /dev/null
+++ b/src/presentation/pages/LoginPage.js
@@ -0,0 +1,70 @@
+import React from "react";
+import styled from "styled-components";
+import BackgroundImg from "../../assets/img/MainBackground.png";
+// import { signInWithGoogle, logout, addAuthStateChangedListener } from "../../../service/firebase/auth";
+import Login_LogoImg from "../../assets/img/Login_Logo.svg";
+import LogoPageIMG from "../../assets/img/LogoPageIMG.svg";
+import CustomGoogleBtn from "../components/buttons/GoogleBtn";
+
+const googleButtonStyle = {
+ borderRadius: '100px',
+};
+
+
+const BackgroundContainer = styled.div`
+ width: 100%;
+ height: 100vh;
+ overflow: hidden;
+ z-index: -10;
+`;
+const Background = styled.img`
+ width: 100%;
+ height: 100%;
+ position: absolute;
+ opacity: 80%;
+ top: 0;
+ left: 0;
+ z-index: -1;
+`;
+
+const Logobox = styled.div`
+ width: 96px;
+ height: 25px;
+ margin-top : 63px;
+ margin-left : 74px;
+`
+const LogoImg = styled.img`
+ width : 100%;
+ height : 100%;
+`
+
+const GroupIMGbox = styled.div`
+text-align: center;
+margin-top : 70px;
+`
+const GroupIMG = styled.img`
+width: 80%;
+height: 80%;
+`
+
+const LoginPage = () => {
+
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default LoginPage;
diff --git a/src/service/chatgpt/chatgpt_service.js b/src/service/chatgpt/chatgpt_service.js
new file mode 100644
index 0000000..9f22e7e
--- /dev/null
+++ b/src/service/chatgpt/chatgpt_service.js
@@ -0,0 +1,65 @@
+import get_openai_api_key from '../../constants/openai_api_key';
+import { Configuration, OpenAIApi } from 'openai';
+
+let openaiInstance = null;
+
+const initializeOpenAI = async () => {
+ if (openaiInstance) {
+ return openaiInstance;
+ }
+
+ const apiKey = await get_openai_api_key();
+
+ const configuration = new Configuration({
+ apiKey: apiKey,
+ });
+
+ delete configuration.baseOptions.headers['User-Agent'];
+
+ openaiInstance = new OpenAIApi(configuration);
+ return openaiInstance;
+};
+
+const systemPrompt = [
+ "I am going to provide a text and I want you to summarize the given text.",
+ "Note that the summarized text will be used as a summarized content of a post before reading the whole content. So people should be able to assume what the post is about.",
+ "Note that you should answer in the same language that the user role used",
+];
+
+const requestSummarize = async (maxNumLetter, targetMessage) => {
+ const maxNumLetterPrompt = `Note that the summarized text should be under ${maxNumLetter} number of letters`;
+ const targetSystemPrompt = [...systemPrompt, maxNumLetterPrompt];
+
+ let messages = targetSystemPrompt.map((prompt) => ({ role: 'system', content: prompt }));
+ const userMessage = { role: 'user', content: targetMessage };
+
+ messages = [...messages, userMessage];
+
+ console.log('openai init..')
+
+ const openai = await initializeOpenAI();
+
+ console.log('openai init done!')
+
+ return generateChatResponse(openai, messages);
+};
+
+const generateChatResponse = async (openai, messages) => {
+ try {
+ console.log('openai summrize request')
+ const response = await openai.createChatCompletion({
+ model: 'gpt-3.5-turbo',
+ messages: messages,
+ });
+ console.log('openai summrize request done!')
+ console.log(response);
+ const chatResponse = response.data.choices[0].message.content;
+ console.log(chatResponse);
+ return chatResponse;
+ } catch (error) {
+ console.error('Error generating chat response:', error);
+ throw error;
+ }
+};
+
+export { requestSummarize, generateChatResponse };
diff --git a/src/service/disquite_api_form.js b/src/service/disquite_api_form.js
new file mode 100644
index 0000000..630f034
--- /dev/null
+++ b/src/service/disquite_api_form.js
@@ -0,0 +1,65 @@
+import React, { useState } from 'react';
+import axios from 'axios';
+
+const CrawlerForm = ({ onCrawl }) => {
+ const [disquiteId, setDisquiteId] = useState('');
+
+ const handleSubmit = async (e) => {
+ e.preventDefault();
+ try {
+ const response = await axios.post('http://127.0.0.1:5000/crawl', { disquite_id: disquiteId });
+ onCrawl(response.data);
+ } catch (error) {
+ console.log('Error:', error);
+ // Handle the error condition
+ }
+ };
+
+ return (
+
+ );
+};
+
+// Rest of the code remains the same
+
+
+const CrawledData = ({ data }) => {
+ return (
+
+
Crawled Data
+ {data.map((item, index) => (
+
+
Date: {item.date}
+
Title: {item.title}
+
URL: {item.url}
+
+
+ ))}
+
+ );
+};
+
+const DisquiteCrawlerForm = () => {
+ const [crawledData, setCrawledData] = useState([]);
+
+ const handleCrawl = (data) => {
+ setCrawledData(data);
+ };
+
+ return (
+
+
+
+
+ );
+};
+
+export default DisquiteCrawlerForm;
diff --git a/src/service/firebase/FirebaseService.js b/src/service/firebase/FirebaseService.js
new file mode 100644
index 0000000..7818f24
--- /dev/null
+++ b/src/service/firebase/FirebaseService.js
@@ -0,0 +1,90 @@
+import firebase from 'firebase/compat/app';
+import 'firebase/compat/firestore';
+import { firebaseConfig } from './firebaseConfig';
+
+class FirebaseService {
+ constructor() {
+ if (!FirebaseService.instance) {
+
+ // Initialize Firebase
+ firebase.initializeApp(firebaseConfig);
+ this.firestore = firebase.firestore();
+
+ FirebaseService.instance = this;
+ }
+
+ return FirebaseService.instance;
+ }
+
+ // Create a document
+ async createDocument(collection, data) {
+ try {
+ const docRef = await this.firestore.collection(collection).add(data);
+ return docRef.id;
+ } catch (error) {
+ console.error('Error creating document:', error);
+ throw error;
+ }
+ }
+
+ // Read a document by ID
+ async getDocument(collection, id) {
+ try {
+ const docRef = await this.firestore.collection(collection).doc(id).get();
+ if (docRef.exists) {
+ return { id: docRef.id, ...docRef.data() };
+ }
+ return null;
+ } catch (error) {
+ console.error('Error getting document:', error);
+ throw error;
+ }
+ }
+
+ // Read multiple documents based on a provided where clause
+ async getDocumentsByQuery(collection, whereClause) {
+ try {
+ const collectionRef = this.firestore.collection(collection);
+ const querySnapshot = await collectionRef.where(...whereClause).get();
+
+ const documents = [];
+ querySnapshot.forEach((doc) => {
+ documents.push({ id: doc.id, ...doc.data() });
+ });
+
+ return documents;
+ } catch (error) {
+ console.error('Error getting documents:', error);
+ throw error;
+ }
+ }
+
+ // Update a document
+ async updateDocument(collection, id, data) {
+ try {
+ await this.firestore.collection(collection).doc(id).update(data);
+ console.log('Document updated successfully!');
+ } catch (error) {
+ console.error('Error updating document:', error);
+ throw error;
+ }
+ }
+
+ // Delete a document
+ async deleteDocument(collection, id) {
+ try {
+ await this.firestore.collection(collection).doc(id).delete();
+ console.log('Document deleted successfully!');
+ } catch (error) {
+ console.error('Error deleting document:', error);
+ throw error;
+ }
+ }
+
+ // Create, Read, Update, Delete methods...
+}
+
+const instance = new FirebaseService();
+Object.freeze(instance);
+
+export default instance;
\ No newline at end of file
diff --git a/src/service/firebase/PostService.js b/src/service/firebase/PostService.js
new file mode 100644
index 0000000..2c2c42f
--- /dev/null
+++ b/src/service/firebase/PostService.js
@@ -0,0 +1,143 @@
+import { requestSummarize } from '../chatgpt/chatgpt_service';
+import FirebaseService from './FirebaseService';
+import firebase from 'firebase/compat/app';
+
+
+const collection = 'posts';
+
+class PostService {
+
+ constructor() {
+ // Create an instance of the FirebaseService
+ this.firebaseService = FirebaseService;
+ }
+
+ async verifyPostOwnership(postId, userId) {
+ const post = await this.getPost(postId, userId);
+ return post && post.userId === userId;
+ }
+
+ async updatePost(postId, userId, postData) {
+ try {
+ const isOwner = await this.verifyPostOwnership(postId, userId);
+
+ if (isOwner) {
+ await FirebaseService.updateDocument(collection, postId, postData);
+ console.log('Post updated successfully!');
+ } else {
+ console.error('Post not found or unauthorized to update.');
+ }
+ } catch (error) {
+ console.error('Error updating post:', error);
+ throw error;
+ }
+ }
+
+ async deletePost(postId, userId) {
+ try {
+ const isOwner = await this.verifyPostOwnership(postId, userId);
+
+ if (isOwner) {
+ await FirebaseService.deleteDocument(collection, postId);
+ console.log('Post deleted successfully!');
+ } else {
+ console.error('Post not found or unauthorized to delete.');
+ }
+ } catch (error) {
+ console.error('Error deleting post:', error);
+ throw error;
+ }
+ }
+
+ // Create a post
+ async createPost(userId, postData) {
+ try {
+ let targetMessageList = [];
+ if(postData["add-free"]){
+ targetMessageList = [
+ postData["add-free"],
+ ...targetMessageList
+ ]
+ }
+ if(postData["add-template-1"]){
+ targetMessageList = [
+ postData["add-template-1"],
+ ...targetMessageList
+ ]
+ }
+ if(postData["add-template-2"]){
+ targetMessageList = [
+ postData["add-template-2"],
+ ...targetMessageList
+ ]
+ }
+ const flattenedMessage = targetMessageList.join(" ");
+
+ let summarizedText = null;
+ if(flattenedMessage.length > 20){
+ console.log('flattenedMessage', flattenedMessage);
+ summarizedText = await requestSummarize(20, flattenedMessage);
+ console.log('summarizedText', summarizedText);
+ }
+
+ // Combine the userId with the post data
+ const post = {
+ 'summary': summarizedText ?? flattenedMessage,
+ 'userId': userId,
+ ...postData,
+ };
+
+ // Specify the collection where the posts are stored
+
+ // Create the post document using the FirebaseService
+ const postId = await this.firebaseService.createDocument(collection, post);
+
+ return postId;
+ } catch (error) {
+ console.error('Error creating post:', error);
+ throw error;
+ }
+ }
+
+ async getPost(postId, userId) {
+ try {
+ const post = await FirebaseService.getDocument(collection, postId);
+
+ if (post && post.userId === userId) {
+ return post;
+ }
+
+ return null;
+ } catch (error) {
+ console.error('Error getting post:', error);
+ throw error;
+ }
+ }
+
+ async getUserPosts(userId, pageSize = 10, lastPost) {
+ let query = firebase.firestore().collection(collection).where('userId', '==', userId).orderBy('date', 'desc');
+
+ if (lastPost) {
+ const lastPostDocRef = firebase.firestore().collection(collection).doc(lastPost.docId);
+ query = query.startAfter(lastPostDocRef);
+
+ }
+
+ query = query.limit(pageSize);
+
+ try {
+ const snapshot = await query.get();
+ const posts = snapshot.docs.map((doc) => ({ docId: doc.id, ...doc.data() }));
+ return posts;
+ } catch (error) {
+ console.error('Error fetching user posts:', error);
+ throw error;
+ }
+ }
+
+
+
+}
+
+const postService = new PostService();
+export default postService;
diff --git a/src/service/firebase/auth.js b/src/service/firebase/auth.js
new file mode 100644
index 0000000..d2da43b
--- /dev/null
+++ b/src/service/firebase/auth.js
@@ -0,0 +1,22 @@
+import { getAuth, signInWithPopup, GoogleAuthProvider, signOut,onAuthStateChanged } from "firebase/auth";
+import app from "./firebase";
+
+export const signInWithGoogle = () => {
+ const provider = new GoogleAuthProvider();
+ const auth = getAuth(app);
+
+ return signInWithPopup(auth, provider);
+};
+
+export const logout = () => {
+ const auth = getAuth(app);
+ return signOut(auth);
+};
+
+// 로그인 상태 변경 리스너 추가
+export const addAuthStateChangedListener = (callback) => {
+ const auth = getAuth(app);
+ onAuthStateChanged(auth, callback);
+};
+
+
diff --git a/src/service/firebase/firebase.js b/src/service/firebase/firebase.js
new file mode 100644
index 0000000..ea44301
--- /dev/null
+++ b/src/service/firebase/firebase.js
@@ -0,0 +1,22 @@
+// Import the functions you need from the SDKs you need
+import { initializeApp } from "firebase/app";
+import { getAnalytics } from "firebase/analytics";
+// TODO: Add SDKs for Firebase products that you want to use
+// https://firebase.google.com/docs/web/setup#available-libraries
+
+// Your web app's Firebase configuration
+// For Firebase JS SDK v7.20.0 and later, measurementId is optional
+const firebaseConfig = {
+ apiKey: "AIzaSyAdk_OwlN5lNt-viG5bS3wOxfsXmd5xZnE",
+ authDomain: "woowa-33196.firebaseapp.com",
+ projectId: "woowa-33196",
+ storageBucket: "woowa-33196.appspot.com",
+ messagingSenderId: "522521462459",
+ appId: "1:522521462459:web:0ab86a9123cd75370c4b9e",
+ measurementId: "G-SWDP415KGZ"
+};
+
+// Initialize Firebase
+const app = initializeApp(firebaseConfig); // 이거가지고 하면됨.
+const analytics = getAnalytics(app);
+export default app;
\ No newline at end of file
diff --git a/src/service/firebase/firebaseConfig.js b/src/service/firebase/firebaseConfig.js
new file mode 100644
index 0000000..4607edd
--- /dev/null
+++ b/src/service/firebase/firebaseConfig.js
@@ -0,0 +1,9 @@
+export const firebaseConfig = {
+ apiKey: "AIzaSyAdk_OwlN5lNt-viG5bS3wOxfsXmd5xZnE",
+ authDomain: "woowa-33196.firebaseapp.com",
+ projectId: "woowa-33196",
+ storageBucket: "woowa-33196.appspot.com",
+ messagingSenderId: "522521462459",
+ appId: "1:522521462459:web:0ab86a9123cd75370c4b9e",
+ measurementId: "G-SWDP415KGZ"
+ };
\ No newline at end of file
diff --git a/src/service/firebase/storageService.js b/src/service/firebase/storageService.js
new file mode 100644
index 0000000..bebddef
--- /dev/null
+++ b/src/service/firebase/storageService.js
@@ -0,0 +1,74 @@
+import firebase from 'firebase/compat/app';
+import 'firebase/compat/storage';
+async function urlToFile(url, filename) {
+ const response = await fetch(url);
+ const blob = await response.blob();
+ return new File([blob], filename);
+}
+
+class FirebaseStorageService {
+ constructor() {
+ this.storage = firebase.storage();
+ }
+
+
+ // Upload an image for a specific post
+ async uploadPostImage(userId, postId, file) {
+ let fileForm = null;
+ if (typeof file === "string") {
+ // Convert URL to File object
+ try {
+ const response = await fetch(file);
+ const blob = await response.blob();
+ fileForm = new File([blob], "file");
+ } catch (error) {
+ console.error("Error converting URL to File:", error);
+ throw error;
+ }
+ } else {
+ fileForm = file; // 'file' is already a File object
+ }
+ if(fileForm == null){
+ console.log('cancel uploadPostImage job...');
+ return;
+ }
+
+ try {
+ const storageRef = this.storage.ref();
+ const fileRef = storageRef.child(`users/${userId}/posts/${postId}/${fileForm.name}`);
+ await fileRef.put(fileForm);
+ const downloadUrl = await fileRef.getDownloadURL();
+ return downloadUrl;
+ } catch (error) {
+ console.error('Error uploading post image:', error);
+ throw error;
+ }
+ }
+
+ // Retrieve the download URL of a post image
+ async getPostImageDownloadUrl(userId, postId, fileName) {
+ try {
+ const storageRef = this.storage.ref();
+ const fileRef = storageRef.child(`users/${userId}/posts/${postId}/${fileName}`);
+ const downloadUrl = await fileRef.getDownloadURL();
+ return downloadUrl;
+ } catch (error) {
+ console.error('Error getting post image download URL:', error);
+ throw error;
+ }
+ }
+
+ // Delete a post image
+ async deletePostImage(userId, postId, fileName) {
+ try {
+ const storageRef = this.storage.ref();
+ const fileRef = storageRef.child(`users/${userId}/posts/${postId}/${fileName}`);
+ await fileRef.delete();
+ } catch (error) {
+ console.error('Error deleting post image:', error);
+ throw error;
+ }
+ }
+}
+const storageService = new FirebaseStorageService();
+export default storageService;
diff --git a/src/service/providers/auth_provider.js b/src/service/providers/auth_provider.js
new file mode 100644
index 0000000..45582af
--- /dev/null
+++ b/src/service/providers/auth_provider.js
@@ -0,0 +1,67 @@
+import React, { createContext, useContext, useState } from "react";
+
+import { getAuth, signInWithPopup, GoogleAuthProvider , signOut} from "firebase/auth";
+
+
+// Create a context for the inputData state
+const AuthContext = createContext();
+
+// Create a DataInputProvider component to provide the context value
+const AuthProvider = ({ children }) => {
+ const [user, setUser] = useState(null);
+
+ // Function to update the inputData state
+ const updateUser = (user) => {
+ setUser(user);
+ };
+
+ return (
+
+ {children}
+
+ );
+};
+
+// Custom hook to access the updateDataInput function
+const useUpdateUser = () => useContext(AuthContext)[0];
+
+// Custom hook to access the inputData state
+const useUser = () => {
+ const user = useContext(AuthContext)[1];
+ return user;
+};
+
+const handleGoogleLogin = () => {
+ const auth = getAuth();
+ const provider = new GoogleAuthProvider();
+
+ console.log('clicked!');
+
+ signInWithPopup(auth, provider)
+ .then((result) => {
+ // Handle successful login
+ const user = result.user;
+ // Update user state or perform any other necessary actions
+ })
+ .catch((error) => {
+ // Handle login error
+ console.log(error);
+ });
+ };
+
+ const handleLogout = () => {
+ const auth = getAuth();
+
+ signOut(auth)
+ .then(() => {
+ // Handle successful logout
+ // Update user state or perform any other necessary actions
+ })
+ .catch((error) => {
+ // Handle logout error
+ console.log(error);
+ });
+ };
+
+
+export { AuthProvider, useUpdateUser, useUser, handleGoogleLogin, handleLogout};
diff --git a/src/service/providers/data_input_provider.js b/src/service/providers/data_input_provider.js
new file mode 100644
index 0000000..2367638
--- /dev/null
+++ b/src/service/providers/data_input_provider.js
@@ -0,0 +1,35 @@
+import React, { createContext, useContext, useState } from "react";
+
+// Create a context for the inputData state
+const DataInputContext = createContext();
+
+// Create a DataInputProvider component to provide the context value
+const DataInputProvider = ({ children }) => {
+ const [inputData, setInputData] = useState({});
+
+ // Function to update the inputData state
+ const updateDataInput = (name, value) => {
+
+ setInputData((prevData) => ({
+ ...prevData,
+ [name]: value,
+ }));
+ };
+
+ return (
+
+ {children}
+
+ );
+};
+
+// Custom hook to access the updateDataInput function
+const useUpdateDataInput = () => useContext(DataInputContext)[0];
+
+// Custom hook to access the inputData state
+const useDataInput = () => {
+ const inputData = useContext(DataInputContext)[1];
+ return inputData;
+};
+
+export { DataInputProvider, useUpdateDataInput, useDataInput };
diff --git a/src/service/providers/image_input_provider.js b/src/service/providers/image_input_provider.js
new file mode 100644
index 0000000..f2ef53e
--- /dev/null
+++ b/src/service/providers/image_input_provider.js
@@ -0,0 +1,33 @@
+import React, { createContext, useContext, useState } from "react";
+
+// Create a context for the inputData state
+const ImageInputContext = createContext();
+
+// Create a DataInputProvider component to provide the context value
+const ImageInputProvider = ({ children }) => {
+ const [previewImage, setPreviewImage] = useState(null);
+
+ // Function to update the inputData state
+ const updateImageInput = (name, value) => {
+ if(name === "image"){
+ setPreviewImage(value);
+ }
+ };
+
+ return (
+
+ {children}
+
+ );
+};
+
+// Custom hook to access the updateDataInput function
+const useUpdateImageInput = () => useContext(ImageInputContext)[0];
+
+// Custom hook to access the inputData state
+const useImageInput = () => {
+ const previewImage = useContext(ImageInputContext)[1];
+ return previewImage;
+};
+
+export { ImageInputProvider, useUpdateImageInput, useImageInput};
diff --git a/src/service/providers/timeline_data_provider.js b/src/service/providers/timeline_data_provider.js
new file mode 100644
index 0000000..7622ddc
--- /dev/null
+++ b/src/service/providers/timeline_data_provider.js
@@ -0,0 +1,34 @@
+import React, { createContext, useContext, useState } from "react";
+
+// Create a context for the inputData state
+const TimelineContext = createContext();
+
+// Create a DataInputProvider component to provide the context value
+const TimelineDataProvider = ({ children }) => {
+ const [inputData, setInputData] = useState({});
+
+ // Function to update the inputData state
+ const updateTimelineData = (name, value) => {
+ setInputData((prevData) => ({
+ ...prevData,
+ [name]: value,
+ }));
+ };
+
+ return (
+
+ {children}
+
+ );
+};
+
+// Custom hook to access the updateDataInput function
+const useUpdateTimelineData = () => useContext(TimelineContext)[0];
+
+// Custom hook to access the inputData state
+const useTimelineData = () => {
+ const inputData = useContext(TimelineContext)[1];
+ return inputData;
+};
+
+export { TimelineDataProvider, useUpdateTimelineData, useTimelineData };
diff --git a/src/styles/theme.js b/src/styles/theme.js
new file mode 100644
index 0000000..0931414
--- /dev/null
+++ b/src/styles/theme.js
@@ -0,0 +1,75 @@
+import { createGlobalStyle } from "styled-components";
+
+
+export const theme = {
+ fontSizes: {
+ Header1: '96px',
+ Header2: '60px',
+ Header3: '60px',
+ Header4: '34px',
+ Header5: '24px',
+ Header6: '20px',
+ Subtitle1: '16px',
+ Subtitle2: '14px',
+ Body1: '16px',
+ Body2: '14px',
+
+ },
+ fontWeights: {
+ light : 300,
+ regular: 400,
+ semibold: 600,
+ bold: 700,
+ heavy : 900,
+ },
+ fontFamily: {
+ mainfont: 'Pretendard',
+ mont : 'Mont',
+ },
+ color: {
+ primary050: '#e1faf2',
+ primary100: '#b6f3de',
+ primary200:'#7febc8',
+ primary300: '#272727', // 메인컬러 바뀌었습니다.
+ primary400: '#7A7A7A', // 임시로 해놓을게요. _승훈.
+ primary500: '#CDCDCD', // 경험작성하기 호버되었을때.
+ primary600: '#00c181',
+ primary700: '#00ad72',
+ primary800: '#009b64',
+ primary900:'#007a4b',
+
+ secondary050: '#F7EBE9',
+ secondary100: '#F6D0C3',
+ secondary200: '#F1B19E',
+ secondary300: '#EE9479',
+ secondary400: '#EC7D5D',
+ secondary500: '#EC6947',
+ secondary600: '#E16342',
+ secondary700: '#D35B3D',
+ secondary800: '#C65538',
+ secondary900: '#AF482E',
+
+ background: '#F4F1EC',
+ surface: '#FFFFFF',
+ error: '#DA5D5D',
+ onPrimary: '#FFFFFF',
+ blackHigh: '#272727',
+ blackMedium: '#7A7A7A',
+ blackBackground: '#1C2530',
+ disabled: '#ABABAB',
+ disabled2: '#CDCDCD',
+ searchBackground: '#EBEBEB',
+
+ // cartagory1: '#4386F7',
+ // cartagory2: '#',
+ // cartagory3: '#',
+ // cartagory4: '#',
+ // cartagory5: '#',
+ // cartagory6: '#',
+ // cartagory7: '#',
+ // cartagory8: '#',
+ }
+};
+
+
+
diff --git a/src/utility/date_formater.js b/src/utility/date_formater.js
new file mode 100644
index 0000000..1bd3943
--- /dev/null
+++ b/src/utility/date_formater.js
@@ -0,0 +1,29 @@
+function getDateFromWeek(week, year) {
+ const date = new Date(year, 0, 1 + (week - 1) * 7);
+ const dayOfWeek = date.getDay();
+ const firstDayOfWeek = new Date(date.valueOf() - (dayOfWeek <= 4 ? dayOfWeek * 86400000 : (7 - dayOfWeek + 1) * 86400000));
+ return firstDayOfWeek;
+}
+
+const convertStrToDate = (dateStr) => {
+ const dateSegement = dateStr.split("/");
+ let month;
+ let day;
+ let year;
+ let week;
+ if(dateSegement.length === 3){
+ month = dateSegement[0];
+ day = dateSegement[1];
+ year = dateSegement[2];
+ } else if(dateSegement.length === 2){
+ month = dateSegement[0];
+ year = dateSegement[1];
+ } else if(dateSegement.length === 1){
+ if(dateStr.length === 4) {
+ year = dateSegement[0];
+ } else{
+ week = dateSegement[0];
+ }
+ }
+ return new Date(year, month - 1, day);
+}
\ No newline at end of file
diff --git a/src/utility/url_preview.js b/src/utility/url_preview.js
new file mode 100644
index 0000000..f99cf3e
--- /dev/null
+++ b/src/utility/url_preview.js
@@ -0,0 +1,21 @@
+import get_preview_api_key from "../constants/preview_api_key";
+import axios from "axios";
+
+
+const fetchPreviewData = async (url) => {
+ try {
+ const preview_api_key = await get_preview_api_key();
+ const response = await axios.get(
+ `https://api.linkpreview.net/?key=${preview_api_key}&q=${encodeURIComponent(
+ url
+ )}`
+ );
+ return response.data;
+ } catch (error) {
+ console.error("Error:", error);
+ return null;
+ }
+ };
+
+export default fetchPreviewData;
+
diff --git a/storage.rules b/storage.rules
new file mode 100644
index 0000000..9f33d22
--- /dev/null
+++ b/storage.rules
@@ -0,0 +1,8 @@
+rules_version = '2';
+service firebase.storage {
+ match /b/{bucket}/o {
+ match /{allPaths=**} {
+ allow read, write: if false;
+ }
+ }
+}