diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 00000000..e7d4891a --- /dev/null +++ b/.eslintrc @@ -0,0 +1,41 @@ +{ + // 전역변수 환경 설정 + "env": { + "browser": true, + "es2021": true, + "node": true + }, + + // npm을 통해 설치한 외부 ESLint 설정 등록 (eslint-config-{name}으로 설치) + "extends": [ + "eslint:recommended", + "plugin:@typescript-eslint/recommended" + ], + + // ESLint에 지원할 JavaScript 옵션 설정 + "parserOptions": { + "ecmaFeatures": { + "jsx": true + }, + "ecmaVersion": 12, + "sourceType": "module" + }, + + // parser 등록 + "parser": "@typescript-eslint/parser", + + // 사용자 규칙 추가할 플러그인 (eslint-plugin-{name}으로 설치) + "plugins": [ + "@typescript-eslint", + "prettier" + ], + + // 플러그인을 통해 설치한 것 외에 규칙 설정 + "rules": { + "prettier/prettier": [ + "error", { + "endOfLine": "auto" + } + ] + } +} \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..9a9a446c --- /dev/null +++ b/.gitignore @@ -0,0 +1,25 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js +package-lock.json +.env +# testing +/coverage + +# production +# /build + +# misc +.DS_Store +.env +.env.local +.env.development.local +.env.test.local +.env.production.local + +npm-debug.log* +yarn-debug.log* +yarn-error.log* diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 00000000..bacfe924 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,8 @@ +{ + "singleQuote": true, + "semi": true, + "tabWidth": 4, + "trailingComma": "all", + "printWidth": 120, + "arrowParens": "always" +} \ No newline at end of file diff --git a/PULL_REQUEST_TEMPLATE.md b/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 00000000..268a37e2 --- /dev/null +++ b/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,15 @@ +## Summary + +## Decribe details + +## Jira link + +## PR guidline check list + +Plz check if your code fulfills the following requirements. + +- [ ] The Commit message follows our conventions. +- [ ] The File follows folder structure. +- [ ] The PR title follows the convention rule. + +## any comment... diff --git a/README.md b/README.md index d6c98571..1aa9e5cb 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,17 @@ +![main](https://github.com/2weeks-team/2weeks-team/assets/39702832/00e5c4cc-0466-4982-a941-38a01abb1a8a) + +## 📌 프로젝트 소개 +**FASTUDY**는 프로젝트 및 스터디원 모집 및 정보 공유 서비스입니다 + +## 📌 배포 사이트 +https://2weeks-team-mzbe-c7xmzksqc-2weeks.vercel.app/ + +
+야놀자 테크 캠프 토이 프로젝트 설명 + # **📅 직원들을 위한 위키 사이트** -직원 들을 위한 위키 사이트를 만들어보세요! +직원들을 위한 위키 사이트를 만들어보세요! 위키 사이트에는 위키 뿐 아니라 여러 기능이 추가되어야 합니다! ### **[과제 수행 및 제출 방법]** @@ -18,186 +29,211 @@ ### **[필수 구현사항]** -- 문서편집, revision 기능을 제공하여 업무일지를 작성할 수 있는 직원들을 위한 위키사이트 구현(마크다운 형식) -- firebase database (Firestore) 이용 -- 모달을 활용한 근무 시간을 표시하는 시계 및 타이머 창 구현 -- 캐러셀을 활용한 회사 공지 페이지 -- **갤러리 페이지 / 업무일지 페이지 등 메뉴를 필터링 또는 카테고리화 하는 선택바 구현** -- netlify 등을 이용한 정적 페이지 배포 -- TypeScript 사용 필수 -- 과제에 대한 설명을 포함한 `README.md` 파일 작성 - - 팀원별로 구현한 부분 소개 +[x] 문서편집, revision 기능을 제공하여 업무일지를 작성할 수 있는 직원들을 위한 위키사이트 구현(마크다운 형식) +[x] firebase database (Firestore) 이용 +[x] 모달을 활용한 근무 시간을 표시하는 시계 및 타이머 창 구현 +[x] 캐러셀을 활용한 회사 공지 페이지 +[x] **갤러리 페이지 / 업무일지 페이지 등 메뉴를 필터링 또는 카테고리화 하는 선택바 구현** +[x] netlify 등을 이용한 정적 페이지 배포 +[x] TypeScript 사용 필수 +[x] 과제에 대한 설명을 포함한 `README.md` 파일 작성 +[x] 팀원별로 구현한 부분 소개 ### **[선택 구현사항]** -- React 사용은 선택 -- 기타 동작이 완료되기 전에 로딩 애니메이션 구현 -- 페이지네이션 -- 관련된 기타 기능도 고려 -- eslint 설정, 커밋컨벤션, 문서화 등 팀프로젝트시 필요한 추가 작업들 - ---- - -## 가이드 - -아래 예시는 모두 하나의 의견입니다! - -따라하는게 아니라 자신만의 결과물을 만들어보세요. - -### 공지사항 -[영상 1] - - -### **모달 타이머** -[영상 2] - - -https://github.com/KDT1-FE/Y_FE_Toy1/assets/38754963/20c18d28-5a01-4163-876c-be74a24f62db - - - -### **마크다운 위키사이트** -[영상 3] - - -https://github.com/KDT1-FE/Y_FE_Toy1/assets/38754963/08e3efca-8137-44d8-a0af-c62a668b810b - - - -### **갤러리** -[영상 4] - ---- - -# **[Firestore]** - -Firestore에 대한 가이드입니다. - -자세한 내용은 [공식 홈페이지](https://firebase.google.com/?hl=ko) 를 찾아보길 적극 권장합니다! - -### **App init** - -```jsx -import { getFirestore } from "firebase/firestore"; - -export const db = getFirestore(fireBaseApp); -``` - -### Firestore 데이터 추가하기 - -Firestore의 데이터를 추가하는 방법은 크게 두가지이다. - -1. Firebase console에서 손수 데이터 추가해주기 -2. 코드로 데이터 추가하기 - -### 1. Firebase console에서 손수 데이터 추가해주기 - -1. [Firebase console](https://console.firebase.google.com/u/0/?hl=ko)에 접속한다. -2. 자신의 프로젝트를 선택한다. -3. 왼쪽 메뉴에서 `Firestore Database`를 선택한다. -4. `+ 버튼`을 눌러 컬렉션 > 문서를 마음대로 추가해준다. -5. 필드를 추가하여 문서에 데이터를 넣어준다. - -### 2. 코드로 데이터 추가하기 - -Firestore는 `setDoc`, `addDoc` 두 가지 함수로 데이터를 추가할 수 있다. - -이제 원하는 데이터를 추가해보자. - -**1. `addDoc`** - -`addDoc`은 아래와 같이 사용하여 원하는 데이터를 추가할 수 있다. +[x] React 사용은 선택 +[x] 기타 동작이 완료되기 전에 로딩 애니메이션 구현 +[x] 페이지네이션 +[x] 관련된 기타 기능도 고려 +[x] eslint 설정, 커밋컨벤션, 문서화 등 팀프로젝트시 필요한 추가 작업들 + +
+ +## 📌 팀 소개 + + + + + + + + + + + + + + + + +
+ + 어승준 프로필 + + + + 박성후 프로필 + + + + 진정민 프로필 + + + + 백상원 프로필 + + + + 서예빈 프로필 + +
+ + 어승준
+ 팀장 (FE) +
+
+ + 박성후
+ 팀원 (FE) +
+
+ + 진정민
+ 팀원 (FE) +
+
+ + 백상원
+ 팀원 (FE) +
+
+ + 서예빈
+ 팀원 (FE) +
+
+ +## 📌 Contributor +> @[JeongMin83](https://github.com/JeongMin83) (진정민) : 메인페이지, 로그인, 모집
+@[seungjun222](https://github.com/seungjun222) (어승준) : 사이드바, 모집
+@[Yamyam-code](https://github.com/Yamyam-code) (백상원) : 마이페이지
+@[HOOOO98](https://github.com/HOOOO98) (박성후) : 갤러리
+@[syb0127](https://github.com/syb0127) (서예빈) : 위키 + +## 📌 기술 스택 + +### Environment + +
+ + + + + +
+ +### FrontEnd + +
+ + + + + +
+ +### DB + +
+ +
+ +### Deploy + +
+ +
+ +### Communication + +
+ + + +
+ +## 📌 주요 화면 및 기능 +### 로그인 +![login](https://github.com/2weeks-team/2weeks-team/assets/39702832/96939258-7566-4c2c-9dd8-43a98e2f1fc2) + +### 메인페이지 +![main](https://github.com/2weeks-team/2weeks-team/assets/39702832/00e5c4cc-0466-4982-a941-38a01abb1a8a) + +### 위키 +![wiki_demo (4)](https://github.com/2weeks-team/2weeks-team/assets/39702832/bb54b442-7eed-468f-80e1-5caaeb55149e) + +### 모집 +![recuritment_demo](https://github.com/2weeks-team/2weeks-team/assets/39702832/04d18ad5-1c37-4f00-baf1-e02f0611d09d) + +### 갤러리 +![gallery](https://github.com/2weeks-team/2weeks-team/assets/39702832/296d7cbe-fe18-4b3a-8de8-dd11d65b1b69) + +### 마이페이지 +![myPage](https://github.com/2weeks-team/2weeks-team/assets/39702832/fe4ced66-b2e9-4ab7-ad09-18a585fcb29a) + +## 📌 DB 스키마 +Movie Database + +## 📌 유저 플로우 + + +## 📌 파일 구조 ``` -import { addDoc, collection } from "firebase/firestore"; - -const writtenDoc = await addDoc(collection(db, "wiki"), { - title: "LGH", - description: "허먼밀러...사고싶다...", -}); - -console.log("Document written with ID: ", writtenDoc.id); -// 새로 생성된 Document의 ID를 반환한다. -``` - -원하는 데이터를 추가하기 위해선 먼저 원하는 collection을 선택해야 한다. 위 예제의 `addDoc` 안에서 사용한 `collection` 함수는 db상에 있는 collection을 선택하거나 없을 경우 새로운 collection을 생성하여 반환한다. - -Firebase의 Doc는 기본적으로 ID를 가져야 하는데, addDoc을 사용하면 ID를 자동으로 만든다. 또한, 이미 존재하는 Doc에 `addDoc`을 사용하면 에러가 발생한다. - -**2. `setDoc`** - -`setDoc`은 아래와 같이 사용하여 원하는 데이터를 추가할 수 있다. - -``` -import { setDoc, doc } from "firebase/firestore"; - -await setDoc(doc(db, "wiki", "new-id"), { - title: "LGH", - description: "허먼밀러...사고싶다...", -}); +2weeks-team/ +├── src/ +│ ├── common/ +│ │ ├── Footer/ +| | | . +| | | . +│ │ └── Header/ +│ │ +│ ├── components/ +│ │ ├── ChannelModal/ +│ │ ├── SidebarGallery/ +| | | . +| | | . +| | | . +│ │ └── Slider/ +│ │ +│ ├── fonts/ # 추후 assets로 +│ ├── pages/ +│ │ ├── Gallery/ +│ │ ├── Home/ +| | | . +| | | . +| | | . +│ │ └── Wiki/ +│ │ +│ ├── utils/ +│ │ ├── firebase.ts +│ │ └── recoil.ts +│ │ +│ ├── App.tsx +│ ├── index.tsx +│ ├── GlobalStyle.tsx +│ ├── fonts.d.ts +│ └── custom.d.ts +│ +├── public/ +│ └── index.html +│ +├── node_modules/ +├── package.json +├── tsconfig.json +├── .eslintrc +├── README.md +└── ... ``` -`addDoc`과의 차이점은 - -1. **id** 를 지정해줘야함 -2. `collection` 대신 `doc`을 사용함 -3. 이미 존재하는 Doc에 사용가능 - -3가지 이다. - -`setDoc`은 `addDoc`과 달리 collection이 아니라 doc를 선택해야 한다. 이는 `setDoc`이 데이터의 추가 뿐 아니라 데이터 덮어쓰기 기능도 가지고 있기 때문이다. 러프하게 생각해보면 `setDoc`은 데이터를 추가할 때 - -1. doc을 선택하거나 새로운 doc을 생성 -2. doc의 내용을 덮어씀 - -의 방식으로 동작하는 것이다. - -Doc을 선택하는 방법은 `doc` 함수를 사용하는 것이다. `[doc()](https://firebase.google.com/docs/reference/js/firestore_.md?hl=ko#doc)` 함수는 `DocumentReference` instance를 반환한다. 절대 경로를 사용하여 원하는 Document를 선택할 수 있다. 위에서 만들어둔 `wiki > completed` 문서는 아래처럼 불러올 수 있다. - -``` -import { doc } from "firebase/firestore"; - -const docRef = doc(db, "wiki", "completed"); -``` - -`doc` 함수의 3번째 인자가 바로 **id** 이다. id는 이미 존재하는 Doc의 id를 사용할 수도 있고, 새로운 id를 사용할 수도 있다. 존재하는 id를 사용하는 경우에는 해당 Doc의 데이터를 덮어쓰게 된다. 그렇지 않은 경우엔 새로운 Doc를 생성한다. - -어쨌거나 데이터를 새로 추가할 수 있는 것이다. - -### Firestore 데이터 수정하기 - -Firestore의 데이터를 수정하는 방법은 크게 두가지이다. - -1. Firebase console에서 손수 데이터 수정해주기 -1. 코드로 데이터 수정하기 - -1번은 데이터 생성과 비슷하게 진행하면 된다. - -**2. 코드로 데이터 수정하기** - -Firestore는 `setDoc`, `update` 두 가지 함수로 데이터를 추가할 수 있다. - -`setDoc`을 사용하는 방법은 위에 적혀있다. - -### `update` - -`setDoc`은 데이터를 덮어쓴다. 따라서 기존의 문서를 유지한 채 일부분의 데이터만 변경하고 싶어도 이전의 데이터를 모두 새로 입력해야 한다. - -그러나 `update`는 기존의 데이터를 유지한 채 일부분의 데이터만 변경할 수 있다. - -```jsx -import { updateDoc, doc } from "firebase/firestore"; - -await updateDoc(doc(db, "wiki", "new-id"), { - description: "허먼밀러...200만원...", -}); -``` - -위와 같이 코드를 작성하면 `new-id`라는 id를 가진 문서의 description만 변경된다. - ---- - -### *참고 링크 - -- **[Firebase](https://firebase.google.com/docs?hl=ko)** -- [**프로토타입 프로젝트**](https://stfe.vercel.app/) +### 📌 개발 기간 : `2주` `23.09.11 ~ 23.09.22` diff --git a/package.json b/package.json new file mode 100644 index 00000000..3bbad772 --- /dev/null +++ b/package.json @@ -0,0 +1,88 @@ +{ + "name": "my-app", + "homepage": "https://2weeks-team-mzbe.vercel.app/", + "version": "0.1.0", + "private": true, + "dependencies": { + "@emotion/styled": "^11.11.0", + "@emotion/styled.macro": "^0.10.6", + "@loadable/component": "^5.15.3", + "@mui/icons-material": "^5.14.9", + "@mui/joy": "^5.0.0-beta.7", + "@mui/material": "^5.14.9", + "@testing-library/jest-dom": "^5.17.0", + "@testing-library/react": "^13.4.0", + "@testing-library/user-event": "^13.5.0", + "@uiw/react-md-editor": "^3.23.5", + "assert": "^2.1.0", + "browserify-zlib": "^0.2.0", + "buffer": "^6.0.3", + "crypto-browserify": "^3.12.0", + "firebase": "^10.3.1", + "https-browserify": "^1.0.0", + "lodash.debounce": "^4.0.8", + "os-browserify": "^0.3.0", + "path-browserify": "^1.0.1", + "prettier": "^3.0.3", + "react": "^18.2.0", + "react-beautiful-dnd": "^13.1.1", + "react-dom": "^18.2.0", + "react-infinite-scroll-component": "^6.1.0", + "react-material-ui-carousel": "^3.4.2", + "react-modal": "^3.16.1", + "react-router-dom": "^6.15.0", + "react-scripts": "5.0.1", + "react-scrollbars-custom": "^4.1.1", + "react-simply-carousel": "^9.0.4", + "recoil": "^0.7.7", + "recoil-persist": "^5.1.0", + "rehype-sanitize": "^6.0.0", + "stream-browserify": "^3.0.0", + "stream-http": "^3.2.0", + "styled-components": "^6.0.7", + "sweetalert": "^2.1.2", + "typescript": "^4.9.5", + "url": "^0.11.3", + "util": "^0.12.5", + "vm-browserify": "^1.1.2", + "web-vitals": "^2.1.4" + }, + "scripts": { + "start": "react-scripts start", + "build": "react-scripts build", + "test": "react-scripts test", + "eject": "react-scripts eject" + }, + "eslintConfig": { + "extends": [ + "react-app", + "react-app/jest" + ] + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + }, + "devDependencies": { + "@types/jest": "^29.5.5", + "@types/lodash.debounce": "^4.0.7", + "@types/node": "^20.6.3", + "@types/react": "^18.2.22", + "@types/react-beautiful-dnd": "^13.1.4", + "@types/react-dom": "^18.2.7", + "@types/react-modal": "^3.16.0", + "@types/react-router-dom": "^5.3.3", + "@typescript-eslint/eslint-plugin": "^5.62.0", + "@typescript-eslint/parser": "^5.62.0", + "eslint": "^8.49.0", + "eslint-plugin-prettier": "^5.0.0" + } +} diff --git a/public/MainImg/galleryMain.png b/public/MainImg/galleryMain.png new file mode 100644 index 00000000..ed89e5f2 Binary files /dev/null and b/public/MainImg/galleryMain.png differ diff --git a/public/MainImg/recruitment.png b/public/MainImg/recruitment.png new file mode 100644 index 00000000..784317aa Binary files /dev/null and b/public/MainImg/recruitment.png differ diff --git a/public/MainImg/recruitmentPost.png b/public/MainImg/recruitmentPost.png new file mode 100644 index 00000000..f19e6669 Binary files /dev/null and b/public/MainImg/recruitmentPost.png differ diff --git a/public/MainImg/wikiMain.png b/public/MainImg/wikiMain.png new file mode 100644 index 00000000..bdc6a008 Binary files /dev/null and b/public/MainImg/wikiMain.png differ diff --git a/public/WikiImg/icons8-done-50.png b/public/WikiImg/icons8-done-50.png new file mode 100644 index 00000000..95cc1458 Binary files /dev/null and b/public/WikiImg/icons8-done-50.png differ diff --git a/public/WikiImg/icons8-edit-50.png b/public/WikiImg/icons8-edit-50.png new file mode 100644 index 00000000..2dc535e2 Binary files /dev/null and b/public/WikiImg/icons8-edit-50.png differ diff --git a/public/_redirects b/public/_redirects new file mode 100644 index 00000000..78f7f206 --- /dev/null +++ b/public/_redirects @@ -0,0 +1 @@ +/* /index.html 200 \ No newline at end of file diff --git a/public/index.html b/public/index.html new file mode 100644 index 00000000..3246a0a5 --- /dev/null +++ b/public/index.html @@ -0,0 +1,39 @@ + + + + + + + + + + + + + FASTUDY - 프로젝트/스터디 인원 모집 사이트 + + + +
+ + diff --git a/public/manifest.json b/public/manifest.json new file mode 100644 index 00000000..080d6c77 --- /dev/null +++ b/public/manifest.json @@ -0,0 +1,25 @@ +{ + "short_name": "React App", + "name": "Create React App Sample", + "icons": [ + { + "src": "favicon.ico", + "sizes": "64x64 32x32 24x24 16x16", + "type": "image/x-icon" + }, + { + "src": "logo192.png", + "type": "image/png", + "sizes": "192x192" + }, + { + "src": "logo512.png", + "type": "image/png", + "sizes": "512x512" + } + ], + "start_url": ".", + "display": "standalone", + "theme_color": "#000000", + "background_color": "#ffffff" +} diff --git a/public/robots.txt b/public/robots.txt new file mode 100644 index 00000000..e9e57dc4 --- /dev/null +++ b/public/robots.txt @@ -0,0 +1,3 @@ +# https://www.robotstxt.org/robotstxt.html +User-agent: * +Disallow: diff --git a/src/App.tsx b/src/App.tsx new file mode 100644 index 00000000..edebe6c8 --- /dev/null +++ b/src/App.tsx @@ -0,0 +1,94 @@ +import React, { useEffect } from 'react'; +import GlobalStyle from './GlobalStyle'; +import Header from './common/Header'; +import { Route, BrowserRouter, Routes } from 'react-router-dom'; +import Home from './pages/Home'; +import Wiki from './pages/Wiki'; +import Gallery from './pages/Gallery'; +import SignIn from './pages/SignIn'; +import LogIn from './pages/LogIn'; +import Recruitment from './pages/Recruitment'; +import RecruitmentDetail from './pages/RecruitmentDetail'; +import RecruitmentPost from './pages/RecruitmentPost'; +import RecruitmentEdit from './pages/RecruitmentEdit'; +import { ThemeProvider } from 'styled-components'; +import { useRecoilState } from 'recoil'; +import { Current, ThemeChange, ThemeRing, UserId } from './utils/recoil'; +import { readUser } from './utils/firebase'; +import { themeBorder } from './components/modal/MyPage/MyPageTheme'; + +const App: React.FC = () => { + const [theme, setTheme] = useRecoilState(ThemeChange); + const [userId, setUserId] = useRecoilState(UserId); + const [themeRing, setThemeRing] = useRecoilState(ThemeRing); + + const defaultTheme = { + navBar: '#350d36', + sideMenu: '#3F0E40', + pointItem: '#4D2A51', + text: '#fff', + activeColor1: '#1164A3', + activeColor2: '#2BAC76', + recruitmentBack: '#ECE7EC', + }; + const user = async () => { + { + const userData = await readUser('user', userId); + + if (userData) { + const userTheme = userData['Theme']; + const userThemeRing = userData['ThemeBorder']; + setTheme(userTheme); + setThemeRing(userThemeRing); + localStorage.setItem('theme', JSON.stringify(userTheme)); + localStorage.setItem('themeRing', JSON.stringify(userThemeRing)); + } + } + }; + + if (!localStorage.getItem('theme')) { + localStorage.setItem('theme', JSON.stringify(defaultTheme)); + localStorage.setItem('themeRing', JSON.stringify(themeBorder[0])); + } + useEffect(() => { + if (localStorage.getItem('theme') && localStorage.getItem('themeRing')) { + const localtheme = localStorage.getItem('theme'); + const localthemeRing = localStorage.getItem('themeRing'); + if (localtheme && localthemeRing) { + setTheme(JSON.parse(localtheme)); + setThemeRing(JSON.parse(localthemeRing)); + } + } else if (userId) { + user(); + } + }, []); + useEffect(() => { + if (userId) { + user(); + } + }, [userId]); + + return ( + + + +
+
+ + }> + }> + }> + }> + }> + }> + }> + }> + }> + +
+
+
+ ); +}; + +export default App; diff --git a/src/GlobalStyle.tsx b/src/GlobalStyle.tsx new file mode 100644 index 00000000..d63ae99a --- /dev/null +++ b/src/GlobalStyle.tsx @@ -0,0 +1,80 @@ +import { createGlobalStyle } from 'styled-components'; +import GmarketSansTTFBold from './fonts/GmarketSansTTFBold.ttf'; +import GmarketSansTTFMedium from './fonts/GmarketSansTTFMedium.ttf'; +import GmarketSansTTFLight from './fonts/GmarketSansTTFLight.ttf'; +import YanoljaTTF from './fonts/YanoljaTTF.ttf'; + +const GlobalStyle = createGlobalStyle` + *, *::before, *::after { + box-sizing: border-box; + } + + @font-face { + font-family: 'GmarketSansTTFBold'; + src: local('GmarketSansTTFBold'), local('GmarketSansTTFBold'); + font-style: normal; + src: url(${GmarketSansTTFBold}) format('truetype'); + } + @font-face { + font-family: 'GmarketSansTTFMedium'; + src: local('GmarketSansTTFMedium'), local('GmarketSansTTFMedium'); + font-style: normal; + src: url(${GmarketSansTTFMedium}) format('truetype'); + } + @font-face { + font-family: 'GmarketSansTTFLight'; + src: local('GmarketSansTTFLight'), local('GmarketSansTTFLight'); + font-style: normal; + src: url(${GmarketSansTTFLight}) format('truetype'); + } + @font-face { + font-family: 'YanoljaTTF'; + src: local('YanoljaTTF'), local('YanoljaTTF'); + font-style: normal; + src: url(${YanoljaTTF}) format('truetype'); + } + @import url('https://fonts.googleapis.com/css2?family=Fuggles&family=Gowun+Dodum&display=swap'); + + :root{ + --navigation-background : #350d36; + --cell-background: #3F0E40; + --active-item: #1164A3; + --active-item-text: #FFFFFF; + --point-item: #4D2A51; + --text: #FFF; + --active-current-status: #2BAC76; + --mention-badge: #ECE7EC; + --navigation-background: #350D36; + --navigation-text: #FFF; + } + + body { + margin: 0; + font-family: GmarketSansTTFMedium,-apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', + 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', + sans-serif; + + background-color: ${(props) => props.theme.recruitmentBack}; + font-size:16px; + line-height: 1.5; + width: 100vw; + } + + a{ + text-decoration:none; + } + + h2, p { + margin: 0; + } + + h2 { + font-size: 1.5rem; + } + + p { + font-size: 1rem; + } +`; + +export default GlobalStyle; diff --git a/src/common/Footer/fastuslogo-2-2.svg b/src/common/Footer/fastuslogo-2-2.svg new file mode 100644 index 00000000..dabedfeb --- /dev/null +++ b/src/common/Footer/fastuslogo-2-2.svg @@ -0,0 +1,199 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/common/Footer/index.tsx b/src/common/Footer/index.tsx new file mode 100644 index 00000000..1d88a4d7 --- /dev/null +++ b/src/common/Footer/index.tsx @@ -0,0 +1,14 @@ +import React from 'react'; +import { FooterComponent } from './style'; + +const Footer: React.FC = () => { + return ( + + + Github + + + ); +}; + +export default Footer; diff --git a/src/common/Footer/style.tsx b/src/common/Footer/style.tsx new file mode 100644 index 00000000..c7e8cfd0 --- /dev/null +++ b/src/common/Footer/style.tsx @@ -0,0 +1,14 @@ +import styled from '@emotion/styled'; + +export const FooterComponent = styled.div` + border-top: 1px solid #efefef; + text-align: center; + padding-top: 10px; + + position: absolute; + bottom: 0; + z-index: 1; + + height: 40px; + width: 100%; +`; diff --git a/src/common/Gallery/bin.png b/src/common/Gallery/bin.png new file mode 100644 index 00000000..d8431824 Binary files /dev/null and b/src/common/Gallery/bin.png differ diff --git a/src/common/Gallery/icons8-upload-64.png b/src/common/Gallery/icons8-upload-64.png new file mode 100644 index 00000000..415235fc Binary files /dev/null and b/src/common/Gallery/icons8-upload-64.png differ diff --git a/src/common/Header/index.tsx b/src/common/Header/index.tsx new file mode 100644 index 00000000..5609e571 --- /dev/null +++ b/src/common/Header/index.tsx @@ -0,0 +1,177 @@ +import React, { useEffect } from 'react'; +import { + HeaderComponent, + HomeHeaderComponent, + TitleAnchor, + AnchorContainer, + ListAnchor, + RightAnchorContainer, + LogoutButton, + LoginButton, +} from './style'; +import { useRecoilState } from 'recoil'; +import { UserId, TimeLog, TimerOn, ThemeChange } from '../../utils/recoil'; +import { useNavigate, useLocation } from 'react-router-dom'; +import { createTimelog, readUser } from '../../utils/firebase'; +import { CreateDay, CreateTime } from '../../components/modal/Hooks/WhatTime'; +import MyPageBtn from '../../components/modal/MyPage/MyPageBtn'; +import swal from 'sweetalert'; +import { RedCircle } from '../../components/modal/MyPage/style'; + +const Header: React.FC = () => { + const [userId, setUserId] = useRecoilState(UserId); + const navigate = useNavigate(); + const { pathname } = useLocation(); + + const logOutHandler = () => { + setUserId(''); + navigate('/', { state: pathname }); + }; + + const [timerOn, setTimerOn] = useRecoilState(TimerOn); + const [timeLog, setTimeLog] = useRecoilState(TimeLog); + const [theme, setTheme] = useRecoilState(ThemeChange); + + // 페이지 전환 시 세션 스토리지에서 타임로그 현재 상태 받아오기 + + useEffect(() => { + if (sessionStorage.getItem('timerOn')) { + const sessionTimerOn = sessionStorage.getItem('timerOn'); + setTimerOn(sessionTimerOn === '1' ? true : false); + const sessionTimelog = sessionStorage.getItem('timelog'); + setTimeLog(sessionTimelog ? sessionTimelog : ''); + console.log(userId); + } + }, [pathname]); + // 퇴실버튼을 누르거나 로그아웃 시 firestore에 데이터 전송 + useEffect(() => { + if (!timerOn) { + if (timeLog.length > 0) { + createTimelog('user', userId, timeLog); + setTimeLog(''); + sessionStorage.setItem('timerOn', '0'); + sessionStorage.setItem('timelog', ''); + return; + } + } else if (timerOn) { + sessionStorage.setItem('timerOn', '1'); + sessionStorage.setItem('timelog', timeLog); + } + }, [timeLog]); + const logOutTimeCheck = () => { + if (timerOn && timeLog !== '') { + setTimeLog(timeLog + ' ' + ' ' + '-' + ' ' + ' ' + '퇴실' + ' ' + ' ' + CreateTime()); + setTimerOn(false); + } + // 타임로그 기록 전 아이디 정보가 사라지는 것을 막기위해 setTimeout처리 + setTimeout(() => { + logOutHandler(); + }, 100); + }; + + const homeValued = location.pathname == '/'; + console.log(homeValued); + + return ( + <> + {homeValued ? ( + + + FASTUDY + + + + + 위키 + + + 모집 + + + 갤러리 + + {userId.length > 0 ? ( + { + swal({ + title: '로그아웃 하시겠습니까?', + icon: 'warning', + buttons: ['취소', '로그아웃'], + }).then((yes) => { + if (yes) { + if (timerOn) { + logOutTimeCheck(); + } else { + logOutHandler(); + } + } + }); + }} + style={{ color: '#fff' }} + > + 로그아웃 + + ) : ( + + 로그인 + + )} + {userId.length > 0 && } + {timerOn && ( + + )} + + + + ) : ( + + FASTUDY + + + 위키 + 모집 + 갤러리 + {userId.length > 0 ? ( + { + swal({ + title: '로그아웃 하시겠습니까?', + icon: 'warning', + buttons: ['취소', '로그아웃'], + }).then((yes) => { + if (yes) { + if (timerOn) { + logOutTimeCheck(); + } else { + logOutHandler(); + } + } + }); + }} + > + 로그아웃 + + ) : ( + + 로그인 + + )} + {userId.length > 0 && } + {timerOn && ( + + )} + + + + )} + + ); +}; + +export default Header; diff --git a/src/common/Header/style.tsx b/src/common/Header/style.tsx new file mode 100644 index 00000000..ac625e39 --- /dev/null +++ b/src/common/Header/style.tsx @@ -0,0 +1,125 @@ +import styled from 'styled-components'; +export const HeaderComponent = styled.div` + display: flex; + align-items: center; + justify-content: space-between; + background-color: ${(props) => props.theme.navBar}; + + position: fixed; + z-index: 2; + + top: 0; + left: 0; + height: 72px; + width: 100%; +`; + +export const HomeHeaderComponent = styled.div` + display: flex; + align-items: center; + justify-content: space-between; + background-color: rgba(0, 0, 0, 0); + + position: fixed; + z-index: 2; + + top: 0; + left: 0; + height: 72px; + width: 100%; +`; + +export const TitleAnchor = styled.a` + font-size: 32px; + font-weight: bold; + color: ${(props) => props.theme.text}; + margin-left: 30px; +`; + +export const AnchorContainer = styled.div` + float: right; +`; + +export const RightAnchorContainer = styled.div` + width: 500px; + display: flex; + align-items: center; + justify-content: space-around; + align-items: center; + gap: 20px; +`; + +export const ListAnchor = styled.a` + font-weight: 700; + font-size: 24px; + font-family: + GmarketSansTTFMedium, + -apple-system, + BlinkMacSystemFont, + 'Segoe UI', + 'Roboto', + 'Oxygen', + 'Ubuntu', + 'Cantarell', + 'Fira Sans', + 'Droid Sans', + 'Helvetica Neue', + sans-serif; + color: ${(props) => props.theme.text}; +`; + +export const LogoutButton = styled.a` + display: flex; + align-items: center; + height: 72px; + border-radius: 10%; + font-weight: 700; + font-size: 24px; + color: ${(props) => props.theme.text}; + background-color: rgba(0, 0, 0, 0); + font-family: + GmarketSansTTFMedium, + -apple-system, + BlinkMacSystemFont, + 'Segoe UI', + 'Roboto', + 'Oxygen', + 'Ubuntu', + 'Cantarell', + 'Fira Sans', + 'Droid Sans', + 'Helvetica Neue', + sans-serif; + border: 0; + &:hover { + cursor: pointer; + } +`; + +export const LoginButton = styled.a` + display: flex; + align-items: center; + height: 72px; + border-radius: 10%; + font-weight: 700; + font-size: 24px; + color: ${(props) => props.theme.text}; + background-color: rgba(0, 0, 0, 0); + font-family: + GmarketSansTTFMedium, + -apple-system, + BlinkMacSystemFont, + 'Segoe UI', + 'Roboto', + 'Oxygen', + 'Ubuntu', + 'Cantarell', + 'Fira Sans', + 'Droid Sans', + 'Helvetica Neue', + sans-serif; + border: 0; + &:hover { + cursor: pointer; + } +`; diff --git a/src/common/fastcampusIcon-removebg-preview.png b/src/common/fastcampusIcon-removebg-preview.png new file mode 100644 index 00000000..e25ff3de Binary files /dev/null and b/src/common/fastcampusIcon-removebg-preview.png differ diff --git a/src/common/profileImgloading.gif b/src/common/profileImgloading.gif new file mode 100644 index 00000000..62d766a2 Binary files /dev/null and b/src/common/profileImgloading.gif differ diff --git a/src/components/ChannelModal/index.tsx b/src/components/ChannelModal/index.tsx new file mode 100644 index 00000000..7ed5bbc3 --- /dev/null +++ b/src/components/ChannelModal/index.tsx @@ -0,0 +1,117 @@ +import React, { useEffect, useState } from 'react'; +import Modal from 'react-modal'; +import { createChannelDoc, updateChannelDoc, addFieldToDoc, updateFieldKeyInDoc } from '../../utils/firebase'; +import { CloseButton, ModalTitle, CreateButton, FallbackButton, TextInput } from './style'; + +interface ChannelModalProps { + isOpen: boolean; + closeModal: () => void; + collectionName: string; + modalType: string; + subChannelId: string; + channelId: string; +} + +const ChannelModal: React.FC = ({ + isOpen, + closeModal, + collectionName, + modalType, + subChannelId, + channelId, +}) => { + const customStyles = { + content: { + top: '50%', + left: '50%', + right: 'auto', + bottom: 'auto', + marginRight: '-50%', + transform: 'translate(-50%, -50%)', + borderRadius: '3%', + width: '635px', + height: '370px', + }, + overlay: { + backgroundColor: 'rgba(0, 0, 0, 0.6)', + }, + }; + const [name, setName] = useState(''); + + const handleNameChange = (event: React.ChangeEvent) => { + setName(event.target.value); + }; + + const handleSubmit = async () => { + try { + if (modalType === 'Create') { + await createChannelDoc(collectionName, name); + } else if (modalType === 'Update') { + await updateChannelDoc(collectionName, channelId, name); + } else if (modalType === 'CreateSub') { + const fieldValue = { content: '', time: '' }; + await addFieldToDoc(collectionName, channelId, name, fieldValue); + } else if (modalType === 'UpdateSub') { + await updateFieldKeyInDoc(collectionName, channelId, subChannelId, name); + } + closeModal(); + } catch (error) { + console.error('오류 발생:', error); + } + }; + + const getTitleText = () => { + switch (modalType) { + case 'Create': + return 'Create a channel'; + case 'Update': + return 'Update a channel'; + case 'CreateSub': + return 'Create a subchannel'; + case 'UpdateSub': + return 'Update a subchannel'; + default: + return ''; + } + }; + const title = getTitleText(); + + useEffect(() => { + if (modalType === 'Update' || modalType === 'UpdateSub') { + setName(modalType === 'Update' ? channelId : subChannelId); + } else { + setName(''); + } + }, [modalType, channelId, subChannelId]); + + return ( + +
+ {title} + X +
+
이름
+
{ + e.preventDefault(); // 폼 제출의 기본 동작을 막습니다. + handleSubmit(); + }} + > + + +
{80 - name.length}
+
채널에서는 특정 주제에 대한 대화가 이루어집니다. 찾고 이해하기 쉬운 이름을 사용하세요.
+ { + setName(''); + closeModal(); + }} + > + 취소 + + 저장 +
+ ); +}; + +export default ChannelModal; diff --git a/src/components/ChannelModal/style.tsx b/src/components/ChannelModal/style.tsx new file mode 100644 index 00000000..d0f98bd5 --- /dev/null +++ b/src/components/ChannelModal/style.tsx @@ -0,0 +1,63 @@ +import styled from 'styled-components'; + +export const CloseButton = styled.button` + font-size: 1.5rem; + padding: 0 10px; + margin-left: auto; + border: none; + border-radius: 5px; + background-color: #ffffff; + &:hover { + background-color: #f0f0f0; + cursor: pointer; + } +`; + +export const ModalTitle = styled.div` + font-weight: bold; + font-size: 1.7rem; +`; + +export const FallbackButton = styled.button` + background-color: #ffffff; + color: black; + border: 0.5px solid black; + height: 13.5%; + width: 15%; + font-size: 1.2rem; + font-weight: bold; + border: 1px solid gray; + border-radius: 5px; + margin-top: 10%; + margin-left: 66%; + &:hover { + cursor: pointer; + } +`; + +export const CreateButton = styled.button` + background-color: #2bac76; + color: #ffffff; + border: none; + height: 13.5%; + width: 15%; + font-size: 1.2rem; + font-weight: bold; + border: 1px solid gray; + border-radius: 5px; + margin-top: 10%; + margin-left: 1rem; + &:hover { + cursor: pointer; + } +`; + +export const TextInput = styled.input` + height: 3rem; + width: 100%; + font-size: 1.1rem; + border-left: 1px solid black; + &:hover { + box-shadow: 0px 0px 7px 2px #1164a3; + } +`; diff --git a/src/components/SidebarGallery/index.tsx b/src/components/SidebarGallery/index.tsx new file mode 100644 index 00000000..edb4fb40 --- /dev/null +++ b/src/components/SidebarGallery/index.tsx @@ -0,0 +1,120 @@ +import React, { useEffect, useState } from 'react'; +import { useRecoilState } from 'recoil'; +import { ThemeChange, channelState, subChannelState } from '../../utils/recoil'; +import { + AllChannelsWrapper, + ChannelWrapper, + ChannelFlexDiv, + SubChannelFlexDiv, + ChannelHr, + ChannelDiv, + SubChannelDiv, +} from './style'; + +import { handleGetDocs, themeType } from '../../utils/firebase'; +import { QuerySnapshot } from 'firebase/firestore'; + +interface DocumentData { + [key: string]: any; +} + +interface SidebarGalleryProps { + onKeyClick: (value: any) => void; // 클릭된 값의 핸들러 함수를 props로 받습니다. +} + +const SidebarGallery: React.FC = ({ onKeyClick }) => { + const [docsWithFields, setDocsWithFields] = useState<{ docId: string; docKeys: string[]; docData: DocumentData }[]>( + [], + ); + const [channel, setChannel] = useRecoilState(channelState); + const [subChannel, setSubChannel] = useRecoilState(subChannelState); + const defaultChannel = '레퍼런스 공유'; + const defaultSubChannel = '취업'; + const [back, setBack] = useState(''); + const [currentTheme, setCurrentTheme] = useRecoilState(ThemeChange); + + useEffect(() => { + const selected = (theme: themeType) => { + setBack(theme.activeColor1); + }; + if (localStorage.getItem('theme')) { + const localtheme = localStorage.getItem('theme'); + if (localtheme) { + const color = JSON.parse(localtheme); + selected(color); + } + } + }, [currentTheme]); + + useEffect(() => { + if (!channel || !subChannel) { + setChannel(defaultChannel); + setSubChannel(defaultSubChannel); + } + }, [channel, subChannel, setChannel, setSubChannel]); + + const isSubChannelActive = (channelName: string, subChannelName: string) => { + return channel === channelName && subChannel === subChannelName; + }; + + useEffect(() => { + const updatedQuerySnapshot = handleGetDocs('gallery', (querySnapshot: QuerySnapshot) => { + const data: { docId: string; docKeys: string[]; docData: DocumentData }[] = []; + + querySnapshot.forEach((doc: any) => { + const docData = doc.data(); + const docId = doc.id; + const docKeys = Object.keys(docData); + docKeys.sort(); // 서브채널을 알파벳순으로 정렬 + data.push({ docId, docKeys, docData }); + }); + // 채널 목록을 알파벳순으로 정렬 + data.sort((a, b) => a.docId.localeCompare(b.docId)); + setDocsWithFields(data); + }); + + return () => { + updatedQuerySnapshot(); + }; + }, []); + + const handleKeyClick = (value: any) => { + onKeyClick(value); // 클릭된 값을 상위 컴포넌트로 전달합니다. + }; + + return ( + + {docsWithFields.map((item, index) => ( + + + # {item.docId} + + +
+ {item.docKeys.map((item2, index2) => ( + { + handleKeyClick(item.docData[item2]); + setChannel(item.docId); + setSubChannel(item2); + }} + style={{ + color: isSubChannelActive(item.docId, item2) ? '#fff' : '', + backgroundColor: isSubChannelActive(item.docId, item2) ? back : '', + borderRadius: isSubChannelActive(item.docId, item2) ? '5px' : '', + marginRight: isSubChannelActive(item.docId, item2) ? '10px' : '', + fontWeight: isSubChannelActive(item.docId, item2) ? 'bold' : '', + }} + > + {item2} + + ))} +
+ {index === 0 && } +
+ ))} +
+ ); +}; + +export default SidebarGallery; diff --git a/src/components/SidebarGallery/style.tsx b/src/components/SidebarGallery/style.tsx new file mode 100644 index 00000000..8828fe39 --- /dev/null +++ b/src/components/SidebarGallery/style.tsx @@ -0,0 +1,48 @@ +import styled from 'styled-components'; + +export const AllChannelsWrapper = styled.div` + height: calc(100vh - 72px); + min-width: 16.25%; + max-width: 16.25%; + padding-top: 1%; + background-color: ${(props) => props.theme.sideMenu}; + color: ${(props) => props.theme.text}; +`; + +export const ChannelWrapper = styled.div` + padding-left: 10%; +`; + +export const ChannelFlexDiv = styled.div` + display: flex; + align-items: center; + position: relative; + font-size: 1.4rem; +`; + +export const SubChannelFlexDiv = styled.div` + display: flex; + align-items: center; + position: relative; + font-size: 1.15rem; + + &:hover { + background-color: ${(props) => props.theme.pointItem}; + cursor: pointer; + } +`; + +export const ChannelHr = styled.hr` + border: solid 1px ${(props) => props.theme.pointItrm}; +`; + +export const ChannelDiv = styled.div` + font-size: 1.4rem; + font-weight: bold; +`; + +export const SubChannelDiv = styled.div` + font-size: 1.15rem; + font-weight: bold; + padding: 5px; +`; diff --git a/src/components/SidebarRecruitment/index.tsx b/src/components/SidebarRecruitment/index.tsx new file mode 100644 index 00000000..3e26a821 --- /dev/null +++ b/src/components/SidebarRecruitment/index.tsx @@ -0,0 +1,86 @@ +import React, { useEffect, useState } from 'react'; +import { useRecoilState } from 'recoil'; +import { ThemeChange, channelState, subChannelState } from '../../utils/recoil'; +import { AllChannelsWrapper, ChannelWrapper, ChannelDiv, SubChannelDiv, ChannelHr } from './style'; +import { themeType } from '../../utils/firebase'; + +const SidebarRecruitment: React.FC = () => { + const [channel, setChannel] = useRecoilState(channelState); + const [subChannel, setSubChannel] = useRecoilState(subChannelState); + const defaultChannel = 'study'; + const defaultSubChannel = '전체'; + const [currentTheme, setCurrentTheme] = useRecoilState(ThemeChange); + const [back, setBack] = useState(''); + + useEffect(() => { + const selected = (theme: themeType) => { + setBack(theme.activeColor1); + }; + if (localStorage.getItem('theme')) { + const localtheme = localStorage.getItem('theme'); + if (localtheme) { + const color = JSON.parse(localtheme); + selected(color); + } + } + }, [currentTheme]); + + useEffect(() => { + if (!channel || !subChannel) { + setChannel(defaultChannel); + setSubChannel(defaultSubChannel); + } + }, [channel, subChannel, setChannel, setSubChannel]); + + const isSubChannelActive = (channelName: string, subChannelName: string) => { + return channel === channelName && subChannel === subChannelName; + }; + + const renderSubChannelDiv = (channelName: string, subChannelName: string, label: string) => { + const isActive = isSubChannelActive(channelName, subChannelName); + return ( + { + setChannel(channelName); + setSubChannel(subChannelName); + }} + > + {label} + + ); + }; + + return ( + + + # 스터디 +
+ {renderSubChannelDiv('study', '전체', '전체')} + {renderSubChannelDiv('study', '코딩테스트', '코딩테스트')} + {renderSubChannelDiv('study', 'CS', 'CS')} + {renderSubChannelDiv('study', '면접', '면접')} + {renderSubChannelDiv('study', '알고리즘', '알고리즘')} +
+
+ + + + # 프로젝트 +
+ {renderSubChannelDiv('project', '전체', '전체')} + {renderSubChannelDiv('project', '토이 프로젝트', '토이 프로젝트')} + {renderSubChannelDiv('project', '연계 프로젝트', '연계 프로젝트')} +
+
+
+ ); +}; + +export default SidebarRecruitment; diff --git a/src/components/SidebarRecruitment/style.tsx b/src/components/SidebarRecruitment/style.tsx new file mode 100644 index 00000000..16783c3c --- /dev/null +++ b/src/components/SidebarRecruitment/style.tsx @@ -0,0 +1,39 @@ +import styled from 'styled-components'; + +export const AllChannelsWrapper = styled.div` + height: calc(100vh - 72px); + + min-width: 16.25%; + max-width: 16.25%; + padding-top: 1%; + background-color: ${(props) => props.theme.sideMenu}; + color: ${(props) => props.theme.text}; +`; + +export const ChannelWrapper = styled.div` + padding-left: 10%; +`; + +export const ChannelDiv = styled.div` + font-size: 1.4rem; + font-weight: bold; + display: flex; + align-items: center; + position: relative; +`; + +export const SubChannelDiv = styled.div` + font-size: 1.15rem; + display: flex; + align-items: center; + position: relative; + padding: 5px; + &:hover { + background-color: ${(props) => props.theme.pointItem}; + cursor: pointer; + } +`; + +export const ChannelHr = styled.hr` + border: solid 1px ${(props) => props.theme.pointItem}; +`; diff --git a/src/components/SidebarWiki/index.tsx b/src/components/SidebarWiki/index.tsx new file mode 100644 index 00000000..b61cedad --- /dev/null +++ b/src/components/SidebarWiki/index.tsx @@ -0,0 +1,352 @@ +// SidebarWiki.tsx 파일 내에서 +import React, { useEffect, useState, useRef } from 'react'; +import { useRecoilState } from 'recoil'; +import { ThemeChange, channelState, subChannelState } from '../../utils/recoil'; +import { + AllChannelsWrapper, + ChannelWrapper, + DropDownOptions, + SubDropDownOptions, + ChannelFlexDiv, + SubChannelFlexDiv, + MoreHorizIconWrapper, + ChannelHr, + CreateChannelDiv, + OptionButton, + ChannelDiv, + SubChannelDiv, +} from './style'; + +import { handleGetDocs, deleteChannelDoc, deleteFieldFromDoc, DocumentData, themeType } from '../../utils/firebase'; +import { QuerySnapshot } from 'firebase/firestore'; +import ChannelModal from '../ChannelModal'; +import MoreHorizIcon from '@mui/icons-material/MoreHoriz'; +import swal from 'sweetalert'; + +interface SidebarWikiProps { + onKeyClick: (value: any) => void; // 클릭된 값의 핸들러 함수를 props로 받습니다. +} + +const SidebarWiki: React.FC = ({ onKeyClick }) => { + const [isModalOpen, setIsModalOpen] = useState(false); + const [modalType, setModalType] = useState(''); + const [selectedSubChannelId, setSelectedSubChannelId] = useState(''); + const [selectedChannelId, setSelectedChannelId] = useState(''); + + const [docsWithFields, setDocsWithFields] = useState<{ docId: string; docKeys: string[]; docData: DocumentData }[]>( + [], + ); + const [channel, setChannel] = useRecoilState(channelState); + const [subChannel, setSubChannel] = useRecoilState(subChannelState); + const defaultChannels = ['기본 정보']; + const defaultSubChannels = ['과정 참여 규칙', '링크 모음']; + + const [back, setBack] = useState(''); + const [themeText, setThemeText] = useState(''); + const [currentTheme, setCurrentTheme] = useRecoilState(ThemeChange); + + useEffect(() => { + const selected = (theme: themeType) => { + setBack(theme.activeColor1); + setThemeText(theme.text); + }; + if (localStorage.getItem('theme')) { + const localtheme = localStorage.getItem('theme'); + if (localtheme) { + const color = JSON.parse(localtheme); + selected(color); + } + } + }, [currentTheme]); + + useEffect(() => { + if (!channel || !subChannel) { + setChannel(defaultChannels[0]); + setSubChannel(defaultSubChannels[0]); + } + }, [channel, subChannel, setChannel, setSubChannel]); + + const isSubChannelActive = (channelName: string, subChannelName: string) => { + return channel === channelName && subChannel === subChannelName; + }; + + // 드롭다운 상태를 각 아이템마다 저장할 배열 추가 + const [dropdownStates, setDropdownStates] = useState([]); + const [dropdownSubStates, setDropdownSubStates] = useState([]); + + const dropdownRef = useRef(null); + const dropdownSubRef = useRef(null); + + const toggleDropDown = (index: number) => { + const updatedStates = [...dropdownStates]; + updatedStates[index] = !updatedStates[index]; + setDropdownStates(updatedStates); + }; + + const toggleDropDownSub = (docIndex: number, itemIndex: number) => { + const updatedSubStates = [...dropdownSubStates]; + updatedSubStates[docIndex][itemIndex] = !updatedSubStates[docIndex][itemIndex]; + setDropdownSubStates(updatedSubStates); + }; + + useEffect(() => { + const updatedQuerySnapshot = handleGetDocs('wiki', (querySnapshot: QuerySnapshot) => { + const data: { docId: string; docKeys: string[]; docData: DocumentData }[] = []; + + querySnapshot.forEach((doc: any) => { + const docData = doc.data(); + const docId = doc.id; + const docKeys = Object.keys(docData); + docKeys.sort(); // 서브채널을 알파벳순으로 정렬 + data.push({ docId, docKeys, docData }); + }); + // 채널 목록을 알파벳순으로 정렬 + data.sort((a, b) => a.docId.localeCompare(b.docId)); + setDocsWithFields(data); + // 초기 드롭다운 상태를 false로 초기화 + setDropdownStates(new Array(data.length).fill(false)); + const subDropdownStates = new Array(data.length).fill([]).map(() => new Array(data.length).fill(false)); + setDropdownSubStates(subDropdownStates); + }); + + return () => { + updatedQuerySnapshot(); + }; + }, []); + + useEffect(() => { + // document에 클릭 이벤트 리스너를 추가하여 DropDownOptions 요소 외부를 클릭했을 때 초기 드롭다운 상태를 false로 변경 + const handleClickOutside = (event: any, currentIndex: number) => { + if (dropdownRef.current && !dropdownRef.current.contains(event.target)) { + // 클릭한 요소가 DropDownOptions 영역 외부에 있는 경우 + const updatedStates = [...dropdownStates]; + updatedStates[currentIndex] = false; + setDropdownStates(updatedStates); + } + if (dropdownSubRef.current && !dropdownSubRef.current.contains(event.target)) { + // 클릭한 요소가 SubDropDownOptions 영역 외부에 있는 경우 + const updatedSubStates = [...dropdownSubStates]; + updatedSubStates[currentIndex].fill(false); + setDropdownSubStates(updatedSubStates); + } + }; + + document.addEventListener('mousedown', (event) => { + docsWithFields.forEach((_, index) => { + handleClickOutside(event, index); + }); + }); + + return () => { + document.removeEventListener('mousedown', (event) => { + docsWithFields.forEach((_, index) => { + handleClickOutside(event, index); + }); + }); + }; + }, [dropdownStates, dropdownSubStates, docsWithFields]); + + const handleKeyClick = (value: any, channel: string, subChannel: string) => { + onKeyClick({ value, channel, subChannel }); // 클릭된 값을 상위 컴포넌트로 전달 + }; + const openModal = () => { + setIsModalOpen(true); + }; + + const closeModal = () => { + setIsModalOpen(false); + }; + + const collectionName = 'wiki'; + + return ( + <> + + {docsWithFields.map((item, index) => { + if (item.docId === defaultChannels[0]) { + return ( + + + # {item.docId} + + +
+ {item.docKeys.map((item2, index2) => ( + { + handleKeyClick(item.docData[item2], item.docId, item2); + setChannel(item.docId); + setSubChannel(item2); + }} + style={{ + color: isSubChannelActive(item.docId, item2) ? '#' : themeText, + backgroundColor: isSubChannelActive(item.docId, item2) ? back : '', + borderRadius: isSubChannelActive(item.docId, item2) ? '5px' : '', + fontWeight: isSubChannelActive(item.docId, item2) ? 'bold' : '', + }} + > + {item2} + + ))} +
+ +
+ ); + } + return null; + })} + + {docsWithFields.map((item, index) => { + if (item.docId !== defaultChannels[0]) { + return ( + + + # {item.docId} + + { + toggleDropDown(index); + console.log(dropdownStates); + }} + > + + {dropdownStates[index] && ( + + { + setModalType('CreateSub'); + setSelectedChannelId(item.docId); + openModal(); + }} + > + 추가 + + { + setModalType('Update'); + // setSelectedSubChannelId(item.docData); + setSelectedChannelId(item.docId); + openModal(); + }} + > + 수정 + + { + swal({ + title: '정말로 삭제하시겠습니까?', + text: '한번 삭제하면 되돌릴 수 없습니다!', + icon: 'warning', + buttons: ['취소', '삭제'], + dangerMode: true, + }).then((willDelete) => { + if (willDelete) { + swal('성공적으로 삭제되었습니다!', { + icon: 'success', + }); + deleteChannelDoc('wiki', item.docId); + } else { + swal('삭제가 취소되었습니다!'); + } + }); + }} + > + 삭제 + + + )} + + +
+ {item.docKeys.map((item2, index2) => ( + { + handleKeyClick(item.docData[item2], item.docId, item2); + setChannel(item.docId); + setSubChannel(item2); + }} + style={{ + color: isSubChannelActive(item.docId, item2) ? themeText : '', + backgroundColor: isSubChannelActive(item.docId, item2) ? back : '', + borderRadius: isSubChannelActive(item.docId, item2) ? '5px' : '', + fontWeight: isSubChannelActive(item.docId, item2) ? 'bold' : '', + }} + > + {item2} + + { + e.stopPropagation(); + toggleDropDownSub(index, index2); + }} + > + + {dropdownSubStates[index][index2] && ( + + { + setModalType('UpdateSub'); + setSelectedSubChannelId(item2); + setSelectedChannelId(item.docId); + openModal(); + }} + > + 수정 + + { + swal({ + title: '정말로 삭제하시겠습니까?', + text: '한번 삭제하면 되돌릴 수 없습니다!', + icon: 'warning', + buttons: ['취소', '삭제'], + dangerMode: true, + }).then((willDelete) => { + if (willDelete) { + swal('성공적으로 삭제되었습니다!', { + icon: 'success', + }); + deleteFieldFromDoc('wiki', item.docId, item2); + } else { + swal('삭제가 취소되었습니다!'); + } + }); + }} + > + 삭제 + + + )} + + ))} +
+ +
+ ); + } + return null; + })} + { + setModalType('Create'); + openModal(); + }} + > + + 채널 추가 + +
+ {isModalOpen && ( + + )} + + ); +}; + +export default SidebarWiki; diff --git a/src/components/SidebarWiki/style.tsx b/src/components/SidebarWiki/style.tsx new file mode 100644 index 00000000..036a14d2 --- /dev/null +++ b/src/components/SidebarWiki/style.tsx @@ -0,0 +1,134 @@ +import styled from 'styled-components'; + +export const AllChannelsWrapper = styled.div` + min-width: 16.25%; + max-width: 16.25%; + padding-top: 1%; + height: calc(100vh - 72px); + + background-color: ${(props) => props.theme.sideMenu}; + color: ${(props) => props.theme.text}; + overflow: auto; + + &::-webkit-scrollbar { + width: 12px; + } + + &::-webkit-scrollbar-thumb { + background-color: #ffffffb3; + border-radius: 6px; + border: 3px solid transparent; + } +`; + +export const ChannelWrapper = styled.div` + padding-left: 10%; +`; + +export const DropDownOptions = styled.div` + background: #ffffff; + border-radius: 8px; + position: absolute; + top: 90%; + left: 55%; + min-width: 5rem; + min-width: 40%; + + display: block; + box-shadow: 0 1px 8px rgba(0, 0, 0, 0.3); + padding: 5px 0; + z-index: 1; +`; + +export const SubDropDownOptions = styled.div` + background: #ffffff; + border-radius: 8px; + position: absolute; + top: 90%; + left: 55%; + min-width: 40%; + display: block; + box-shadow: 0 1px 8px rgba(0, 0, 0, 0.3); + padding: 5px 0; + z-index: 1; +`; + +export const MoreHorizIconWrapper = styled.div` + background-color: ${(props) => props.theme.pointItem}; + padding: 1px; + border-radius: 50%; + display: flex; + align-items: center; + margin-left: auto; + margin-right: 5%; + cursor: pointer; +`; + +export const ChannelFlexDiv = styled.div` + display: flex; + align-items: center; + position: relative; + font-size: 1.4rem; +`; + +export const SubChannelFlexDiv = styled.div` + display: flex; + align-items: center; + position: relative; + font-size: 1.15rem; + &:hover { + background-color: ${(props) => props.theme.pointItem}; + cursor: pointer; + & ${MoreHorizIconWrapper} { + background-color: ${(props) => props.theme.sideMenu}; + color: ${(props) => props.theme.text}; + } + } +`; + +export const ChannelHr = styled.hr` + border: solid 1px ${(props) => props.theme.pointItem}; +`; + +export const CreateChannelDiv = styled.div` + font-size: 1.4rem; + font-weight: bold; + padding: 5px 0px 5px 10%; + + &:hover { + background-color: ${(props) => props.theme.pointItem}; + cursor: pointer; + } +`; + +export const OptionButton = styled.button` + display: flex; + justify-content: center; + width: 100%; + font-size: 1.2rem; + border: none; + background-color: #ffffff; + + &:hover { + background-color: ${(props) => props.theme.activeColor1}; + cursor: pointer; + color: #ffffff; + } +`; + +export const ChannelDiv = styled.div` + font-size: 1.4rem; + font-weight: bold; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; +`; + +export const SubChannelDiv = styled.div` + font-size: 1.15rem; + font-weight: bold; + padding: 5px; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; +`; diff --git a/src/components/Slider/index.tsx b/src/components/Slider/index.tsx new file mode 100644 index 00000000..92c4597f --- /dev/null +++ b/src/components/Slider/index.tsx @@ -0,0 +1,48 @@ +import React from 'react'; +import banner01 from '../../common/mainImg/main-banner01.png'; +import banner02 from '../../common/mainImg/main-banner02.png'; +import banner03 from '../../common/mainImg/main-banner03.png'; + +import Carousel from 'react-material-ui-carousel'; +import { Paper } from '@mui/material'; +import { SliderImg, SliderItem } from './style'; +// REactSimplyCarousel uninstall + +interface slideItem { + item: { + img: string; + }; +} + +function Item(props: slideItem) { + return ( + + + + + + ); +} + +const Slider: React.FC = () => { + const items = [ + { + img: banner01, + }, + { + img: banner02, + }, + { + img: banner03, + }, + ]; + return ( + + {items.map((item) => ( + + ))} + + ); +}; + +export default Slider; diff --git a/src/components/Slider/style.tsx b/src/components/Slider/style.tsx new file mode 100644 index 00000000..064eb7be --- /dev/null +++ b/src/components/Slider/style.tsx @@ -0,0 +1,17 @@ +import styled from '@emotion/styled'; + +export const SliderItem = styled.div` + width: 1440px; + height: 400px; + + margin: 0 auto; + + background-color: #efefef; +`; + +export const SliderImg = styled.img` + display: block; + + width: 1440px; + height: 400px; +`; diff --git a/src/components/modal/Hooks/WhatTime.ts b/src/components/modal/Hooks/WhatTime.ts new file mode 100644 index 00000000..cd5ea8e2 --- /dev/null +++ b/src/components/modal/Hooks/WhatTime.ts @@ -0,0 +1,21 @@ +const CreateDay = () => { + // 현재 날짜를 생성 + const date = new Date(); + const year = date.getFullYear(); + const month = date.getMonth(); + const day = date.getDate(); + return year + '.' + month + '.' + day; +}; + +const CreateTime = () => { + // 현재 시간을 생성 + let Hours: number | string = new Date().getHours(); + let Min: number | string = new Date().getMinutes(); + let Sec: number | string = new Date().getSeconds(); + Hours = String(Hours).padStart(2, '0'); + Min = String(Min).padStart(2, '0'); + Sec = String(Sec).padStart(2, '0'); + return Hours + ' ' + ':' + ' ' + Min + ' ' + ':' + ' ' + Sec; +}; + +export { CreateDay, CreateTime }; diff --git a/src/components/modal/MyPage/MyPageBtn.tsx b/src/components/modal/MyPage/MyPageBtn.tsx new file mode 100644 index 00000000..5002cec6 --- /dev/null +++ b/src/components/modal/MyPage/MyPageBtn.tsx @@ -0,0 +1,66 @@ +import React, { useEffect, useState } from 'react'; +import { ModalBtn, ModalBtnImg } from './style'; +import MyPageModal from './MyPageModal'; +import { useRecoilState } from 'recoil'; +import { SlideOn, ThemeChange, UserEmail, UserId, UserImg, UserInfo, UserName } from '../../../utils/recoil'; +import { readUser } from '../../../utils/firebase'; +import Loading from '../../../common/profileImgloading.gif'; + +export default function MyPageBtn() { + const [userId, setUserId] = useRecoilState(UserId); + const [showMyPage, setShowMyPage] = useState(false); + const [slideOn, setSlideOn] = useRecoilState(SlideOn); + const [userName, setUserName] = useRecoilState(UserName); + const [userEmail, setUserEmail] = useRecoilState(UserEmail); + const [userInfo, setUserInfo] = useRecoilState(UserInfo); + const [userImg, setUserImg] = useRecoilState(UserImg); + + const onErrorImg = (e: any) => { + e.target.src = Loading; + }; + + useEffect(() => { + async function getUserData() { + try { + const user = await readUser('user', userId); + if (user) { + setUserName(user['name']); + setUserEmail(user['email']); + setUserImg(user['imageURL']); + setUserInfo(user['info']); + } + } catch { + console.log('error'); + } + } + getUserData(); + }, [userId]); + + const handleMyPage = () => { + if (showMyPage) { + setSlideOn(false); + setTimeout(() => { + setShowMyPage(false); + }, 900); + } else { + setShowMyPage(true); + setTimeout(() => { + setSlideOn(true); + }, 10); + } + }; + return ( + { + if (userId.length > 0) { + handleMyPage(); + } else { + alert('로그인 후 사용이 가능합니다.'); + } + }} + > + + {showMyPage && userId.length > 0 && } + + ); +} diff --git a/src/components/modal/MyPage/MyPageCommute.tsx b/src/components/modal/MyPage/MyPageCommute.tsx new file mode 100644 index 00000000..5eee3d89 --- /dev/null +++ b/src/components/modal/MyPage/MyPageCommute.tsx @@ -0,0 +1,19 @@ +import React from 'react'; +import ShowCurrentTime from '../Timer/ShowCurrentTime'; +import Btns from '../Timer/Btns'; +import { CommuteModalBox } from './commuteStyle'; + +interface OwnProps { + setTimeRenewal: React.Dispatch>; +} + +const MyPageCommute: React.FC = ({ setTimeRenewal }) => { + return ( + + + + + ); +}; + +export default MyPageCommute; diff --git a/src/components/modal/MyPage/MyPageModal.tsx b/src/components/modal/MyPage/MyPageModal.tsx new file mode 100644 index 00000000..12a2cee7 --- /dev/null +++ b/src/components/modal/MyPage/MyPageModal.tsx @@ -0,0 +1,76 @@ +import React, { useEffect, useState } from 'react'; +import { MarginLeft, MyPage, MyPageHeader, MyPageFooter, MyPageExitBtn, MyPageContents } from './style'; +import CloseIcon from '@mui/icons-material/Close'; +import MyPageUser from './MyPageUser'; +import MyPageTimelog from './MyPageTimelog'; +import { useRecoilState } from 'recoil'; +import { ReadTimelog, SlideOn, TimeLog, UserId } from '../../../utils/recoil'; +import MyPageCommute from './MyPageCommute'; +import FastcampusDday from '../Timer/FastcampusDday'; +import MyPageReadLog from './MyPageReadLog'; +import { readUser } from '../../../utils/firebase'; +import MyPageTheme from './MyPageTheme'; + +interface OwnProps { + handleMyPage(): void; +} + +const MyPageModal: React.FC = ({ handleMyPage }) => { + const [userId, setUserId] = useRecoilState(UserId); + const [slideOn, setSlideOn] = useRecoilState(SlideOn); + const [showTimerModal, setShowTimerModal] = useState(false); + const [showReadModal, setShowReadModal] = useState(false); + const [showModal, setShowModal] = useState(true); + const [timeRenewal, setTimeRenewal] = useState(); + const [readTimelog, setReadTimelog] = useRecoilState(ReadTimelog); + const [timelog, setTimelog] = useRecoilState(TimeLog); + + useEffect(() => { + async function getTimelog() { + try { + const userData = await readUser('user', userId); + if (userData) { + const timelog = userData['timelog'].reverse(); + setReadTimelog(timelog); + } + } catch { + console.log('error'); + } + } + getTimelog(); + }, [timelog]); + + const handleModal = (number: number) => { + if (number) { + setShowModal(true); + } else { + setShowModal(false); + } + }; + + return ( + { + e.stopPropagation(); + }} + value={slideOn} + > + + Profile + + + + + + + + {showModal && } + {!showModal && } + + + + + ); +}; + +export default MyPageModal; diff --git a/src/components/modal/MyPage/MyPageReadLog.tsx b/src/components/modal/MyPage/MyPageReadLog.tsx new file mode 100644 index 00000000..35c0976d --- /dev/null +++ b/src/components/modal/MyPage/MyPageReadLog.tsx @@ -0,0 +1,29 @@ +import React from 'react'; +import { TimelogBox, TimelogBoxScroll, TimelogEl, TimelogText } from './style'; +import { ReadTimelog } from '../../../utils/recoil'; +import { useRecoilState } from 'recoil'; + +export default function MyPageReadLog() { + const [readTimelog, setReadTimelog] = useRecoilState(ReadTimelog); + + return ( + + + {readTimelog.length > 0 ? ( + readTimelog.map((e: string, i) => { + e = e.replace('|', '\n'); + return ( + + {e} + + ); + }) + ) : ( + + 현재 저장된 기록이 없습니다. + + )} + + + ); +} diff --git a/src/components/modal/MyPage/MyPageTheme.tsx b/src/components/modal/MyPage/MyPageTheme.tsx new file mode 100644 index 00000000..c67beb2a --- /dev/null +++ b/src/components/modal/MyPage/MyPageTheme.tsx @@ -0,0 +1,130 @@ +import React from 'react'; +import { MyPageThemeBox, ThemeColorEl, ThemeColors } from './style'; +import { useRecoilState } from 'recoil'; +import { ThemeChange, ThemeRing, UserId } from '../../../utils/recoil'; +import { updateUserTheme } from '../../../utils/firebase'; + +const themeBorder = [ + { + first: '3px solid #fff', + second: 'none', + third: 'none', + fourth: 'none', + }, + { + first: 'none', + second: '3px solid #fff', + third: 'none', + fourth: 'none', + }, + { + first: 'none', + second: 'none', + third: '3px solid #fff', + fourth: 'none', + }, + { + first: 'none', + second: 'none', + third: 'none', + fourth: '3px solid #fff', + }, +]; + +export default function MyPageTheme() { + const [theme, setTheme] = useRecoilState(ThemeChange); + const [userId, setUserId] = useRecoilState(UserId); + const [themeRing, setThemeRing] = useRecoilState(ThemeRing); + const themes = { + eggPlant: { + navBar: '#350d36', + sideMenu: '#3F0E40', + pointItem: '#4D2A51', + text: '#fff', + activeColor1: '#1164A3', + activeColor2: '#2BAC76', + recruitmentBack: '#ECE7EC', + }, + banana: { + navBar: '#FFC806', + sideMenu: '#FFEB84', + pointItem: '#FFF8D4', + text: '#591035', + activeColor1: '#c24d51', + activeColor2: '#4C6DC2', + recruitmentBack: '#fef1ad', + }, + sweetDessert: { + navBar: '#FFC2C0', + sideMenu: '#FFEEED', + pointItem: '#fff', + text: '#4A154B', + activeColor1: '#f89d48', + activeColor2: '#37BD8D', + recruitmentBack: '#fff6f5', + }, + indigo: { + navBar: '#001A5E', + sideMenu: '#7d9bfd', + pointItem: '#5d80ff', + text: '#fff', + activeColor1: '#50ce73', + activeColor2: '#2153FF', + recruitmentBack: '#e9eeff', + }, + }; + + return ( + + Theme + + { + setThemeRing(themeBorder[0]); + setTheme(themes.eggPlant); + updateUserTheme('user', userId, themes.eggPlant, themeBorder[0]); + localStorage.setItem('theme', JSON.stringify(themes.eggPlant)); + localStorage.setItem('themeRing', JSON.stringify(themeBorder[0])); + }} + thisTheme={themes.eggPlant} + selectTheme={themeRing.first} + > + { + setThemeRing(themeBorder[1]); + setTheme(themes.banana); + updateUserTheme('user', userId, themes.banana, themeBorder[1]); + localStorage.setItem('theme', JSON.stringify(themes.banana)); + localStorage.setItem('themeRing', JSON.stringify(themeBorder[1])); + }} + thisTheme={themes.banana} + selectTheme={themeRing.second} + > + { + setThemeRing(themeBorder[2]); + setTheme(themes.sweetDessert); + updateUserTheme('user', userId, themes.sweetDessert, themeBorder[2]); + localStorage.setItem('theme', JSON.stringify(themes.sweetDessert)); + localStorage.setItem('themeRing', JSON.stringify(themeBorder[2])); + }} + thisTheme={themes.sweetDessert} + selectTheme={themeRing.third} + > + { + setThemeRing(themeBorder[3]); + setTheme(themes.indigo); + updateUserTheme('user', userId, themes.indigo, themeBorder[3]); + localStorage.setItem('theme', JSON.stringify(themes.indigo)); + localStorage.setItem('themeRing', JSON.stringify(themeBorder[3])); + }} + thisTheme={themes.indigo} + selectTheme={themeRing.fourth} + > + + + ); +} + +export { themeBorder }; diff --git a/src/components/modal/MyPage/MyPageTimelog.tsx b/src/components/modal/MyPage/MyPageTimelog.tsx new file mode 100644 index 00000000..c26d2f66 --- /dev/null +++ b/src/components/modal/MyPage/MyPageTimelog.tsx @@ -0,0 +1,48 @@ +import React from 'react'; +import { FlexBox, FlexBoxColumn, MarginLeft, MyPageContents, RedCircle } from './style'; +import { useRecoilState } from 'recoil'; +import { TimerOn } from '../../../utils/recoil'; +import TimerIcon from '@mui/icons-material/Timer'; +import BookIcon from '@mui/icons-material/Book'; +import { ChangeTimelog, ChangeTimer } from './commuteStyle'; + +interface OwnProps { + handleTimerModal(number: number): void; + handleReadModal(number: number): void; + showModal: boolean; +} + +const MyPageTimelog: React.FC = ({ handleTimerModal, handleReadModal, showModal }) => { + const [timerOn, setTimerOn] = useRecoilState(TimerOn); + + return ( + + + + Commute + + + + + + { + handleTimerModal(1); + }} + style={{ cursor: 'pointer' }} + > + + + { + handleReadModal(0); + }} + style={{ cursor: 'pointer' }} + > + + + + ); +}; + +export default MyPageTimelog; diff --git a/src/components/modal/MyPage/MyPageUser.tsx b/src/components/modal/MyPage/MyPageUser.tsx new file mode 100644 index 00000000..08153c0a --- /dev/null +++ b/src/components/modal/MyPage/MyPageUser.tsx @@ -0,0 +1,36 @@ +import { MyPageProfile, ProfileContent, ProfileImg } from './style'; +import { useRecoilState } from 'recoil'; +import { UserId, UserName, UserEmail, UserInfo, UserImg } from '../../../utils/recoil'; +import UserEditBtn from './UserEditBtn'; +import Textarea from '@mui/joy/Textarea'; +import { updateUserInfo } from '../../../utils/firebase'; + +export default function MyPageUser() { + const [userId, setUserId] = useRecoilState(UserId); + const [userName, setUserName] = useRecoilState(UserName); + const [userEmail, setUserEmail] = useRecoilState(UserEmail); + const [userInfo, setUserInfo] = useRecoilState(UserInfo); + const [userImg, setUserImg] = useRecoilState(UserImg); + + return ( + + + + {userName} + + + {userEmail} +