├── .eslintcache ├── .gitignore ├── LICENSE ├── README.md ├── package.json ├── public ├── favicon.ico ├── images │ └── snow.svg ├── index.html ├── manifest.json └── robots.txt ├── src ├── index.css ├── index.tsx ├── pages │ ├── GeneratePage.tsx │ ├── HomePage.tsx │ └── LetterPage.tsx ├── react-app-env.d.ts ├── reportWebVitals.ts ├── routes │ └── index.tsx └── setupTests.ts ├── tsconfig.json └── yarn.lock /.eslintcache: -------------------------------------------------------------------------------- 1 | [{"/Users/suhdonghwi/Documents/projects/letter-to-santa/src/index.tsx":"1","/Users/suhdonghwi/Documents/projects/letter-to-santa/src/pages/HomePage.tsx":"2","/Users/suhdonghwi/Documents/projects/letter-to-santa/src/reportWebVitals.ts":"3","/Users/suhdonghwi/Documents/projects/letter-to-santa/src/routes/index.tsx":"4","/Users/suhdonghwi/Documents/projects/letter-to-santa/src/pages/GeneratePage.tsx":"5","/Users/suhdonghwi/Documents/projects/letter-to-santa/src/pages/LetterPage.tsx":"6"},{"size":1030,"mtime":1608390144275,"results":"7","hashOfConfig":"8"},{"size":1667,"mtime":1608408200091,"results":"9","hashOfConfig":"8"},{"size":425,"mtime":1608370283739,"results":"10","hashOfConfig":"8"},{"size":582,"mtime":1608387956754,"results":"11","hashOfConfig":"8"},{"size":4360,"mtime":1608395605772,"results":"12","hashOfConfig":"8"},{"size":5508,"mtime":1608391894568,"results":"13","hashOfConfig":"8"},{"filePath":"14","messages":"15","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},"1fakstz",{"filePath":"16","messages":"17","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"18","messages":"19","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"20","messages":"21","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"22","messages":"23","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"24","messages":"25","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},"/Users/suhdonghwi/Documents/projects/letter-to-santa/src/index.tsx",[],"/Users/suhdonghwi/Documents/projects/letter-to-santa/src/pages/HomePage.tsx",[],"/Users/suhdonghwi/Documents/projects/letter-to-santa/src/reportWebVitals.ts",[],"/Users/suhdonghwi/Documents/projects/letter-to-santa/src/routes/index.tsx",[],"/Users/suhdonghwi/Documents/projects/letter-to-santa/src/pages/GeneratePage.tsx",[],"/Users/suhdonghwi/Documents/projects/letter-to-santa/src/pages/LetterPage.tsx",[]] -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 서동휘 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 산타 할아버지께 편지 🎅 2 | 3 | 아이들이 온라인으로 산타에게 가지고 싶은 선물들을 적어놓은 편지를 전송하면 미리 등록해둔 이메일로 4 | 해당 내용이 전송되는, 착한 아이들을 위한 피싱 사이트입니다. 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "letter-to-santa", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@testing-library/jest-dom": "^5.11.4", 7 | "@testing-library/react": "^11.1.0", 8 | "@testing-library/user-event": "^12.1.10", 9 | "@types/jest": "^26.0.15", 10 | "@types/node": "^12.0.0", 11 | "@types/react": "^16.9.53", 12 | "@types/react-dom": "^16.9.8", 13 | "@types/react-router-dom": "^5.1.6", 14 | "@types/styled-components": "^5.1.7", 15 | "dayjs": "^1.9.7", 16 | "emailjs-com": "^2.6.4", 17 | "firebase": "^8.2.1", 18 | "react": "^17.0.1", 19 | "react-dom": "^17.0.1", 20 | "react-router-dom": "^5.2.0", 21 | "react-scripts": "4.0.1", 22 | "react-snowfall": "^1.0.2", 23 | "styled-components": "^5.2.1", 24 | "sweetalert2": "^10.12.5", 25 | "typescript": "^4.0.3", 26 | "web-vitals": "^0.2.4" 27 | }, 28 | "scripts": { 29 | "start": "react-scripts start", 30 | "build": "react-scripts build", 31 | "test": "react-scripts test", 32 | "eject": "react-scripts eject" 33 | }, 34 | "eslintConfig": { 35 | "extends": [ 36 | "react-app", 37 | "react-app/jest" 38 | ] 39 | }, 40 | "browserslist": { 41 | "production": [ 42 | ">0.2%", 43 | "not dead", 44 | "not op_mini all" 45 | ], 46 | "development": [ 47 | "last 1 chrome version", 48 | "last 1 firefox version", 49 | "last 1 safari version" 50 | ] 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/suhdonghwi/letter-to-santa/ed874cee542250d8ccabe5da845e5e145ef667e8/public/favicon.ico -------------------------------------------------------------------------------- /public/images/snow.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | 산타 할아버지께 편지 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Letter To Santa", 3 | "name": "Letter To Santa", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#20c997", 14 | "background_color": "#f1f3f5" 15 | } 16 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'NanumBarunpen'; 3 | src: url('https://cdn.jsdelivr.net/gh/projectnoonnu/noonfonts_two@1.0/NanumBarunpen.woff') format('woff'); 4 | font-weight: normal; 5 | font-style: normal; 6 | } 7 | 8 | html, body, #root { 9 | font-family: 'NanumBarunpen'; 10 | 11 | height: 100%; 12 | margin: 0; 13 | } 14 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import "./index.css"; 4 | import reportWebVitals from "./reportWebVitals"; 5 | import firebase from "firebase"; 6 | 7 | import Root from "./routes"; 8 | import { init } from "emailjs-com"; 9 | 10 | const firebaseConfig = { 11 | apiKey: "AIzaSyBcEy7ZESIHxNlSVcPLtMat3TSoPjWJHzI", 12 | authDomain: "letter-to-santa-61efd.firebaseapp.com", 13 | projectId: "letter-to-santa-61efd", 14 | storageBucket: "letter-to-santa-61efd.appspot.com", 15 | messagingSenderId: "1082826458249", 16 | appId: "1:1082826458249:web:b4cf47d13705cd8776b258", 17 | measurementId: "G-8XMJXTNVCF", 18 | }; 19 | 20 | firebase.initializeApp(firebaseConfig); 21 | firebase.analytics(); 22 | 23 | init("user_tbeVQ3Dgxtdq1fCYiQ2OB"); 24 | 25 | ReactDOM.render( 26 | 27 | 28 | , 29 | document.getElementById("root") 30 | ); 31 | 32 | // If you want to start measuring performance in your app, pass a function 33 | // to log results (for example: reportWebVitals(console.log)) 34 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 35 | reportWebVitals(); 36 | -------------------------------------------------------------------------------- /src/pages/GeneratePage.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import styled from "styled-components/macro"; 3 | import Swal from "sweetalert2"; 4 | import firebase from "firebase"; 5 | 6 | const Main = styled.main` 7 | min-height: 100%; 8 | 9 | display: flex; 10 | flex-direction: column; 11 | align-items: center; 12 | justify-content: center; 13 | 14 | padding: 3rem 2rem; 15 | box-sizing: border-box; 16 | `; 17 | 18 | const Title = styled.h1` 19 | font-family: "NanumBarunpen"; 20 | font-size: 3rem; 21 | color: #212529; 22 | 23 | margin: 0.5rem 0; 24 | 25 | @media screen and (max-width: 560px) { 26 | font-size: 1.7rem; 27 | } 28 | `; 29 | 30 | const Description = styled.p` 31 | font-family: "NanumBarunpen"; 32 | font-size: 1.4rem; 33 | 34 | color: #868e96; 35 | max-width: 40rem; 36 | margin: 2rem 0 5rem 0; 37 | 38 | @media screen and (max-width: 560px) { 39 | font-size: 1rem; 40 | margin: 1.5rem 0 3rem 0; 41 | } 42 | `; 43 | 44 | const Form = styled.form` 45 | width: 17rem; 46 | `; 47 | 48 | const Label = styled.label` 49 | display: block; 50 | 51 | font-family: "NanumBarunpen"; 52 | color: #212529; 53 | font-size: 1.1rem; 54 | 55 | margin-bottom: 0.5rem; 56 | `; 57 | 58 | const Input = styled.input` 59 | background-color: #e9ecef; 60 | border: none; 61 | border-radius: 10px; 62 | 63 | font-family: "NanumBarunpen"; 64 | font-size: 1.2rem; 65 | padding: 0.6rem 0.7rem; 66 | 67 | width: 100%; 68 | box-sizing: border-box; 69 | 70 | margin-bottom: 2rem; 71 | `; 72 | 73 | const Submit = styled.input` 74 | cursor: pointer; 75 | 76 | font-family: "NanumBarunpen"; 77 | 78 | appearance: none; 79 | background-color: #12b886; 80 | 81 | border: none; 82 | border-radius: 10px; 83 | 84 | padding: 0.5rem 1rem; 85 | font-size: 1.1rem; 86 | color: white; 87 | width: 100%; 88 | 89 | &.loading { 90 | background-color: #adb5bd; 91 | } 92 | `; 93 | 94 | function generateKey(existingKeys: string[]) { 95 | let result: string; 96 | const characters = 97 | "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; 98 | 99 | do { 100 | result = ""; 101 | for (let i = 0; i < 5; i++) { 102 | result += characters.charAt( 103 | Math.floor(Math.random() * characters.length) 104 | ); 105 | } 106 | } while (existingKeys.includes(result)); 107 | 108 | return result; 109 | } 110 | 111 | export default function GeneratePage() { 112 | const [name, setName] = useState(""); 113 | const [email, setEmail] = useState(""); 114 | const [loading, setLoading] = useState(false); 115 | 116 | async function onSubmit(event: React.FormEvent) { 117 | event.preventDefault(); 118 | if (loading) return; 119 | 120 | let trimmedName = name.trim(); 121 | let trimmedEmail = email.trim(); 122 | 123 | if (trimmedName.length === 0) { 124 | Swal.fire({ 125 | icon: "error", 126 | title: "다시 확인해주세요!", 127 | text: "이름을 입력해주세요.", 128 | heightAuto: false, 129 | }); 130 | return; 131 | } 132 | 133 | if (trimmedEmail.length === 0) { 134 | Swal.fire({ 135 | icon: "error", 136 | title: "다시 확인해주세요!", 137 | text: "이메일을 입력해주세요.", 138 | heightAuto: false, 139 | }); 140 | return; 141 | } 142 | 143 | setLoading(true); 144 | 145 | const dataSnapshot = await firebase.database().ref("data").once("value"); 146 | const key = generateKey(Object.keys(dataSnapshot.val())); 147 | 148 | await firebase 149 | .database() 150 | .ref(`data/${key}`) 151 | .set({ name: trimmedName, email: trimmedEmail }); 152 | 153 | setLoading(false); 154 | setName(""); 155 | setEmail(""); 156 | 157 | const link = "https://official.christmas/" + key; 158 | Swal.fire({ 159 | icon: "success", 160 | title: "성공!", 161 | html: `생성된 링크 : ${link}
위 링크를 복사해서 아이에게 보내주세요.`, 162 | heightAuto: false, 163 | }); 164 | } 165 | 166 | return ( 167 |
168 | 산타 할아버지께 편지 🎄 169 | 170 | 아이들이 받고 싶은 크리스마스 선물들을 편지에 써서 전송하면 미리 171 | 등록해두신 이메일 주소로 전송됩니다. 아이들의 크리스마스 선물 구입에 172 | 참고해보시고, 행복한 연말 보내세요! 173 | 174 | 175 |
176 | 177 | setName(e.target.value)} 182 | /> 183 | 184 | setEmail(e.target.value)} 189 | /> 190 | 191 | 196 | 197 |
198 | ); 199 | } 200 | -------------------------------------------------------------------------------- /src/pages/HomePage.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled from "styled-components/macro"; 3 | import dayjs from "dayjs"; 4 | import Snowfall from "react-snowfall"; 5 | 6 | const Main = styled.main` 7 | height: 100%; 8 | 9 | padding: 0 1rem; 10 | box-sizing: border-box; 11 | 12 | background-image: url("./images/snow.svg"); 13 | background-repeat: no-repeat; 14 | background-position: center bottom; 15 | background-size: auto 50vh; 16 | background-color: #f1f3f5; 17 | 18 | @media screen and (max-height: 600px) { 19 | background: none; 20 | } 21 | `; 22 | 23 | const ContentBox = styled.div` 24 | display: flex; 25 | flex-direction: column; 26 | align-items: center; 27 | justify-content: center; 28 | 29 | height: 50vh; 30 | 31 | @media screen and (max-height: 600px) { 32 | height: 100%; 33 | } 34 | `; 35 | 36 | const CheerMessage = styled.p` 37 | font-family: "NanumBarunpen"; 38 | font-size: 2rem; 39 | 40 | color: #495057; 41 | 42 | @media screen and (max-width: 560px) { 43 | font-size: 1.4rem; 44 | margin: 0.2rem 0; 45 | } 46 | 47 | @media screen and (max-height: 600px) { 48 | margin: 0.2rem 0; 49 | } 50 | `; 51 | 52 | const DDayText = styled.p` 53 | font-family: "NanumBarunpen"; 54 | font-size: 7rem; 55 | margin: 0; 56 | `; 57 | 58 | const ToChristmas = styled.small` 59 | font-family: "NanumBarunpen"; 60 | font-size: 1.5rem; 61 | color: #868e96; 62 | `; 63 | 64 | export default function HomePage() { 65 | const dday = Math.floor(dayjs().diff(dayjs("2020-12-25"), "day", true)); 66 | const ddayString = 67 | dday === 0 ? "-DAY" : dday < 0 ? dday.toString() : "+" + dday; 68 | 69 | return ( 70 |
71 | 72 | 73 | 조금 특별했던 2020, 수고 많으셨습니다! 🎄 74 | D{ddayString} 75 | 크리스마스까지 76 | 77 |
78 | ); 79 | } 80 | -------------------------------------------------------------------------------- /src/pages/LetterPage.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | import styled from "styled-components/macro"; 3 | import { RouteComponentProps, useHistory } from "react-router-dom"; 4 | import firebase from "firebase"; 5 | import Swal from "sweetalert2"; 6 | import emailjs from "emailjs-com"; 7 | 8 | const Main = styled.main` 9 | min-height: 100%; 10 | 11 | display: flex; 12 | flex-direction: column; 13 | align-items: center; 14 | justify-content: center; 15 | 16 | padding: 3rem 2rem; 17 | box-sizing: border-box; 18 | `; 19 | 20 | const LoadingText = styled.p` 21 | font-size: 1.5rem; 22 | `; 23 | 24 | const Title = styled.h1` 25 | font-family: "NanumBarunpen"; 26 | font-size: 3rem; 27 | color: #212529; 28 | 29 | margin: 0.5rem 0; 30 | 31 | @media screen and (max-width: 560px) { 32 | font-size: 1.7rem; 33 | } 34 | `; 35 | 36 | const Description = styled.p` 37 | font-family: "NanumBarunpen"; 38 | font-size: 1.4rem; 39 | 40 | color: #868e96; 41 | max-width: 40rem; 42 | margin: 2rem 0 3rem 0; 43 | 44 | @media screen and (max-width: 560px) { 45 | font-size: 1rem; 46 | margin: 1.5rem 0 3rem 0; 47 | } 48 | `; 49 | 50 | const Form = styled.div` 51 | width: 30rem; 52 | 53 | @media screen and (max-width: 560px) { 54 | width: 20rem; 55 | } 56 | 57 | @media screen and (max-width: 350px) { 58 | width: 16rem; 59 | } 60 | `; 61 | 62 | const Label = styled.label` 63 | display: block; 64 | 65 | font-family: "NanumBarunpen"; 66 | color: #212529; 67 | font-size: 1.1rem; 68 | 69 | margin-bottom: 0.5rem; 70 | `; 71 | 72 | const LetterInput = styled.textarea` 73 | background-color: #f1f3f5; 74 | border: none; 75 | border-radius: 10px; 76 | 77 | font-family: "NanumBarunpen"; 78 | font-size: 1.2rem; 79 | padding: 1rem; 80 | 81 | width: 100%; 82 | box-sizing: border-box; 83 | 84 | margin-bottom: 2rem; 85 | resize: vertical; 86 | outline: none; 87 | `; 88 | 89 | const YesNo = styled.div` 90 | display: flex; 91 | margin-top: 1rem; 92 | `; 93 | 94 | const YesNoButton = styled.button<{ color: string }>` 95 | cursor: pointer; 96 | outline: none; 97 | 98 | flex: 1; 99 | 100 | background-color: ${(props) => props.color}; 101 | appearance: none; 102 | 103 | padding: 0.5rem 0; 104 | color: white; 105 | 106 | font-family: "NanumBarunpen"; 107 | font-size: 1.2rem; 108 | 109 | border: none; 110 | border-radius: 10px; 111 | 112 | margin: 0 0.5rem; 113 | `; 114 | 115 | export default function LetterPage({ 116 | match, 117 | }: RouteComponentProps<{ key: string }>) { 118 | const [loading, setLoading] = useState(true); 119 | const [sending, setSending] = useState(false); 120 | 121 | const [content, setContent] = useState(""); 122 | const [name, setName] = useState(""); 123 | const [email, setEmail] = useState(""); 124 | 125 | const key = match.params.key; 126 | const history = useHistory(); 127 | 128 | useEffect(() => { 129 | async function fetch() { 130 | const keySnapshot = await firebase.database().ref("data").once("value"); 131 | if (!keySnapshot.hasChild(key)) { 132 | history.push("/"); 133 | return; 134 | } 135 | 136 | const dataSnapshot = await firebase 137 | .database() 138 | .ref(`data/${key}`) 139 | .once("value"); 140 | const data = dataSnapshot.val(); 141 | 142 | if (data.sent) { 143 | history.push("/"); 144 | return; 145 | } 146 | 147 | setName(data.name); 148 | setEmail(data.email); 149 | setLoading(false); 150 | } 151 | 152 | fetch(); 153 | }); 154 | 155 | async function onYes() { 156 | if (sending) return; 157 | if (content.trim().length < 10) { 158 | Swal.fire({ 159 | icon: "error", 160 | title: "오류", 161 | text: "편지 내용이 너무 짧아요! 조금 더 입력해주세요.", 162 | heightAuto: false, 163 | }); 164 | return; 165 | } 166 | 167 | const params = { 168 | from_name: name, 169 | to_email: email, 170 | message: content, 171 | }; 172 | 173 | try { 174 | setSending(true); 175 | await emailjs.send("service_axuu45f", "template_fl5v0ub", params); 176 | await firebase.database().ref(`data/${key}/sent`).set(true); 177 | await Swal.fire({ 178 | icon: "success", 179 | title: "성공!", 180 | text: 181 | "산타 할아버지께 편지를 보냈어요! 크리스마스까지 잘 기다릴 수 있죠?", 182 | heightAuto: false, 183 | }); 184 | 185 | history.push("/"); 186 | } catch (err) { 187 | console.log(err); 188 | Swal.fire({ 189 | icon: "error", 190 | title: "오류", 191 | text: "편지를 보내기에 실패했어요. 이따가 다시 써주세요!", 192 | heightAuto: false, 193 | }); 194 | 195 | setSending(false); 196 | } 197 | } 198 | 199 | function onNo() { 200 | if (sending) return; 201 | Swal.fire({ 202 | icon: "error", 203 | title: "이런..", 204 | text: "산타 할아버지께서는 착한 아이에게만 소중한 선물을 준답니다.", 205 | heightAuto: false, 206 | }); 207 | } 208 | 209 | return ( 210 |
211 | {loading ? ( 212 | 로딩중 ... 213 | ) : ( 214 | <> 215 | 산타 할아버지께 편지 🎄 216 | 217 | {name} 친구! 올해는 산타 할아버지께 어떤 선물을 받고 싶나요? 산타 218 | 할아버지께 쓰고 싶은 말과 함께 적어보아요! 정성스럽게 적을수록 산타 219 | 할아버지께서 좋아하실 거예요! 220 | 221 |
222 | 223 | setContent(e.target.value)} 228 | /> 229 | 230 | 231 | 235 | 네! 236 | 237 | 241 | 아니요.. 242 | 243 | 244 | 245 | 246 | )} 247 |
248 | ); 249 | } 250 | -------------------------------------------------------------------------------- /src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /src/reportWebVitals.ts: -------------------------------------------------------------------------------- 1 | import { ReportHandler } from 'web-vitals'; 2 | 3 | const reportWebVitals = (onPerfEntry?: ReportHandler) => { 4 | if (onPerfEntry && onPerfEntry instanceof Function) { 5 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 6 | getCLS(onPerfEntry); 7 | getFID(onPerfEntry); 8 | getFCP(onPerfEntry); 9 | getLCP(onPerfEntry); 10 | getTTFB(onPerfEntry); 11 | }); 12 | } 13 | }; 14 | 15 | export default reportWebVitals; 16 | -------------------------------------------------------------------------------- /src/routes/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { BrowserRouter, Switch, Route, Redirect } from "react-router-dom"; 3 | 4 | import HomePage from "../pages/HomePage"; 5 | import GeneratePage from "../pages/GeneratePage"; 6 | import LetterPage from "../pages/LetterPage"; 7 | 8 | export default function Root() { 9 | return ( 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /src/setupTests.ts: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom'; 6 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "strict": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "noFallthroughCasesInSwitch": true, 16 | "module": "esnext", 17 | "moduleResolution": "node", 18 | "resolveJsonModule": true, 19 | "isolatedModules": true, 20 | "noEmit": true, 21 | "jsx": "react-jsx" 22 | }, 23 | "include": [ 24 | "src" 25 | ] 26 | } 27 | --------------------------------------------------------------------------------