commit
1351fec866
|
|
@ -15,9 +15,13 @@
|
|||
"axios": "^0.21.1",
|
||||
"dayjs": "^1.10.4",
|
||||
"gapi-script": "^1.2.0",
|
||||
"i18next": "^20.2.4",
|
||||
"i18next-browser-languagedetector": "^6.1.1",
|
||||
"i18next-http-backend": "^1.2.4",
|
||||
"react": "^17.0.1",
|
||||
"react-dom": "^17.0.1",
|
||||
"react-hook-form": "^6.15.4",
|
||||
"react-i18next": "^11.8.15",
|
||||
"react-router-dom": "^5.2.0",
|
||||
"react-scripts": "4.0.3",
|
||||
"typescript": "^4.2.2",
|
||||
|
|
|
|||
62
crabfit-frontend/public/i18n/en-US/common.json
Normal file
62
crabfit-frontend/public/i18n/en-US/common.json
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
{
|
||||
"name": "Crab Fit",
|
||||
"tagline": "Create your own",
|
||||
"cta": "Create your own Crab Fit!",
|
||||
"created": "Created {{date}}",
|
||||
"donate": {
|
||||
"info": "Thank you for using Crab Fit. If you like it, consider donating.",
|
||||
"button": "Donate",
|
||||
"title": "Every amount counts :)",
|
||||
"options": {
|
||||
"$2": "Donate $2",
|
||||
"$5": "Donate $5",
|
||||
"$10": "Donate $10",
|
||||
"choose": "Choose an amount"
|
||||
},
|
||||
"messages": {
|
||||
"about": "Did you know that Crab Fit costs more that $100 per month? If it's helped you out at all, consider donating to help keep it running. 🦀",
|
||||
"success": "Thank you for your donation! Without you, Crab Fit wouldn't be free, so thank you and keep being super awesome!",
|
||||
"error": "Cannot make donation through Google. Please try donating through the website crab.fit 🦀"
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"name": "Options",
|
||||
"weekStart": {
|
||||
"label": "Week starts on",
|
||||
"options": {
|
||||
"Sunday": "Sunday",
|
||||
"Monday": "Monday"
|
||||
}
|
||||
},
|
||||
"timeFormat": {
|
||||
"label": "Time format",
|
||||
"options": {
|
||||
"12h": "12h",
|
||||
"24h": "24h"
|
||||
}
|
||||
},
|
||||
"theme": {
|
||||
"label": "Theme",
|
||||
"options": {
|
||||
"System": "System",
|
||||
"Light": "Light",
|
||||
"Dark": "Dark"
|
||||
}
|
||||
},
|
||||
"highlight": {
|
||||
"label": "Highlight highest availability",
|
||||
"title": "Make the highest availability on the heatmap stand out",
|
||||
"options": {
|
||||
"Off": "Off",
|
||||
"On": "On"
|
||||
}
|
||||
},
|
||||
"language": {
|
||||
"label": "Language",
|
||||
"options": {
|
||||
"en-US": "English (US)",
|
||||
"ko": "Korean"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
63
crabfit-frontend/public/i18n/en-US/event.json
Normal file
63
crabfit-frontend/public/i18n/en-US/event.json
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
{
|
||||
"available": "available",
|
||||
|
||||
"nav": {
|
||||
"title": "Click to copy",
|
||||
"copied": "Copied!",
|
||||
"shareinfo": "Copy the link to this page, or share via <1>email</1>.",
|
||||
"shareinfo_alt": "Click the link above to copy it to your clipboard, or share via <1>email</1>.",
|
||||
"email_subject": "Scheduling {{event_name}}",
|
||||
"email_body": "Visit this link to enter your availabilities:"
|
||||
},
|
||||
"form": {
|
||||
"signed_out": "Sign in to add your availability",
|
||||
"signed_in": "Signed in as {{name}}",
|
||||
|
||||
"name": "Your name",
|
||||
"password": "Password (optional)",
|
||||
"button": "Login",
|
||||
"info": "These details are only for this event. Use a password to prevent others from changing your availability.",
|
||||
|
||||
"timezone": "Your time zone",
|
||||
|
||||
"errors": {
|
||||
"password_incorrect": "Password is incorrect. Check your name is spelled right.",
|
||||
"unknown": "Failed to login. Please try again."
|
||||
},
|
||||
|
||||
"created_in_timezone": "This event was created in the timezone <strong>{{timezone}}</strong>. <3>Click here</3> to use it.",
|
||||
"local_timezone": "Your local timezone is detected to be <strong>{{timezone}}</strong>. <3>Click here</3> to use it."
|
||||
},
|
||||
"offline": {
|
||||
"title": "You are offline",
|
||||
"body": "A Crab Fit doesn't work offline.<br />Make sure you're connected to the internet and try again."
|
||||
},
|
||||
"error": {
|
||||
"title": "Event not found",
|
||||
"body": "Check that the url you entered is correct."
|
||||
},
|
||||
|
||||
"tabs": {
|
||||
"you": "Your availability",
|
||||
"you_tooltip": "Login to set your availability",
|
||||
"group": "Group availability"
|
||||
},
|
||||
|
||||
"group": {
|
||||
"legend_tooltip": "Click to highlight highest availability",
|
||||
"info1": "Hover or tap the calendar below to see who is available",
|
||||
"info2": "Click the names below to view people individually"
|
||||
},
|
||||
|
||||
"you": {
|
||||
"info": "Click and drag the calendar below to set your availabilities",
|
||||
"google_cal": {
|
||||
"login": "Sync with Google Calendar",
|
||||
"logout": "log out",
|
||||
"select_all": "Select all",
|
||||
"select_none": "Select none",
|
||||
"info": "Importing will overwrite your current availability",
|
||||
"button": "Import availability"
|
||||
}
|
||||
}
|
||||
}
|
||||
23
crabfit-frontend/public/i18n/en-US/help.json
Normal file
23
crabfit-frontend/public/i18n/en-US/help.json
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
{
|
||||
"name": "How to Crab Fit",
|
||||
|
||||
"p1": "Crab Fit is a tool that helps you when planning events with friends or coworkers. You just create an event, enter your availability, send it out, and see when everyone is free!",
|
||||
"p2": "See below for detailed steps of how to Crab Fit your event.",
|
||||
|
||||
"s1": "Step 1",
|
||||
|
||||
"p3": "Use the form at <1>crab.fit</1> to make a new event. You only need to put in the rough time period for when your event occurs here, not your availability.",
|
||||
"p4": "For example, we'll use \"Jenny's Birthday Lunch\". Jenny wants her birthday lunch to happen on the same week as her birthday, the 15th of April, but she knows that not all of her friends are available on the 15th. She also doesn't want to do it on the weekend.",
|
||||
"p5": "Jenny also knows that since it's a lunch event, it can't start before 11am or go any later than 5pm.",
|
||||
|
||||
"s2": "Step 2",
|
||||
|
||||
"p6": "Enter your availability for the event you just created.",
|
||||
"p7": "In our example, Jenny now puts in her availability for her birthday lunch. She is free all week, except after 3pm on Tuesday and Wednesday, and before 1pm on Friday.",
|
||||
|
||||
"s3": "Step 3",
|
||||
|
||||
"p8": "Send the link to everyone you want to come.",
|
||||
"p9": "After Jenny has sent the link to her friends and waited for them to also fill out their availabilities, she can now easily see them all on the heatmap below and choose the darkest area for a time that suits everyone!",
|
||||
"p10": "In this example, 1pm to 3pm on Friday the 16th works for all Jenny's friends."
|
||||
}
|
||||
57
crabfit-frontend/public/i18n/en-US/home.json
Normal file
57
crabfit-frontend/public/i18n/en-US/home.json
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
{
|
||||
"create": "CREATE A",
|
||||
"recently_visited": "Recently visited",
|
||||
"nav": {
|
||||
"about": "About",
|
||||
"donate": "Donate"
|
||||
},
|
||||
"form": {
|
||||
"name": {
|
||||
"label": "Give your event a name!",
|
||||
"sublabel": "Or leave blank to generate one"
|
||||
},
|
||||
"dates": {
|
||||
"label": "What dates might work?",
|
||||
"sublabel": "Click and drag to select",
|
||||
"options": {
|
||||
"specific": "Specific dates",
|
||||
"week": "Days of the week"
|
||||
},
|
||||
"tooltips": {
|
||||
"previous": "Previous month",
|
||||
"next": "Next month",
|
||||
"today": "today"
|
||||
}
|
||||
},
|
||||
"times": {
|
||||
"label": "What times might work?",
|
||||
"sublabel": "Click and drag to select a time range"
|
||||
},
|
||||
"timezone": {
|
||||
"label": "And the timezone",
|
||||
"defaultOption": "Select..."
|
||||
},
|
||||
|
||||
"button": "Create",
|
||||
"errors": {
|
||||
"no_dates": "There aren't any dates selected",
|
||||
"same_times": "The start and end times can't be the same",
|
||||
"no_time": "There isn't any time selected",
|
||||
"unknown": "Something went wrong. Please try again later."
|
||||
}
|
||||
},
|
||||
"offline": "You can't create a Crab Fit when you don't have an internet connection. Please make sure you're connected.",
|
||||
|
||||
"about": {
|
||||
"name": "About Crab Fit",
|
||||
"events": "Events created",
|
||||
"availabilities": "Availabilities entered",
|
||||
"content": {
|
||||
"p1": "Crab Fit helps you fit your event around everyone's schedules. Simply create an event above and send the link to everyone that is participating. Results update live and you will be able to see a heat-map of when everyone is free.<1/><2>Learn more about how to Crab Fit</2>.",
|
||||
"p2": "Create a lot of Crab Fits? Get the <1>Chrome extension</1> or <3>Firefox extension</3> for your browser! You can also download the <5>Android app</5> to Crab Fit on the go.",
|
||||
"p3": "Created by <1>Ben Grant</1>, Crab Fit is the modern-day solution to your group event planning debates.",
|
||||
"p4": "The code for Crab Fit is open source, if you find any issues or want to contribute, you can visit the <1>repository</1>. By using Crab Fit you agree to the <3>privacy policy</3>.",
|
||||
"p5": "Crab Fit costs more than <strong>$100 per month</strong> to run. Consider donating below if it helped you out so it can stay free for everyone. 🦀"
|
||||
}
|
||||
}
|
||||
}
|
||||
52
crabfit-frontend/public/i18n/en-US/privacy.json
Normal file
52
crabfit-frontend/public/i18n/en-US/privacy.json
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
{
|
||||
"name": "Privacy Policy",
|
||||
|
||||
"p1": "This SERVICE is provided by Benjamin Grant at no cost and is intended for use as is.",
|
||||
"p2": "This page is used to inform visitors regarding the policies of the collection, use, and disclosure of Personal Information if using the Service.",
|
||||
"p3": "If you choose to use the Service, then you agree to the collection and use of information in relation to this policy. The Personal Information that is collected is used for providing and improving the Service. Your information will not be used or shared with anyone except as described in this Privacy Policy.",
|
||||
|
||||
"h1": "Information Collection and Use",
|
||||
|
||||
"p4": "The Service uses third party services that may collect information used to identify you.",
|
||||
"p5": "Links to privacy policies of the third party service providers used by the Service:",
|
||||
"link": "Google Play Services",
|
||||
|
||||
"h2": "Log Data",
|
||||
|
||||
"p6": "When you use the Service, in the case of an error, data and information is collected to improve the Service, which may include your IP address, device name, operating system version, app configuration and the time and date of the error.",
|
||||
|
||||
"h3": "Cookies",
|
||||
|
||||
"p7": "Cookies are files with a small amount of data that are commonly used as anonymous unique identifiers. These are sent to your browser from the websites that you visit and are stored on your device's internal memory.",
|
||||
"p8": "Cookies are used by Google Analytics to track you across the web and provide anonymous statistics to improve the Service.",
|
||||
|
||||
"h4": "Service Providers",
|
||||
|
||||
"p9": "Third-party companies may be employed for the following reasons:",
|
||||
"l1": "To facilitate the Service",
|
||||
"l2": "To provide the Service on our behalf",
|
||||
"l3": "To perform Service-related services",
|
||||
"l4": "To assist in analyzing how the Service is used",
|
||||
"p10": "To perform these tasks, the third parties may have access to your Personal Information, but are obligated not to disclose or use this information for any purpose except the above.",
|
||||
|
||||
"h5": "Security",
|
||||
|
||||
"p11": "Personal Information that is shared via the Service is protected, however remember that no method of transmission over the internet, or method of electronic storage is 100% secure and reliable, so take care when sharing Personal Information.",
|
||||
|
||||
"h6": "Links to Other Sites",
|
||||
|
||||
"p12": "The Service may contain links to other sites. If you click on a third-party link, you will be directed to that site. Note that these external sites are not operated by the Service. Therefore, you are advised to review the Privacy Policy of these websites.",
|
||||
|
||||
"h7": "Children's Privacy",
|
||||
|
||||
"p13": "The Service does not address anyone under the age of 13. Personally identifiable information is not knowingly collected from children under 13. If discovered that a child under 13 has provided the Service with personal information, such information will be immediately deleted from the servers. If you are a parent or guardian and you are aware that your child has provided the Service with personal information, please <1>contact us</1> so that this information can be removed.",
|
||||
|
||||
"h8": "Changes to This Privacy Policy",
|
||||
|
||||
"p14": "This Privacy Policy may be updated from time to time. Thus, you are advised to review this page periodically for any changes.",
|
||||
"p15": "This policy is effective as of 2021-04-20",
|
||||
|
||||
"h9": "Contact Us",
|
||||
|
||||
"p16": "If you have any questions or suggestions about the Privacy Policy, do not hesitate to contact us at <1>benjamin.grantGRA0007+crabfit@gmail.com</1>."
|
||||
}
|
||||
62
crabfit-frontend/public/i18n/ko/common.json
Normal file
62
crabfit-frontend/public/i18n/ko/common.json
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
{
|
||||
"name": "Crab Fit",
|
||||
"tagline": "직접 만들어 봐",
|
||||
"cta": "나만의 Crab Fit을 만드세요!",
|
||||
"created": "{{date}} 생성됨",
|
||||
"donate": {
|
||||
"info": "Crab Fit을 이용해 주셔서 감사합니다. 마음에 들면 기부를 고려하십시오.",
|
||||
"button": "기부하다",
|
||||
"title": "모든 금액이 차이를 만듭니다",
|
||||
"options": {
|
||||
"$2": "$ 2 기부",
|
||||
"$5": "$ 5 기부",
|
||||
"$10": "$ 10 기부",
|
||||
"choose": "금액을 선택하세요"
|
||||
},
|
||||
"messages": {
|
||||
"about": "Crab Fit이 한 달에 $ 100 이상이라는 사실을 알고 계셨습니까? 도움이 되었으면 계속해서 운영 할 수 있도록 기부하는 것을 고려하십시오. 🦀",
|
||||
"success": "기부 해 주셔서 감사합니다! 당신이 없었다면 Crab Fit은 무료가 될 수 없으니 감사하고 계속해서 최고가 되세요!",
|
||||
"error": "Google을 통해 기부 할 수 없습니다. 웹 사이트 crab.fit을 통해 기부 해주세요 🦀"
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"name": "옵션",
|
||||
"weekStart": {
|
||||
"label": "주 시작",
|
||||
"options": {
|
||||
"Sunday": "일요일",
|
||||
"Monday": "월요일"
|
||||
}
|
||||
},
|
||||
"timeFormat": {
|
||||
"label": "시간 형식",
|
||||
"options": {
|
||||
"12h": "12 시간",
|
||||
"24h": "24 시간"
|
||||
}
|
||||
},
|
||||
"theme": {
|
||||
"label": "색조",
|
||||
"options": {
|
||||
"System": "계통",
|
||||
"Light": "연한",
|
||||
"Dark": "짙은"
|
||||
}
|
||||
},
|
||||
"highlight": {
|
||||
"label": "고 가용성 강조",
|
||||
"title": "히트 맵에서 가장 높은 가용성을 돋보이게합니다.",
|
||||
"options": {
|
||||
"Off": "꺼진",
|
||||
"On": "켜진"
|
||||
}
|
||||
},
|
||||
"language": {
|
||||
"label": "언어",
|
||||
"options": {
|
||||
"en-US": "영어 (미국)",
|
||||
"ko": "한국어"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
63
crabfit-frontend/public/i18n/ko/event.json
Normal file
63
crabfit-frontend/public/i18n/ko/event.json
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
{
|
||||
"available": "가능",
|
||||
|
||||
"nav": {
|
||||
"title": "클릭하여 클립 보드에 복사",
|
||||
"copied": "클립 보드에 복사되었습니다!",
|
||||
"shareinfo": "이 페이지에 대한 링크를 복사하거나 <1>이메일을</1> 통해 공유하세요.",
|
||||
"shareinfo_alt": "위의 링크를 클릭하여 클립 보드에 복사하거나 <1>이메일로</1> 공유하세요.",
|
||||
"email_subject": "일정 {{event_name}}",
|
||||
"email_body": "당신의 이용 가능성을 입력하려면이 링크를 방문하십시오 :"
|
||||
},
|
||||
"form": {
|
||||
"signed_out": "당신의 가용성을 추가하려면 로그인",
|
||||
"signed_in": "{{name}} 으로 로그인되었습니다.",
|
||||
|
||||
"name": "당신의 이름",
|
||||
"password": "비밀번호 (선택 사항)",
|
||||
"button": "로그인",
|
||||
"info": "이러한 세부 정보는이 이벤트에 한정됩니다. 다른 사람이 귀하의 가용성을 변경하지 못하도록 암호를 사용하십시오.",
|
||||
|
||||
"timezone": "시간대",
|
||||
|
||||
"errors": {
|
||||
"password_incorrect": "비밀번호가 맞지 않습니다. 당신의 이름이 바로 철자 확인하십시오.",
|
||||
"unknown": "로그인 실패. 다시 시도하십시오."
|
||||
},
|
||||
|
||||
"created_in_timezone": "이 이벤트는 <strong>{{timezone}}</strong> 시간대로 생성되었습니다. 그것을 사용하려면 <3>여기를 클릭하십시오</3>.",
|
||||
"local_timezone": "현지 시간대가 <strong>{{timezone}}</strong> 감지되었습니다. 그것을 사용하려면 <3>여기를 클릭하십시오</3>."
|
||||
},
|
||||
"offline": {
|
||||
"title": "너는 지금 접속이 안되있어",
|
||||
"body": "Crab Fit은 오프라인에서 작동하지 않습니다.<br />인터넷에 연결되어 있는지 확인하고 다시 시도하세요."
|
||||
},
|
||||
"error": {
|
||||
"title": "이벤트를 찾을 수 없습니다",
|
||||
"body": "입력 한 URL이 올바른지 확인하십시오."
|
||||
},
|
||||
|
||||
"tabs": {
|
||||
"you": "귀하의 가용성",
|
||||
"you_tooltip": "가용성을 설정하려면 로그인하십시오.",
|
||||
"group": "그룹 가용성"
|
||||
},
|
||||
|
||||
"group": {
|
||||
"legend_tooltip": "가장 높은 가용성을 강조하려면 클릭하십시오",
|
||||
"info1": "누가 무료인지 확인하려면 아래 캘린더를 가리 키거나 탭하세요",
|
||||
"info2": "사람들을 개별적으로 보려면 아래 이름을 클릭하십시오"
|
||||
},
|
||||
|
||||
"you": {
|
||||
"info": "사용 가능 여부를 설정하려면 아래 캘린더를 클릭하고 드래그하세요",
|
||||
"google_cal": {
|
||||
"login": "Google 캘린더와 동기화",
|
||||
"logout": "로그 아웃",
|
||||
"select_all": "모두 선택",
|
||||
"select_none": "모두 선택 해제",
|
||||
"info": "가져 오면 현재 사용 가능 여부를 덮어 씁니다",
|
||||
"button": "가용성 가져 오기"
|
||||
}
|
||||
}
|
||||
}
|
||||
23
crabfit-frontend/public/i18n/ko/help.json
Normal file
23
crabfit-frontend/public/i18n/ko/help.json
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
{
|
||||
"name": "Crab Fit 방법",
|
||||
|
||||
"p1": "게 맞춤이 친구 나 동료와 이벤트를 계획 할 때 당신을 도와주는 도구이다. 이벤트를 만들고, 사용 가능 여부를 입력하고, 보내고, 모든 사람이 언제 무료인지 확인하기 만하면됩니다!",
|
||||
"p2": "방법에 대한 자세한 단계는 아래를 참조하십시오.",
|
||||
|
||||
"s1": "1 단계",
|
||||
|
||||
"p3": "<1>crab.fit</1>의 양식을 사용하여 새 이벤트를 만드세요. 여기서 이벤트가 발생하는 대략적인 기간 만 입력하면됩니다.",
|
||||
"p4": "예를 들어 \"Jenny의 생일 점심을\" 사용합니다. Jenny는 4 월 15 일 생일과 같은 주에 생일 점심 식사를하기를 원하지만 모든 친구가 15 일에 참석할 수있는 것은 아니라는 것을 알고 있습니다. 그녀는 또한 주말에하고 싶지 않습니다.",
|
||||
"p5": "Jenny는 점심 행사이기 때문에 오전 11시 이전에 시작하거나 오후 5시 이후에 갈 수 없다는 것도 알고 있습니다.",
|
||||
|
||||
"s2": "2 단계",
|
||||
|
||||
"p6": "방금 만든 이벤트에 대한 가용성을 입력하십시오.",
|
||||
"p7": "이 예에서 Jenny는 이제 생일 점심에 사용할 수있는 시간을 입력합니다. Jenny는 화요일과 수요일 오후 3시 이후와 금요일 오후 1시 이전을 제외하고 일주일 내내 무료입니다.",
|
||||
|
||||
"s3": "3 단계",
|
||||
|
||||
"p8": "오고 싶은 모든 사람에게 링크를 보냅니다.",
|
||||
"p9": "Jenny가 친구에게 링크를 전송하고 그들이 사용 가능 여부를 입력 할 때까지 기다린 후 Jenny는 아래 히트 맵에서 모두를보고 모두에게 적합한 시간 동안 가장 어두운 영역을 선택할 수 있습니다!",
|
||||
"p10": "이 예에서, 금요일 16 일 오후 3시에서 오후 1시까지 모든 제니의 친구를 위해 작동합니다."
|
||||
}
|
||||
57
crabfit-frontend/public/i18n/ko/home.json
Normal file
57
crabfit-frontend/public/i18n/ko/home.json
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
{
|
||||
"create": "만들기",
|
||||
"recently_visited": "최근 방문",
|
||||
"nav": {
|
||||
"about": "정보",
|
||||
"donate": "기부하다"
|
||||
},
|
||||
"form": {
|
||||
"name": {
|
||||
"label": "이벤트 이름을 지정하세요!",
|
||||
"sublabel": "또는 공백으로 두어 생성하십시오"
|
||||
},
|
||||
"dates": {
|
||||
"label": "어떤 날짜를 작동 할 수 있습니다?",
|
||||
"sublabel": "클릭하고 드래그하여 선택",
|
||||
"options": {
|
||||
"specific": "특정 날짜",
|
||||
"week": "요일"
|
||||
},
|
||||
"tooltips": {
|
||||
"previous": "지난 달",
|
||||
"next": "다음 달",
|
||||
"today": "오늘"
|
||||
}
|
||||
},
|
||||
"times": {
|
||||
"label": "어느 시간이 효과가 있습니까?",
|
||||
"sublabel": "시간 범위를 선택하려면 클릭하고 드래그하세요"
|
||||
},
|
||||
"timezone": {
|
||||
"label": "시간대",
|
||||
"defaultOption": "고르다..."
|
||||
},
|
||||
|
||||
"button": "창조하다",
|
||||
"errors": {
|
||||
"no_dates": "선택한 모든 날짜가 없습니다",
|
||||
"same_times": "시작 시간과 종료 시간은 같을 수 없습니다",
|
||||
"no_time": "선택한 시간이 없습니다",
|
||||
"unknown": "문제가 발생했습니다. 나중에 다시 시도 해주십시오."
|
||||
}
|
||||
},
|
||||
"offline": "인터넷에 연결되어 있지 않으면 Crab Fit을 만들 수 없습니다. 당신이 연결되어 있는지 확인하십시오.",
|
||||
|
||||
"about": {
|
||||
"name": "Crab Fit 정보",
|
||||
"events": "생성 된 이벤트 수",
|
||||
"availabilities": "입력 된 가용성 수",
|
||||
"content": {
|
||||
"p1": "Crab Fit은 모든 사람의 일정에 맞춰 이벤트를 조정할 수 있도록 도와줍니다. 위에서 이벤트를 만들고 참여하는 모든 사람에게 링크를 보내십시오. 결과가 실시간으로 업데이트되고 모든 사람이 가용성의 히트 맵을 볼 수 있습니다.<1/><2>Crab Fit 방법</2>.",
|
||||
"p2": "많이 만드시겠습니까? 브라우저 용 <1>Chrome 확장</1> 프로그램 또는 <3>Firefox 확장</3> 프로그램을 받으세요! <5>Android 앱을</5> 다운로드하여 이동 중에 Crab Fit을 사용할 수도 있습니다.",
|
||||
"p3": "<1>Ben Grant</1>가 만든 Crab Fit은 그룹 이벤트 계획 토론에 대한 현대적인 솔루션입니다.",
|
||||
"p4": "Crab Fit의 코드는 오픈 소스이므로 문제를 발견하거나 기여하고 싶다면 <1>저장소를</1> 방문 할 수 있습니다. Crab Fit을 사용하면 <3>개인 정보 보호 정책에</3> 동의하게됩니다.",
|
||||
"p5": "Crab Fit을 실행하는 데 <strong>월 $ 100</strong> 이상이 듭니다. 모든 사람이 무료로 사용할 수 있도록 아래 기부를 고려하세요. 🦀"
|
||||
}
|
||||
}
|
||||
}
|
||||
3
crabfit-frontend/public/i18n/ko/privacy.json
Normal file
3
crabfit-frontend/public/i18n/ko/privacy.json
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"name": "개인 정보 보호 정책에"
|
||||
}
|
||||
|
|
@ -139,7 +139,9 @@ const App = () => {
|
|||
)} />
|
||||
</Switch>
|
||||
|
||||
<Settings />
|
||||
<Suspense fallback={<Loading />}>
|
||||
<Settings />
|
||||
</Suspense>
|
||||
|
||||
{eggVisible && <Egg eggKey={eggKey} onClose={() => setEggVisible(false)} />}
|
||||
</ThemeProvider>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { useState, useRef, Fragment } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import dayjs from 'dayjs';
|
||||
import localeData from 'dayjs/plugin/localeData';
|
||||
import customParseFormat from 'dayjs/plugin/customParseFormat';
|
||||
|
|
@ -20,7 +21,7 @@ import {
|
|||
} from 'components/AvailabilityViewer/availabilityViewerStyle';
|
||||
import { Time } from './availabilityEditorStyle';
|
||||
|
||||
import { GoogleCalendar } from 'components';
|
||||
import { GoogleCalendar, Center } from 'components';
|
||||
|
||||
dayjs.extend(localeData);
|
||||
dayjs.extend(customParseFormat);
|
||||
|
|
@ -36,6 +37,7 @@ const AvailabilityEditor = ({
|
|||
onChange,
|
||||
...props
|
||||
}) => {
|
||||
const { t } = useTranslation('event');
|
||||
const [selectingTimes, _setSelectingTimes] = useState([]);
|
||||
const staticSelectingTimes = useRef([]);
|
||||
const setSelectingTimes = newTimes => {
|
||||
|
|
@ -53,6 +55,9 @@ const AvailabilityEditor = ({
|
|||
|
||||
return (
|
||||
<>
|
||||
<StyledMain>
|
||||
<Center>{t('event:you.info')}</Center>
|
||||
</StyledMain>
|
||||
{isSpecificDates && (
|
||||
<StyledMain>
|
||||
<GoogleCalendar
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { useState, useEffect, useRef, Fragment } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import dayjs from 'dayjs';
|
||||
import localeData from 'dayjs/plugin/localeData';
|
||||
import customParseFormat from 'dayjs/plugin/customParseFormat';
|
||||
|
|
@ -51,6 +52,7 @@ const AvailabilityViewer = ({
|
|||
const [touched, setTouched] = useState(false);
|
||||
const [tempFocus, setTempFocus] = useState(null);
|
||||
const [focusCount, setFocusCount] = useState(null);
|
||||
const { t } = useTranslation('event');
|
||||
|
||||
const wrapper = useRef();
|
||||
|
||||
|
|
@ -68,10 +70,10 @@ const AvailabilityViewer = ({
|
|||
total={people.filter(p => p.availability.length > 0).length}
|
||||
onSegmentFocus={count => setFocusCount(count)}
|
||||
/>
|
||||
<Center>Hover or tap the calendar below to see who is available</Center>
|
||||
<Center>{t('event:group.info1')}</Center>
|
||||
{people.length > 1 && (
|
||||
<>
|
||||
<Center>Click the names below to view people individually</Center>
|
||||
<Center>{t('event:group.info2')}</Center>
|
||||
<People>
|
||||
{people.map((person, i) =>
|
||||
<Person
|
||||
|
|
@ -152,7 +154,7 @@ const AvailabilityViewer = ({
|
|||
setTooltip({
|
||||
x: Math.round(cellBox.x-wrapperBox.x + cellBox.width/2),
|
||||
y: Math.round(cellBox.y-wrapperBox.y + cellBox.height)+6,
|
||||
available: `${peopleHere.length} / ${people.length} available`,
|
||||
available: `${peopleHere.length} / ${people.length} ${t('event:available')}`,
|
||||
date: parsedDate.hour(time.slice(0, 2)).minute(time.slice(2, 4)).format(isSpecificDates ? `${timeText} ddd, D MMM YYYY` : `${timeText} ddd`),
|
||||
people: peopleHere,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { useState, useEffect, useRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import dayjs from 'dayjs';
|
||||
import isToday from 'dayjs/plugin/isToday';
|
||||
import localeData from 'dayjs/plugin/localeData';
|
||||
|
|
@ -54,6 +55,7 @@ const CalendarField = ({
|
|||
...props
|
||||
}) => {
|
||||
const weekStart = useSettingsStore(state => state.weekStart);
|
||||
const { t } = useTranslation('home');
|
||||
|
||||
const [type, setType] = useState(0);
|
||||
|
||||
|
|
@ -110,9 +112,12 @@ const CalendarField = ({
|
|||
<ToggleField
|
||||
id="calendarMode"
|
||||
name="calendarMode"
|
||||
options={['Specific dates', 'Days of the week']}
|
||||
value={type ? 'Days of the week' : 'Specific dates'}
|
||||
onChange={value => setType(value === 'Specific dates' ? 0 : 1)}
|
||||
options={{
|
||||
'specific': t('form.dates.options.specific'),
|
||||
'week': t('form.dates.options.week'),
|
||||
}}
|
||||
value={type === 0 ? 'specific' : 'week'}
|
||||
onChange={value => setType(value === 'specific' ? 0 : 1)}
|
||||
/>
|
||||
|
||||
{type === 0 ? (
|
||||
|
|
@ -121,7 +126,7 @@ const CalendarField = ({
|
|||
<Button
|
||||
buttonHeight="30px"
|
||||
buttonWidth="30px"
|
||||
title="Previous month"
|
||||
title={t('form.dates.tooltips.previous')}
|
||||
type="button"
|
||||
onClick={() => {
|
||||
if (month-1 < 0) {
|
||||
|
|
@ -136,7 +141,7 @@ const CalendarField = ({
|
|||
<Button
|
||||
buttonHeight="30px"
|
||||
buttonWidth="30px"
|
||||
title="Next month"
|
||||
title={t('form.dates.tooltips.next')}
|
||||
type="button"
|
||||
onClick={() => {
|
||||
if (month+1 > 11) {
|
||||
|
|
@ -161,7 +166,7 @@ const CalendarField = ({
|
|||
key={y+x}
|
||||
otherMonth={date.month() !== month}
|
||||
isToday={date.isToday()}
|
||||
title={`${date.date()} ${dayjs.months()[date.month()]}${date.isToday() ? ' (today)' : ''}`}
|
||||
title={`${date.date()} ${dayjs.months()[date.month()]}${date.isToday() ? ` (${t('form.dates.tooltips.today')})` : ''}`}
|
||||
selected={selectedDates.includes(date.format('DDMMYYYY'))}
|
||||
selecting={selectingDates.includes(date)}
|
||||
mode={mode}
|
||||
|
|
@ -203,7 +208,7 @@ const CalendarField = ({
|
|||
<Date
|
||||
key={name}
|
||||
isToday={dayjs.weekdaysShort()[dayjs().day()-weekStart] === name}
|
||||
title={dayjs.weekdaysShort()[dayjs().day()-weekStart] === name ? 'Today' : ''}
|
||||
title={dayjs.weekdaysShort()[dayjs().day()-weekStart] === name ? t('form.dates.tooltips.today') : ''}
|
||||
selected={selectedDays.includes(((i + weekStart) % 7 + 7) % 7)}
|
||||
selecting={selectingDays.includes(((i + weekStart) % 7 + 7) % 7)}
|
||||
mode={mode}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,14 @@
|
|||
import { useEffect } from 'react';
|
||||
import { Button } from 'components';
|
||||
import { useTWAStore } from 'stores';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const PAYMENT_METHOD = 'https://play.google.com/billing';
|
||||
const SKU = 'crab_donation';
|
||||
|
||||
const Donate = ({ onDonate = null }) => {
|
||||
const store = useTWAStore();
|
||||
const { t } = useTranslation('common');
|
||||
|
||||
useEffect(() => {
|
||||
if (store.TWA === undefined) {
|
||||
|
|
@ -53,18 +55,18 @@ const Donate = ({ onDonate = null }) => {
|
|||
if (response.details && response.details.token) {
|
||||
const token = response.details.token;
|
||||
console.log(`Read Token: ${token.substring(0, 6)}...`);
|
||||
alert('Thank you for your donation! Without you, Crab Fit wouldn\'t be free, so thank you and keep being super awesome!');
|
||||
alert(t('donate.messages.success'));
|
||||
acknowledge(token);
|
||||
}
|
||||
})
|
||||
.catch(e => {
|
||||
console.error(e.message);
|
||||
alert('Cannot make donation through Google. Please try donating through the website crab.fit 🦀');
|
||||
alert(t('donate.messages.error'));
|
||||
});
|
||||
})
|
||||
.catch(e => {
|
||||
console.error(e);
|
||||
alert('Cannot make donation through Google. Please try donating through the website crab.fit 🦀');
|
||||
alert(t('donate.messages.error'));
|
||||
});
|
||||
};
|
||||
|
||||
|
|
@ -75,9 +77,9 @@ const Donate = ({ onDonate = null }) => {
|
|||
gtag('event', 'donate', { 'event_category': 'donate' });
|
||||
if (store.TWA) {
|
||||
event.preventDefault();
|
||||
if (window.confirm('Did you know that Crab Fit costs more that $100 per month? If it\'s helped you out at all, consider donating to help keep it running. 🦀')) {
|
||||
if (window.confirm(t('donate.messages.about'))) {
|
||||
if (purchase() === false) {
|
||||
alert('Cannot make donation through Google. Please try donating through the website crab.fit 🦀');
|
||||
alert(t('donate.messages.error'));
|
||||
}
|
||||
}
|
||||
} else if (onDonate !== null) {
|
||||
|
|
@ -94,8 +96,8 @@ const Donate = ({ onDonate = null }) => {
|
|||
buttonWidth="90px"
|
||||
type="button"
|
||||
tabIndex="-1"
|
||||
title="Every amount counts :)"
|
||||
>Donate</Button>
|
||||
title={t('donate.title')}
|
||||
>{t('donate.button')}</Button>
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,23 +1,25 @@
|
|||
import { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { Donate } from 'components';
|
||||
import { Wrapper, Link } from './footerStyle';
|
||||
|
||||
const Footer = () => {
|
||||
const Footer = (props) => {
|
||||
const [donateMode, setDonateMode] = useState(false);
|
||||
const { t } = useTranslation('common');
|
||||
|
||||
return (
|
||||
<Wrapper id="donate" donateMode={donateMode}>
|
||||
<Wrapper id="donate" donateMode={donateMode} {...props}>
|
||||
{donateMode ? (
|
||||
<>
|
||||
<Link href="https://www.paypal.com/donate?business=N89X6YXRT5HKW&item_name=Crab+Fit+Donation¤cy_code=AUD&amount=2" target="_blank">Donate $2</Link>
|
||||
<Link href="https://www.paypal.com/donate?business=N89X6YXRT5HKW&item_name=Crab+Fit+Donation¤cy_code=AUD&amount=5" target="_blank"><strong>Donate $5</strong></Link>
|
||||
<Link href="https://www.paypal.com/donate?business=N89X6YXRT5HKW&item_name=Crab+Fit+Donation¤cy_code=AUD&amount=10" target="_blank">Donate $10</Link>
|
||||
<Link href="https://www.paypal.com/donate?business=N89X6YXRT5HKW&item_name=Crab+Fit+Donation¤cy_code=AUD" target="_blank">Choose an amount</Link>
|
||||
<Link href="https://www.paypal.com/donate?business=N89X6YXRT5HKW&item_name=Crab+Fit+Donation¤cy_code=AUD&amount=2" target="_blank">{t('donate.options.$2')}</Link>
|
||||
<Link href="https://www.paypal.com/donate?business=N89X6YXRT5HKW&item_name=Crab+Fit+Donation¤cy_code=AUD&amount=5" target="_blank"><strong>{t('donate.options.$5')}</strong></Link>
|
||||
<Link href="https://www.paypal.com/donate?business=N89X6YXRT5HKW&item_name=Crab+Fit+Donation¤cy_code=AUD&amount=10" target="_blank">{t('donate.options.$10')}</Link>
|
||||
<Link href="https://www.paypal.com/donate?business=N89X6YXRT5HKW&item_name=Crab+Fit+Donation¤cy_code=AUD" target="_blank">{t('donate.options.choose')}</Link>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<span>Thank you for using Crab Fit. If you like it, consider donating.</span>
|
||||
<span>{t('donate.info')}</span>
|
||||
<Donate onDonate={() => setDonateMode(true)} />
|
||||
</>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,22 @@ export const Wrapper = styled.footer`
|
|||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
${props => props.small && `
|
||||
margin: 60px auto 0;
|
||||
width: 250px;
|
||||
max-width: initial;
|
||||
display: block;
|
||||
|
||||
& span {
|
||||
display: block;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
`}
|
||||
|
||||
${props => props.donateMode && `
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
`}
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { useState, useEffect } from 'react';
|
||||
import { loadGapiInsideDOM } from 'gapi-script';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { Button, Center } from 'components';
|
||||
import { Loader } from '../Loading/loadingStyle';
|
||||
|
|
@ -23,6 +24,7 @@ const GoogleCalendar = ({ timeZone, timeMin, timeMax, onImport }) => {
|
|||
const [signedIn, setSignedIn] = useState(undefined);
|
||||
const [calendars, setCalendars] = useState(undefined);
|
||||
const [freeBusyLoading, setFreeBusyLoading] = useState(false);
|
||||
const { t } = useTranslation('event');
|
||||
|
||||
const calendarLogin = async () => {
|
||||
const gapi = await loadGapiInsideDOM();
|
||||
|
|
@ -101,7 +103,7 @@ const GoogleCalendar = ({ timeZone, timeMin, timeMax, onImport }) => {
|
|||
secondaryColor="#3367BD">
|
||||
<LoginButton>
|
||||
<img src={googleLogo} alt="" />
|
||||
<span>Sync with Google Calendar</span>
|
||||
<span>{t('event:you.google_cal.login')}</span>
|
||||
</LoginButton>
|
||||
</Button>
|
||||
</Center>
|
||||
|
|
@ -109,10 +111,10 @@ const GoogleCalendar = ({ timeZone, timeMin, timeMax, onImport }) => {
|
|||
<CalendarList>
|
||||
<p>
|
||||
{/* eslint-disable-next-line */}
|
||||
<strong>Sync with Google Calendar</strong> (<a href="#" onClick={e => {
|
||||
<strong>{t('event:you.google_cal.login')}</strong> (<a href="#" onClick={e => {
|
||||
e.preventDefault();
|
||||
signOut();
|
||||
}}>log out</a>)
|
||||
}}>{t('event:you.google_cal.logout')}</a>)
|
||||
</p>
|
||||
<Options>
|
||||
{calendars !== undefined && !calendars.every(c => c.checked) && (
|
||||
|
|
@ -120,14 +122,14 @@ const GoogleCalendar = ({ timeZone, timeMin, timeMax, onImport }) => {
|
|||
<a href="#" onClick={e => {
|
||||
e.preventDefault();
|
||||
setCalendars(calendars.map(c => ({...c, checked: true})));
|
||||
}}>Select all</a>
|
||||
}}>{t('event:you.google_cal.select_all')}</a>
|
||||
)}
|
||||
{calendars !== undefined && calendars.every(c => c.checked) && (
|
||||
/* eslint-disable-next-line */
|
||||
<a href="#" onClick={e => {
|
||||
e.preventDefault();
|
||||
setCalendars(calendars.map(c => ({...c, checked: false})));
|
||||
}}>Select none</a>
|
||||
}}>{t('event:you.google_cal.select_none')}</a>
|
||||
)}
|
||||
</Options>
|
||||
{calendars !== undefined ? calendars.map(calendar => (
|
||||
|
|
@ -148,14 +150,14 @@ const GoogleCalendar = ({ timeZone, timeMin, timeMax, onImport }) => {
|
|||
)}
|
||||
{calendars !== undefined && (
|
||||
<>
|
||||
<Info>Importing will overwrite your current availability</Info>
|
||||
<Info>{t('event:you.google_cal.info')}</Info>
|
||||
<Button
|
||||
buttonWidth="170px"
|
||||
buttonHeight="35px"
|
||||
isLoading={freeBusyLoading}
|
||||
disabled={freeBusyLoading}
|
||||
onClick={() => importAvailability()}
|
||||
>Import availability</Button>
|
||||
>{t('event:you.google_cal.button')}</Button>
|
||||
</>
|
||||
)}
|
||||
</CalendarList>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { useTheme } from '@emotion/react';
|
||||
import { useSettingsStore } from 'stores';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import {
|
||||
Wrapper,
|
||||
|
|
@ -16,17 +17,18 @@ const Legend = ({
|
|||
...props
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation('event');
|
||||
const highlight = useSettingsStore(state => state.highlight);
|
||||
const setHighlight = useSettingsStore(state => state.setHighlight);
|
||||
|
||||
return (
|
||||
<Wrapper>
|
||||
<Label>{min}/{total} available</Label>
|
||||
<Label>{min}/{total} {t('event:available')}</Label>
|
||||
|
||||
<Bar
|
||||
onMouseOut={() => onSegmentFocus(null)}
|
||||
onClick={() => setHighlight(!highlight)}
|
||||
title="Click to highlight highest availability"
|
||||
title={t('event:group.legend_tooltip')}
|
||||
>
|
||||
{[...Array(max+1-min).keys()].map(i => i+min).map(i =>
|
||||
<Grade
|
||||
|
|
@ -38,7 +40,7 @@ const Legend = ({
|
|||
)}
|
||||
</Bar>
|
||||
|
||||
<Label>{max}/{total} available</Label>
|
||||
<Label>{max}/{total} {t('event:available')}</Label>
|
||||
</Wrapper>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
30
crabfit-frontend/src/components/Recents/Recents.tsx
Normal file
30
crabfit-frontend/src/components/Recents/Recents.tsx
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
import { useTranslation } from 'react-i18next';
|
||||
import { useRecentsStore } from 'stores';
|
||||
import dayjs from 'dayjs';
|
||||
import relativeTime from 'dayjs/plugin/relativeTime';
|
||||
|
||||
import { AboutSection, StyledMain } from '../../pages/Home/homeStyle';
|
||||
import { Recent } from './recentsStyle';
|
||||
|
||||
dayjs.extend(relativeTime);
|
||||
|
||||
const Recents = () => {
|
||||
const recents = useRecentsStore(state => state.recents);
|
||||
const { t } = useTranslation(['home', 'common']);
|
||||
|
||||
return !!recents.length && (
|
||||
<AboutSection id="recents">
|
||||
<StyledMain>
|
||||
<h2>{t('home:recently_visited')}</h2>
|
||||
{recents.map(event => (
|
||||
<Recent href={`/${event.id}`} key={event.id}>
|
||||
<span className="name">{event.name}</span>
|
||||
<span className="date" title={dayjs.unix(event.created).format('D MMMM, YYYY')}>{t('common:created', { date: dayjs.unix(event.created).fromNow() })}</span>
|
||||
</Recent>
|
||||
))}
|
||||
</StyledMain>
|
||||
</AboutSection>
|
||||
);
|
||||
};
|
||||
|
||||
export default Recents;
|
||||
36
crabfit-frontend/src/components/Recents/recentsStyle.ts
Normal file
36
crabfit-frontend/src/components/Recents/recentsStyle.ts
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
import styled from '@emotion/styled';
|
||||
|
||||
export const Recent = styled.a`
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 5px 0;
|
||||
flex-wrap: wrap;
|
||||
|
||||
& .name {
|
||||
font-weight: 700;
|
||||
font-size: 1.1em;
|
||||
color: ${props => props.theme.primaryDark};
|
||||
flex: 1;
|
||||
display: block;
|
||||
}
|
||||
& .date {
|
||||
font-weight: 400;
|
||||
opacity: .8;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
&:hover .name {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
@media (max-width: 500px) {
|
||||
display: block;
|
||||
|
||||
& .date {
|
||||
white-space: normal;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
|
@ -11,23 +11,31 @@ const SelectField = ({
|
|||
id,
|
||||
options = [],
|
||||
inline = false,
|
||||
small = false,
|
||||
defaultOption,
|
||||
register,
|
||||
...props
|
||||
}) => (
|
||||
<Wrapper inline={inline}>
|
||||
{label && <StyledLabel htmlFor={id} inline={inline}>{label}</StyledLabel>}
|
||||
<Wrapper inline={inline} small={small}>
|
||||
{label && <StyledLabel htmlFor={id} inline={inline} small={small}>{label}</StyledLabel>}
|
||||
{subLabel && <StyledSubLabel htmlFor={id}>{subLabel}</StyledSubLabel>}
|
||||
|
||||
<StyledSelect
|
||||
id={id}
|
||||
ref={register}
|
||||
small={small}
|
||||
{...props}
|
||||
>
|
||||
{defaultOption && <option value="">{defaultOption}</option>}
|
||||
{options.map((value, i) =>
|
||||
<option key={i} value={value}>{value}</option>
|
||||
)}
|
||||
{Array.isArray(options) ? (
|
||||
options.map(value =>
|
||||
<option key={value} value={value}>{value}</option>
|
||||
)
|
||||
) : (
|
||||
Object.entries(options).map(([key, value]) =>
|
||||
<option key={key} value={key}>{value}</option>
|
||||
)
|
||||
)}
|
||||
</StyledSelect>
|
||||
</Wrapper>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -6,6 +6,9 @@ export const Wrapper = styled.div`
|
|||
${props => props.inline && `
|
||||
margin: 0;
|
||||
`}
|
||||
${props => props.small && `
|
||||
margin: 10px 0;
|
||||
`}
|
||||
`;
|
||||
|
||||
export const StyledLabel = styled.label`
|
||||
|
|
@ -16,6 +19,9 @@ export const StyledLabel = styled.label`
|
|||
${props => props.inline && `
|
||||
font-size: 16px;
|
||||
`}
|
||||
${props => props.small && `
|
||||
font-size: .9rem;
|
||||
`}
|
||||
`;
|
||||
|
||||
export const StyledSubLabel = styled.label`
|
||||
|
|
@ -42,4 +48,8 @@ export const StyledSelect = styled.select`
|
|||
border: 1px solid ${props => props.theme.primary};
|
||||
box-shadow: inset 0 -3px 0 0 ${props => props.theme.primary};
|
||||
}
|
||||
|
||||
${props => props.small && `
|
||||
padding: 6px 8px;
|
||||
`}
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
import { useState } from 'react';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { ToggleField } from 'components';
|
||||
import { ToggleField, SelectField } from 'components';
|
||||
|
||||
import { useSettingsStore } from 'stores';
|
||||
|
||||
|
|
@ -16,6 +17,7 @@ const Settings = () => {
|
|||
const theme = useTheme();
|
||||
const store = useSettingsStore();
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const { t, i18n } = useTranslation('common');
|
||||
|
||||
return (
|
||||
<>
|
||||
|
|
@ -23,51 +25,77 @@ const Settings = () => {
|
|||
isOpen={isOpen}
|
||||
tabIndex="1"
|
||||
type="button"
|
||||
onClick={() => setIsOpen(!isOpen)} title="Options"
|
||||
onClick={() => setIsOpen(!isOpen)} title={t('options.name')}
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke={theme.text} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><circle cx="12" cy="12" r="3"></circle><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path></svg>
|
||||
</OpenButton>
|
||||
|
||||
<Cover isOpen={isOpen} onClick={() => setIsOpen(false)} />
|
||||
<Modal isOpen={isOpen}>
|
||||
<Heading>Options</Heading>
|
||||
<Heading>{t('options.name')}</Heading>
|
||||
|
||||
<ToggleField
|
||||
label="Week starts on"
|
||||
label={t('options.weekStart.label')}
|
||||
name="weekStart"
|
||||
id="weekStart"
|
||||
options={['Sunday', 'Monday']}
|
||||
value={store.weekStart === 1 ? 'Monday' : 'Sunday'}
|
||||
onChange={value => store.setWeekStart(value === 'Monday' ? 1 : 0)}
|
||||
options={{
|
||||
'Sunday': t('options.weekStart.options.Sunday'),
|
||||
'Monday': t('options.weekStart.options.Monday'),
|
||||
}}
|
||||
value={store.weekStart === 0 ? 'Sunday' : 'Monday'}
|
||||
onChange={value => store.setWeekStart(value === 'Sunday' ? 0 : 1)}
|
||||
/>
|
||||
|
||||
<ToggleField
|
||||
label="Time format"
|
||||
label={t('options.timeFormat.label')}
|
||||
name="timeFormat"
|
||||
id="timeFormat"
|
||||
options={['12h', '24h']}
|
||||
options={{
|
||||
'12h': t('options.timeFormat.options.12h'),
|
||||
'24h': t('options.timeFormat.options.24h'),
|
||||
}}
|
||||
value={store.timeFormat}
|
||||
onChange={value => store.setTimeFormat(value)}
|
||||
/>
|
||||
|
||||
<ToggleField
|
||||
label="Theme"
|
||||
label={t('options.theme.label')}
|
||||
name="theme"
|
||||
id="theme"
|
||||
options={['System', 'Light', 'Dark']}
|
||||
options={{
|
||||
'System': t('options.theme.options.System'),
|
||||
'Light': t('options.theme.options.Light'),
|
||||
'Dark': t('options.theme.options.Dark'),
|
||||
}}
|
||||
value={store.theme}
|
||||
onChange={value => store.setTheme(value)}
|
||||
/>
|
||||
|
||||
<ToggleField
|
||||
label="Highlight highest availability"
|
||||
label={t('options.highlight.label')}
|
||||
name="highlight"
|
||||
id="highlight"
|
||||
title="Make the highest availability on the heatmap stand out"
|
||||
options={['Off', 'On']}
|
||||
title={t('options.highlight.title')}
|
||||
options={{
|
||||
'Off': t('options.highlight.options.Off'),
|
||||
'On': t('options.highlight.options.On'),
|
||||
}}
|
||||
value={store.highlight ? 'On' : 'Off'}
|
||||
onChange={value => store.setHighlight(value === 'On')}
|
||||
/>
|
||||
|
||||
<SelectField
|
||||
label={t('options.language.label')}
|
||||
name="language"
|
||||
id="language"
|
||||
options={i18n.language === 'cimode' ? {
|
||||
cimode: 'DEV',
|
||||
english: 'en-US'
|
||||
} : t('options.language.options', { returnObjects: true })}
|
||||
small
|
||||
value={i18n.language}
|
||||
onChange={event => i18n.changeLanguage(event.target.value)}
|
||||
/>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -21,17 +21,17 @@ const ToggleField = ({
|
|||
{label && <StyledLabel title={title}>{label}</StyledLabel>}
|
||||
|
||||
<ToggleContainer>
|
||||
{options.map(option =>
|
||||
<Option key={option}>
|
||||
{Object.entries(options).map(([key, label]) =>
|
||||
<Option key={label}>
|
||||
<HiddenInput
|
||||
type="radio"
|
||||
name={name}
|
||||
value={option}
|
||||
id={`${name}-${option}`}
|
||||
checked={value === option}
|
||||
onChange={() => onChange(option)}
|
||||
value={label}
|
||||
id={`${name}-${label}`}
|
||||
checked={value === key}
|
||||
onChange={() => onChange(key)}
|
||||
/>
|
||||
<LabelButton htmlFor={`${name}-${option}`}>{option}</LabelButton>
|
||||
<LabelButton htmlFor={`${name}-${label}`}>{label}</LabelButton>
|
||||
</Option>
|
||||
)}
|
||||
</ToggleContainer>
|
||||
|
|
|
|||
|
|
@ -17,3 +17,4 @@ export { default as Donate } from './Donate/Donate';
|
|||
export { default as Settings } from './Settings/Settings';
|
||||
export { default as Egg } from './Egg/Egg';
|
||||
export { default as Footer } from './Footer/Footer';
|
||||
export { default as Recents } from './Recents/Recents';
|
||||
|
|
|
|||
21
crabfit-frontend/src/i18n/index.ts
Normal file
21
crabfit-frontend/src/i18n/index.ts
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
import i18n from 'i18next';
|
||||
import { initReactI18next } from 'react-i18next';
|
||||
import LanguageDetector from 'i18next-browser-languagedetector';
|
||||
import Backend from 'i18next-http-backend';
|
||||
|
||||
i18n
|
||||
.use(LanguageDetector)
|
||||
.use(Backend)
|
||||
.use(initReactI18next)
|
||||
.init({
|
||||
fallbackLng: 'en-US',
|
||||
debug: process.env.NODE_ENV !== 'production',
|
||||
interpolation: {
|
||||
escapeValue: false,
|
||||
},
|
||||
backend: {
|
||||
loadPath: '/i18n/{{lng}}/{{ns}}.json',
|
||||
},
|
||||
});
|
||||
|
||||
export default i18n;
|
||||
|
|
@ -3,6 +3,7 @@ import ReactDOM from 'react-dom';
|
|||
import App from './App';
|
||||
import * as serviceWorkerRegistration from './serviceWorkerRegistration';
|
||||
import reportWebVitals from './reportWebVitals';
|
||||
import 'i18n';
|
||||
|
||||
ReactDOM.render(
|
||||
<React.StrictMode>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { useEffect, useState } from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useTranslation, Trans } from 'react-i18next';
|
||||
|
||||
import dayjs from 'dayjs';
|
||||
import utc from 'dayjs/plugin/utc';
|
||||
|
|
@ -13,8 +14,9 @@ import {
|
|||
TimeRangeField,
|
||||
SelectField,
|
||||
Button,
|
||||
Donate,
|
||||
Error,
|
||||
Recents,
|
||||
Footer,
|
||||
} from 'components';
|
||||
|
||||
import {
|
||||
|
|
@ -25,12 +27,7 @@ import {
|
|||
P,
|
||||
OfflineMessage,
|
||||
ShareInfo,
|
||||
Footer,
|
||||
AboutSection,
|
||||
} from './createStyle';
|
||||
import {
|
||||
Recent,
|
||||
} from '../Home/homeStyle';
|
||||
|
||||
import api from 'services';
|
||||
import { useRecentsStore } from 'stores';
|
||||
|
|
@ -53,8 +50,9 @@ const Create = ({ offline }) => {
|
|||
const [copied, setCopied] = useState(null);
|
||||
|
||||
const { push } = useHistory();
|
||||
const { t } = useTranslation(['common', 'home', 'event']);
|
||||
|
||||
const recentsStore = useRecentsStore();
|
||||
const addRecent = useRecentsStore(state => state.addRecent);
|
||||
|
||||
useEffect(() => {
|
||||
if (window.self === window.top) {
|
||||
|
|
@ -71,11 +69,11 @@ const Create = ({ offline }) => {
|
|||
const dates = JSON.parse(data.dates);
|
||||
|
||||
if (dates.length === 0) {
|
||||
return setError(`You haven't selected any dates!`);
|
||||
return setError(t('home:form.errors.no_dates'));
|
||||
}
|
||||
const isSpecificDates = typeof dates[0] === 'string' && dates[0].length === 8;
|
||||
if (start === end) {
|
||||
return setError(`The start and end times can't be the same`);
|
||||
return setError(t('home:form.errors.same_times'));
|
||||
}
|
||||
|
||||
let times = dates.reduce((times, date) => {
|
||||
|
|
@ -112,7 +110,7 @@ const Create = ({ offline }) => {
|
|||
}, []);
|
||||
|
||||
if (times.length === 0) {
|
||||
return setError(`You don't have any time selected`);
|
||||
return setError(t('home:form.errors.no_time'));
|
||||
}
|
||||
|
||||
const response = await api.post('/event', {
|
||||
|
|
@ -123,16 +121,16 @@ const Create = ({ offline }) => {
|
|||
},
|
||||
});
|
||||
setCreatedEvent(response.data);
|
||||
recentsStore.addRecent({
|
||||
addRecent({
|
||||
id: response.data.id,
|
||||
created: response.data.created,
|
||||
name: response.data.name,
|
||||
});
|
||||
gtag('event', 'create_event', {
|
||||
'event_category': 'home',
|
||||
'event_category': 'create',
|
||||
});
|
||||
} catch (e) {
|
||||
setError('An error ocurred while creating the event. Please try again later.');
|
||||
setError(t('home:form.errors.unknown'));
|
||||
console.error(e);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
|
|
@ -142,18 +140,18 @@ const Create = ({ offline }) => {
|
|||
return (
|
||||
<>
|
||||
<StyledMain>
|
||||
<TitleSmall>CREATE A</TitleSmall>
|
||||
<TitleSmall>{t('home:create')}</TitleSmall>
|
||||
<TitleLarge>CRAB FIT</TitleLarge>
|
||||
</StyledMain>
|
||||
|
||||
{createdEvent ? (
|
||||
<StyledMain>
|
||||
<OfflineMessage>
|
||||
<h2>Created {createdEvent.name}</h2>
|
||||
<h2>{t('common:created', { date: createdEvent?.name })}</h2>
|
||||
<ShareInfo
|
||||
onClick={() => navigator.clipboard?.writeText(`https://crab.fit/${createdEvent.id}`)
|
||||
.then(() => {
|
||||
setCopied('Copied!');
|
||||
setCopied(t('event:nav.copied'));
|
||||
setTimeout(() => setCopied(null), 1000);
|
||||
gtag('event', 'copy_link', {
|
||||
'event_category': 'event',
|
||||
|
|
@ -161,45 +159,30 @@ const Create = ({ offline }) => {
|
|||
})
|
||||
.catch((e) => console.error('Failed to copy', e))
|
||||
}
|
||||
title={!!navigator.clipboard ? 'Click to copy' : ''}
|
||||
>{copied ?? `https://crab.fit/${createdEvent.id}`}</ShareInfo>
|
||||
title={!!navigator.clipboard ? t('event:nav.title') : ''}
|
||||
>{copied ?? `https://crab.fit/${createdEvent?.id}`}</ShareInfo>
|
||||
<ShareInfo>
|
||||
{/* eslint-disable-next-line */}
|
||||
Click the link above to copy it to your clipboard, or share via <a onClick={() => gtag('event', 'send_email', { 'event_category': 'event' })} href={`mailto:?subject=${encodeURIComponent(`Scheduling ${createdEvent.name}`)}&body=${encodeURIComponent(`Visit this link to enter your availabilities: https://crab.fit/${createdEvent.id}`)}`} target="_blank">email</a>.
|
||||
<Trans i18nKey="event:nav.shareinfo_alt">Click the link above to copy it to your clipboard, or share via <a onClick={() => gtag('event', 'send_email', { 'event_category': 'event' })} href={`mailto:?subject=${encodeURIComponent(t('event:nav.email_subject', { event_name: createdEvent?.name }))}&body=${encodeURIComponent(`${t('event:nav.email_body')} https://crab.fit/${createdEvent?.id}`)}`} target="_blank">email</a>.</Trans>
|
||||
</ShareInfo>
|
||||
<Footer>
|
||||
<span>Thank you for using Crab Fit. If you like it, consider donating.</span>
|
||||
<Donate />
|
||||
</Footer>
|
||||
<Footer small />
|
||||
</OfflineMessage>
|
||||
</StyledMain>
|
||||
) : (
|
||||
<>
|
||||
{!!recentsStore.recents.length && (
|
||||
<AboutSection id="recents">
|
||||
<StyledMain>
|
||||
<h2>Recently visited</h2>
|
||||
{recentsStore.recents.map(event => (
|
||||
<Recent href={`/${event.id}`} target="_blank" key={event.id}>
|
||||
<span className="name">{event.name}</span>
|
||||
<span className="date">Created {dayjs.unix(event.created).format('D MMMM, YYYY')}</span>
|
||||
</Recent>
|
||||
))}
|
||||
</StyledMain>
|
||||
</AboutSection>
|
||||
)}
|
||||
<Recents />
|
||||
|
||||
<StyledMain>
|
||||
{offline ? (
|
||||
<OfflineMessage>
|
||||
<h1>🦀📵</h1>
|
||||
<P>You can't create a Crab Fit when you don't have an internet connection. Please make sure you're connected.</P>
|
||||
<P>{t('home:offline')}</P>
|
||||
</OfflineMessage>
|
||||
) : (
|
||||
<CreateForm onSubmit={handleSubmit(onSubmit)} id="create">
|
||||
<TextField
|
||||
label="Give your event a name!"
|
||||
subLabel="Or leave blank to generate one"
|
||||
label={t('home:form.name.label')}
|
||||
subLabel={t('home:form.name.sublabel')}
|
||||
type="text"
|
||||
name="name"
|
||||
id="name"
|
||||
|
|
@ -207,8 +190,8 @@ const Create = ({ offline }) => {
|
|||
/>
|
||||
|
||||
<CalendarField
|
||||
label="What dates might work?"
|
||||
subLabel="Click and drag to select"
|
||||
label={t('home:form.dates.label')}
|
||||
subLabel={t('home:form.dates.sublabel')}
|
||||
name="dates"
|
||||
id="dates"
|
||||
required
|
||||
|
|
@ -216,8 +199,8 @@ const Create = ({ offline }) => {
|
|||
/>
|
||||
|
||||
<TimeRangeField
|
||||
label="What times might work?"
|
||||
subLabel="Click and drag to select a time range"
|
||||
label={t('home:form.times.label')}
|
||||
subLabel={t('home:form.times.sublabel')}
|
||||
name="times"
|
||||
id="times"
|
||||
required
|
||||
|
|
@ -225,20 +208,20 @@ const Create = ({ offline }) => {
|
|||
/>
|
||||
|
||||
<SelectField
|
||||
label="And the timezone"
|
||||
label={t('home:form.timezone.label')}
|
||||
name="timezone"
|
||||
id="timezone"
|
||||
register={register}
|
||||
options={timezones}
|
||||
required
|
||||
defaultOption="Select..."
|
||||
defaultOption={t('home:form.timezone.defaultOption')}
|
||||
/>
|
||||
|
||||
{error && (
|
||||
<Error onClose={() => setError(null)}>{error}</Error>
|
||||
)}
|
||||
|
||||
<Button type="submit" isLoading={isLoading} disabled={isLoading} buttonWidth="100%">Create</Button>
|
||||
<Button type="submit" isLoading={isLoading} disabled={isLoading} buttonWidth="100%">{t('home:form.button')}</Button>
|
||||
</CreateForm>
|
||||
)}
|
||||
</StyledMain>
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ export const TitleSmall = styled.span`
|
|||
font-weight: 400;
|
||||
color: ${props => props.theme.primaryDark};
|
||||
line-height: 1em;
|
||||
text-transform: uppercase;
|
||||
`;
|
||||
|
||||
export const TitleLarge = styled.h1`
|
||||
|
|
@ -30,6 +31,7 @@ export const TitleLarge = styled.h1`
|
|||
font-weight: 400;
|
||||
text-shadow: 0 3px 0 ${props => props.theme.primaryDark};
|
||||
line-height: 1em;
|
||||
text-transform: uppercase;
|
||||
`;
|
||||
|
||||
export const P = styled.p`
|
||||
|
|
@ -37,16 +39,6 @@ export const P = styled.p`
|
|||
line-height: 1.6em;
|
||||
`;
|
||||
|
||||
export const Footer = styled.footer`
|
||||
margin: 60px auto 0;
|
||||
width: 250px;
|
||||
|
||||
& span {
|
||||
display: block;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
`;
|
||||
|
||||
export const OfflineMessage = styled.div`
|
||||
text-align: center;
|
||||
margin: 50px 0 20px;
|
||||
|
|
@ -66,14 +58,3 @@ export const ShareInfo = styled.p`
|
|||
}
|
||||
`}
|
||||
`;
|
||||
|
||||
export const AboutSection = styled.section`
|
||||
margin: 30px 0 0;
|
||||
background-color: ${props => props.theme.primaryBackground};
|
||||
padding: 10px 0;
|
||||
|
||||
& h2 {
|
||||
margin: 0 0 10px;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { Link } from 'react-router-dom';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useTranslation, Trans } from 'react-i18next';
|
||||
|
||||
import dayjs from 'dayjs';
|
||||
import utc from 'dayjs/plugin/utc';
|
||||
|
|
@ -50,6 +51,8 @@ const Event = (props) => {
|
|||
|
||||
const addRecent = useRecentsStore(state => state.addRecent);
|
||||
|
||||
const { t } = useTranslation(['common', 'event']);
|
||||
|
||||
const { register, handleSubmit } = useForm();
|
||||
const { id } = props.match.params;
|
||||
const { offline } = props;
|
||||
|
|
@ -244,7 +247,7 @@ const Event = (props) => {
|
|||
setTab('you');
|
||||
} catch (e) {
|
||||
if (e.status === 401) {
|
||||
setError('Password is incorrect. Check your name is spelled right.');
|
||||
setError(t('event:form.errors.password_incorrect'));
|
||||
} else if (e.status === 404) {
|
||||
// Create user
|
||||
try {
|
||||
|
|
@ -261,7 +264,7 @@ const Event = (props) => {
|
|||
});
|
||||
setTab('you');
|
||||
} catch (e) {
|
||||
setError('Failed to create user. Please try again.');
|
||||
setError(t('event:form.errors.unknown'));
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
|
|
@ -280,17 +283,17 @@ const Event = (props) => {
|
|||
<Logo src={logo} alt="" />
|
||||
<Title>CRAB FIT</Title>
|
||||
</Center>
|
||||
<Center style={{ textDecoration: 'underline', fontSize: 14, paddingTop: 6 }}>Create your own</Center>
|
||||
<Center style={{ textDecoration: 'underline', fontSize: 14, paddingTop: 6 }}>{t('common:tagline')}</Center>
|
||||
</Link>
|
||||
|
||||
{(!!event || isLoading) ? (
|
||||
<>
|
||||
<EventName isLoading={isLoading}>{event?.name}</EventName>
|
||||
<EventDate isLoading={isLoading} title={event?.created && dayjs.unix(event?.created).format('D MMMM, YYYY')}>{event?.created && `Created ${dayjs.unix(event?.created).fromNow()}`}</EventDate>
|
||||
<EventDate isLoading={isLoading} title={event?.created && dayjs.unix(event?.created).format('D MMMM, YYYY')}>{event?.created && t('common:created', { date: dayjs.unix(event?.created).fromNow() })}</EventDate>
|
||||
<ShareInfo
|
||||
onClick={() => navigator.clipboard?.writeText(`https://crab.fit/${id}`)
|
||||
.then(() => {
|
||||
setCopied('Copied!');
|
||||
setCopied(t('event:nav.copied'));
|
||||
setTimeout(() => setCopied(null), 1000);
|
||||
gtag('event', 'copy_link', {
|
||||
'event_category': 'event',
|
||||
|
|
@ -298,24 +301,24 @@ const Event = (props) => {
|
|||
})
|
||||
.catch((e) => console.error('Failed to copy', e))
|
||||
}
|
||||
title={!!navigator.clipboard ? 'Click to copy' : ''}
|
||||
title={!!navigator.clipboard ? t('event:nav.title') : ''}
|
||||
>{copied ?? `https://crab.fit/${id}`}</ShareInfo>
|
||||
<ShareInfo isLoading={isLoading}>
|
||||
{!!event?.name &&
|
||||
<>Copy the link to this page, or share via <a onClick={() => gtag('event', 'send_email', { 'event_category': 'event' })} href={`mailto:?subject=${encodeURIComponent(`Scheduling ${event?.name}`)}&body=${encodeURIComponent(`Visit this link to enter your availabilities: https://crab.fit/${id}`)}`}>email</a>.</>
|
||||
<Trans i18nKey="event:nav.shareinfo">Copy the link to this page, or share via <a onClick={() => gtag('event', 'send_email', { 'event_category': 'event' })} href={`mailto:?subject=${encodeURIComponent(t('event:nav.email_subject', { event_name: event?.name }))}&body=${encodeURIComponent(`${t('event:nav.email_body')} https://crab.fit/${id}`)}`}>email</a>.</Trans>
|
||||
}
|
||||
</ShareInfo>
|
||||
</>
|
||||
) : (
|
||||
offline ? (
|
||||
<div style={{ margin: '100px 0' }}>
|
||||
<EventName>You are offline</EventName>
|
||||
<ShareInfo>A Crab Fit doesn't work offline.<br />Make sure you're connected to the internet and try again.</ShareInfo>
|
||||
<EventName>{t('event:offline.title')}</EventName>
|
||||
<ShareInfo><Trans i18nKey="event:offline.body" /></ShareInfo>
|
||||
</div>
|
||||
) : (
|
||||
<div style={{ margin: '100px 0' }}>
|
||||
<EventName>Event not found</EventName>
|
||||
<ShareInfo>Check that the url you entered is correct.</ShareInfo>
|
||||
<EventName>{t('event:error.title')}</EventName>
|
||||
<ShareInfo>{t('event:error.body')}</ShareInfo>
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
|
|
@ -326,13 +329,13 @@ const Event = (props) => {
|
|||
<LoginSection id="login">
|
||||
<StyledMain>
|
||||
{user ? (
|
||||
<h2>Signed in as {user.name}</h2>
|
||||
<h2>{t('event:form.signed_in', { name: user.name })}</h2>
|
||||
) : (
|
||||
<>
|
||||
<h2>Sign in to add your availability</h2>
|
||||
<h2>{t('event:form.signed_out')}</h2>
|
||||
<LoginForm onSubmit={handleSubmit(onSubmit)}>
|
||||
<TextField
|
||||
label="Your name"
|
||||
label={t('event:form.name')}
|
||||
type="text"
|
||||
name="name"
|
||||
id="name"
|
||||
|
|
@ -342,7 +345,7 @@ const Event = (props) => {
|
|||
/>
|
||||
|
||||
<TextField
|
||||
label="Password (optional)"
|
||||
label={t('event:form.password')}
|
||||
type="password"
|
||||
name="password"
|
||||
id="password"
|
||||
|
|
@ -354,15 +357,15 @@ const Event = (props) => {
|
|||
type="submit"
|
||||
isLoading={isLoginLoading}
|
||||
disabled={isLoginLoading || isLoading}
|
||||
>Login</Button>
|
||||
>{t('event:form.button')}</Button>
|
||||
</LoginForm>
|
||||
{error && <Error onClose={() => setError(null)}>{error}</Error>}
|
||||
<Info>These details are only for this event. Use a password to prevent others from changing your availability.</Info>
|
||||
<Info>{t('event:form.info')}</Info>
|
||||
</>
|
||||
)}
|
||||
|
||||
<SelectField
|
||||
label="Your time zone"
|
||||
label={t('event:form.timezone')}
|
||||
name="timezone"
|
||||
id="timezone"
|
||||
inline
|
||||
|
|
@ -371,10 +374,10 @@ const Event = (props) => {
|
|||
options={timezones}
|
||||
/>
|
||||
{/* eslint-disable-next-line */}
|
||||
{event?.timezone && event.timezone !== timezone && <p>This event was created in the timezone <strong>{event.timezone}</strong>. <a href="#" onClick={e => {
|
||||
{event?.timezone && event.timezone !== timezone && <p><Trans i18nKey="event:form.created_in_timezone">This event was created in the timezone <strong>{{timezone: event.timezone}}</strong>. <a href="#" onClick={e => {
|
||||
e.preventDefault();
|
||||
setTimezone(event.timezone);
|
||||
}}>Click here</a> to use it.</p>}
|
||||
}}>Click here</a> to use it.</Trans></p>}
|
||||
{((
|
||||
Intl.DateTimeFormat().resolvedOptions().timeZone !== timezone
|
||||
&& (event?.timezone && event.timezone !== Intl.DateTimeFormat().resolvedOptions().timeZone)
|
||||
|
|
@ -383,10 +386,10 @@ const Event = (props) => {
|
|||
&& Intl.DateTimeFormat().resolvedOptions().timeZone !== timezone
|
||||
)) && (
|
||||
/* eslint-disable-next-line */
|
||||
<p>Your local timezone is detected to be <strong>{Intl.DateTimeFormat().resolvedOptions().timeZone}</strong>. <a href="#" onClick={e => {
|
||||
<p><Trans i18nKey="event:form.local_timezone">Your local timezone is detected to be <strong>{{timezone: Intl.DateTimeFormat().resolvedOptions().timeZone}}</strong>. <a href="#" onClick={e => {
|
||||
e.preventDefault();
|
||||
setTimezone(Intl.DateTimeFormat().resolvedOptions().timeZone);
|
||||
}}>Click here</a> to use it.</p>
|
||||
}}>Click here</a> to use it.</Trans></p>
|
||||
)}
|
||||
</StyledMain>
|
||||
</LoginSection>
|
||||
|
|
@ -403,8 +406,8 @@ const Event = (props) => {
|
|||
}}
|
||||
selected={tab === 'you'}
|
||||
disabled={!user}
|
||||
title={user ? '' : 'Login to set your availability'}
|
||||
>Your availability</Tab>
|
||||
title={user ? '' : t('event:tabs.you_tooltip')}
|
||||
>{t('event:tabs.you')}</Tab>
|
||||
<Tab
|
||||
href="#group"
|
||||
onClick={e => {
|
||||
|
|
@ -412,7 +415,7 @@ const Event = (props) => {
|
|||
setTab('group');
|
||||
}}
|
||||
selected={tab === 'group'}
|
||||
>Group availability</Tab>
|
||||
>{t('event:tabs.group')}</Tab>
|
||||
</Tabs>
|
||||
</StyledMain>
|
||||
|
||||
|
|
@ -430,9 +433,6 @@ const Event = (props) => {
|
|||
</section>
|
||||
) : (
|
||||
<section id="you">
|
||||
<StyledMain>
|
||||
<Center>Click and drag the calendar below to set your availabilities</Center>
|
||||
</StyledMain>
|
||||
<AvailabilityEditor
|
||||
times={times}
|
||||
timeLabels={timeLabels}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { useEffect } from 'react';
|
||||
import { Link, useHistory } from 'react-router-dom';
|
||||
import { useTranslation, Trans } from 'react-i18next';
|
||||
|
||||
import {
|
||||
Button,
|
||||
|
|
@ -23,10 +24,11 @@ import logo from 'res/logo.svg';
|
|||
|
||||
const Help = () => {
|
||||
const { push } = useHistory();
|
||||
const { t } = useTranslation(['common', 'help']);
|
||||
|
||||
useEffect(() => {
|
||||
document.title = 'How to Crab Fit';
|
||||
}, []);
|
||||
document.title = t('help:name');
|
||||
}, [t]);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
|
@ -36,31 +38,31 @@ const Help = () => {
|
|||
<Logo src={logo} alt="" />
|
||||
<Title>CRAB FIT</Title>
|
||||
</Center>
|
||||
<Center style={{ textDecoration: 'underline', fontSize: 14, paddingTop: 6 }}>Create your own</Center>
|
||||
<Center style={{ textDecoration: 'underline', fontSize: 14, paddingTop: 6 }}>{t('common:tagline')}</Center>
|
||||
</Link>
|
||||
</StyledMain>
|
||||
|
||||
<StyledMain>
|
||||
<h1>How to Crab Fit</h1>
|
||||
<P>Crab Fit is a tool that helps you when planning events with friends or coworkers. You just create an event, enter your availability, send it out, and see when everyone is free!</P>
|
||||
<P>See below for detailed steps of how to Crab Fit your event.</P>
|
||||
<h1>{t('help:name')}</h1>
|
||||
<P>{t('help:p1')}</P>
|
||||
<P>{t('help:p2')}</P>
|
||||
|
||||
<Step>Step 1</Step>
|
||||
<P>Use the form at <Link to="/">crab.fit</Link> to make a new event. You only need to put in the rough time period for when your event occurs here, not your availability.</P>
|
||||
<P>For example, we'll use "Jenny's Birthday Lunch". Jenny wants her birthday lunch to happen on the same week as her birthday, the 15th of April, but she knows that not all of her friends are available on the 15th. She also doesn't want to do it on the weekend.</P>
|
||||
<Step>{t('help:s1')}</Step>
|
||||
<P><Trans i18nKey="help:p3">Use the form at <Link to="/">crab.fit</Link> to make a new event. You only need to put in the rough time period for when your event occurs here, not your availability.</Trans></P>
|
||||
<P>{t('help:p4')}</P>
|
||||
<FakeCalendar>
|
||||
<div className="days"><span>Sun</span><span>Mon</span><span>Tue</span><span>Wed</span><span>Thu</span><span>Fri</span><span>Sat</span></div>
|
||||
<div className="dates"><span>11</span><span className="selected">12</span><span className="selected">13</span><span className="selected">14</span><span className="selected">15</span><span className="selected">16</span><span>17</span></div>
|
||||
</FakeCalendar>
|
||||
<P>Jenny also knows that since it's a lunch event, it can't start before 11am or go any later than 5pm.</P>
|
||||
<P>{t('help:p5')}</P>
|
||||
<FakeTimeRange>
|
||||
<div className="start" data-label="11am"></div>
|
||||
<div className="end" data-label="5pm"></div>
|
||||
</FakeTimeRange>
|
||||
|
||||
<Step>Step 2</Step>
|
||||
<P>Enter your availability for the event you just created.</P>
|
||||
<P>In our example, Jenny now puts in her availability for her birthday lunch. She is free all week, except after 3pm on Tuesday and Wednesday, and before 1pm on Friday.</P>
|
||||
<Step>{t('help:s2')}</Step>
|
||||
<P>{t('help:p6')}</P>
|
||||
<P>{t('help:p7')}</P>
|
||||
<AvailabilityViewer
|
||||
times={["1100-12042021","1115-12042021","1130-12042021","1145-12042021","1200-12042021","1215-12042021","1230-12042021","1245-12042021","1300-12042021","1315-12042021","1330-12042021","1345-12042021","1400-12042021","1415-12042021","1430-12042021","1445-12042021","1500-12042021","1515-12042021","1530-12042021","1545-12042021","1600-12042021","1615-12042021","1630-12042021","1645-12042021","1100-13042021","1115-13042021","1130-13042021","1145-13042021","1200-13042021","1215-13042021","1230-13042021","1245-13042021","1300-13042021","1315-13042021","1330-13042021","1345-13042021","1400-13042021","1415-13042021","1430-13042021","1445-13042021","1500-13042021","1515-13042021","1530-13042021","1545-13042021","1600-13042021","1615-13042021","1630-13042021","1645-13042021","1100-14042021","1115-14042021","1130-14042021","1145-14042021","1200-14042021","1215-14042021","1230-14042021","1245-14042021","1300-14042021","1315-14042021","1330-14042021","1345-14042021","1400-14042021","1415-14042021","1430-14042021","1445-14042021","1500-14042021","1515-14042021","1530-14042021","1545-14042021","1600-14042021","1615-14042021","1630-14042021","1645-14042021","1100-15042021","1115-15042021","1130-15042021","1145-15042021","1200-15042021","1215-15042021","1230-15042021","1245-15042021","1300-15042021","1315-15042021","1330-15042021","1345-15042021","1400-15042021","1415-15042021","1430-15042021","1445-15042021","1500-15042021","1515-15042021","1530-15042021","1545-15042021","1600-15042021","1615-15042021","1630-15042021","1645-15042021","1100-16042021","1115-16042021","1130-16042021","1145-16042021","1200-16042021","1215-16042021","1230-16042021","1245-16042021","1300-16042021","1315-16042021","1330-16042021","1345-16042021","1400-16042021","1415-16042021","1430-16042021","1445-16042021","1500-16042021","1515-16042021","1530-16042021","1545-16042021","1600-16042021","1615-16042021","1630-16042021","1645-16042021"]}
|
||||
timeLabels={[{"label":"11 AM","time":"1100"},{"label":"","time":"1115"},{"label":"","time":"1130"},{"label":"","time":"1145"},{"label":"12 PM","time":"1200"},{"label":"","time":"1215"},{"label":"","time":"1230"},{"label":"","time":"1245"},{"label":"1 PM","time":"1300"},{"label":"","time":"1315"},{"label":"","time":"1330"},{"label":"","time":"1345"},{"label":"2 PM","time":"1400"},{"label":"","time":"1415"},{"label":"","time":"1430"},{"label":"","time":"1445"},{"label":"3 PM","time":"1500"},{"label":"","time":"1515"},{"label":"","time":"1530"},{"label":"","time":"1545"},{"label":"4 PM","time":"1600"},{"label":"","time":"1615"},{"label":"","time":"1630"},{"label":"","time":"1645"},{"label":"5 PM","time":null}]}
|
||||
|
|
@ -71,10 +73,10 @@ const Help = () => {
|
|||
max={1}
|
||||
/>
|
||||
|
||||
<Step>Step 3</Step>
|
||||
<P>Send the link to everyone you want to come.</P>
|
||||
<P>After Jenny has sent the link to her friends and waited for them to also fill out their availabilities, she can now easily see them all on the heatmap below and choose the darkest area for a time that suits everyone!</P>
|
||||
<P>In this example, 1pm to 3pm on Friday the 16th works for all Jenny's friends.</P>
|
||||
<Step>{t('help:s3')}</Step>
|
||||
<P>{t('help:p8')}</P>
|
||||
<P>{t('help:p9')}</P>
|
||||
<P>{t('help:p10')}</P>
|
||||
<AvailabilityViewer
|
||||
times={["1100-12042021","1115-12042021","1130-12042021","1145-12042021","1200-12042021","1215-12042021","1230-12042021","1245-12042021","1300-12042021","1315-12042021","1330-12042021","1345-12042021","1400-12042021","1415-12042021","1430-12042021","1445-12042021","1500-12042021","1515-12042021","1530-12042021","1545-12042021","1600-12042021","1615-12042021","1630-12042021","1645-12042021","1100-13042021","1115-13042021","1130-13042021","1145-13042021","1200-13042021","1215-13042021","1230-13042021","1245-13042021","1300-13042021","1315-13042021","1330-13042021","1345-13042021","1400-13042021","1415-13042021","1430-13042021","1445-13042021","1500-13042021","1515-13042021","1530-13042021","1545-13042021","1600-13042021","1615-13042021","1630-13042021","1645-13042021","1100-14042021","1115-14042021","1130-14042021","1145-14042021","1200-14042021","1215-14042021","1230-14042021","1245-14042021","1300-14042021","1315-14042021","1330-14042021","1345-14042021","1400-14042021","1415-14042021","1430-14042021","1445-14042021","1500-14042021","1515-14042021","1530-14042021","1545-14042021","1600-14042021","1615-14042021","1630-14042021","1645-14042021","1100-15042021","1115-15042021","1130-15042021","1145-15042021","1200-15042021","1215-15042021","1230-15042021","1245-15042021","1300-15042021","1315-15042021","1330-15042021","1345-15042021","1400-15042021","1415-15042021","1430-15042021","1445-15042021","1500-15042021","1515-15042021","1530-15042021","1545-15042021","1600-15042021","1615-15042021","1630-15042021","1645-15042021","1100-16042021","1115-16042021","1130-16042021","1145-16042021","1200-16042021","1215-16042021","1230-16042021","1245-16042021","1300-16042021","1315-16042021","1330-16042021","1345-16042021","1400-16042021","1415-16042021","1430-16042021","1445-16042021","1500-16042021","1515-16042021","1530-16042021","1545-16042021","1600-16042021","1615-16042021","1630-16042021","1645-16042021"]}
|
||||
timeLabels={[{"label":"11 AM","time":"1100"},{"label":"","time":"1115"},{"label":"","time":"1130"},{"label":"","time":"1145"},{"label":"12 PM","time":"1200"},{"label":"","time":"1215"},{"label":"","time":"1230"},{"label":"","time":"1245"},{"label":"1 PM","time":"1300"},{"label":"","time":"1315"},{"label":"","time":"1330"},{"label":"","time":"1345"},{"label":"2 PM","time":"1400"},{"label":"","time":"1415"},{"label":"","time":"1430"},{"label":"","time":"1445"},{"label":"3 PM","time":"1500"},{"label":"","time":"1515"},{"label":"","time":"1530"},{"label":"","time":"1545"},{"label":"4 PM","time":"1600"},{"label":"","time":"1615"},{"label":"","time":"1630"},{"label":"","time":"1645"},{"label":"5 PM","time":null}]}
|
||||
|
|
@ -88,7 +90,7 @@ const Help = () => {
|
|||
|
||||
<AboutSection id="about">
|
||||
<StyledMain>
|
||||
<Center><Button buttonWidth="230px" onClick={() => push('/')}>Create your own Crab Fit!</Button></Center>
|
||||
<Center><Button buttonWidth="230px" onClick={() => push('/')}>{t('common:cta')}</Button></Center>
|
||||
</StyledMain>
|
||||
</AboutSection>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { useEffect, useState } from 'react';
|
||||
import { useHistory, Link } from 'react-router-dom';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useTranslation, Trans } from 'react-i18next';
|
||||
|
||||
import dayjs from 'dayjs';
|
||||
import utc from 'dayjs/plugin/utc';
|
||||
|
|
@ -16,6 +17,7 @@ import {
|
|||
Center,
|
||||
Error,
|
||||
Footer,
|
||||
Recents,
|
||||
} from 'components';
|
||||
|
||||
import {
|
||||
|
|
@ -32,11 +34,9 @@ import {
|
|||
StatNumber,
|
||||
StatLabel,
|
||||
OfflineMessage,
|
||||
Recent,
|
||||
} from './homeStyle';
|
||||
|
||||
import api from 'services';
|
||||
import { useRecentsStore } from 'stores';
|
||||
|
||||
import logo from 'res/logo.svg';
|
||||
import timezones from 'res/timezones.json';
|
||||
|
|
@ -59,7 +59,7 @@ const Home = ({ offline }) => {
|
|||
version: 'loading...',
|
||||
});
|
||||
const { push } = useHistory();
|
||||
const recentsStore = useRecentsStore();
|
||||
const { t } = useTranslation(['common', 'home']);
|
||||
|
||||
useEffect(() => {
|
||||
const fetch = async () => {
|
||||
|
|
@ -152,38 +152,26 @@ const Home = ({ offline }) => {
|
|||
<Center>
|
||||
<Logo src={logo} alt="" />
|
||||
</Center>
|
||||
<TitleSmall>CREATE A</TitleSmall>
|
||||
<TitleSmall>{t('home:create')}</TitleSmall>
|
||||
<TitleLarge>CRAB FIT</TitleLarge>
|
||||
<Links>
|
||||
<a href="#about">About</a> / <a href="#donate">Donate</a>
|
||||
<a href="#about">{t('home:nav.about')}</a> / <a href="#donate">{t('home:nav.donate')}</a>
|
||||
</Links>
|
||||
</StyledMain>
|
||||
|
||||
{!!recentsStore.recents.length && (
|
||||
<AboutSection id="recents">
|
||||
<StyledMain>
|
||||
<h2>Recently visited</h2>
|
||||
{recentsStore.recents.map(event => (
|
||||
<Recent href={`/${event.id}`} key={event.id}>
|
||||
<span className="name">{event.name}</span>
|
||||
<span className="date">Created {dayjs.unix(event.created).format('D MMMM, YYYY')}</span>
|
||||
</Recent>
|
||||
))}
|
||||
</StyledMain>
|
||||
</AboutSection>
|
||||
)}
|
||||
<Recents />
|
||||
|
||||
<StyledMain>
|
||||
{offline ? (
|
||||
<OfflineMessage>
|
||||
<h1>🦀📵</h1>
|
||||
<P>You can't create a Crab Fit when you don't have an internet connection. Please make sure you're connected.</P>
|
||||
<P>{t('home:offline')}</P>
|
||||
</OfflineMessage>
|
||||
) : (
|
||||
<CreateForm onSubmit={handleSubmit(onSubmit)} id="create">
|
||||
<TextField
|
||||
label="Give your event a name!"
|
||||
subLabel="Or leave blank to generate one"
|
||||
label={t('home:form.name.label')}
|
||||
subLabel={t('home:form.name.sublabel')}
|
||||
type="text"
|
||||
name="name"
|
||||
id="name"
|
||||
|
|
@ -191,8 +179,8 @@ const Home = ({ offline }) => {
|
|||
/>
|
||||
|
||||
<CalendarField
|
||||
label="What dates might work?"
|
||||
subLabel="Click and drag to select"
|
||||
label={t('home:form.dates.label')}
|
||||
subLabel={t('home:form.dates.sublabel')}
|
||||
name="dates"
|
||||
id="dates"
|
||||
required
|
||||
|
|
@ -200,8 +188,8 @@ const Home = ({ offline }) => {
|
|||
/>
|
||||
|
||||
<TimeRangeField
|
||||
label="What times might work?"
|
||||
subLabel="Click and drag to select a time range"
|
||||
label={t('home:form.times.label')}
|
||||
subLabel={t('home:form.times.sublabel')}
|
||||
name="times"
|
||||
id="times"
|
||||
required
|
||||
|
|
@ -209,13 +197,13 @@ const Home = ({ offline }) => {
|
|||
/>
|
||||
|
||||
<SelectField
|
||||
label="And the timezone"
|
||||
label={t('home:form.timezone.label')}
|
||||
name="timezone"
|
||||
id="timezone"
|
||||
register={register}
|
||||
options={timezones}
|
||||
required
|
||||
defaultOption="Select..."
|
||||
defaultOption={t('home:form.timezone.defaultOption')}
|
||||
/>
|
||||
|
||||
{error && (
|
||||
|
|
@ -223,7 +211,7 @@ const Home = ({ offline }) => {
|
|||
)}
|
||||
|
||||
<Center>
|
||||
<Button type="submit" isLoading={isLoading} disabled={isLoading}>Create</Button>
|
||||
<Button type="submit" isLoading={isLoading} disabled={isLoading}>{t('home:form.button')}</Button>
|
||||
</Center>
|
||||
</CreateForm>
|
||||
)}
|
||||
|
|
@ -231,24 +219,24 @@ const Home = ({ offline }) => {
|
|||
|
||||
<AboutSection id="about">
|
||||
<StyledMain>
|
||||
<h2>About Crab Fit</h2>
|
||||
<h2>{t('home:about.name')}</h2>
|
||||
<Stats>
|
||||
<Stat>
|
||||
<StatNumber>{stats.eventCount ?? '100+'}</StatNumber>
|
||||
<StatLabel>Events created</StatLabel>
|
||||
<StatNumber>{stats.eventCount ?? '300+'}</StatNumber>
|
||||
<StatLabel>{t('home:about.events')}</StatLabel>
|
||||
</Stat>
|
||||
<Stat>
|
||||
<StatNumber>{stats.personCount ?? '100+'}</StatNumber>
|
||||
<StatLabel>Availabilities entered</StatLabel>
|
||||
<StatNumber>{stats.personCount ?? '400+'}</StatNumber>
|
||||
<StatLabel>{t('home:about.availabilities')}</StatLabel>
|
||||
</Stat>
|
||||
</Stats>
|
||||
<P>Crab Fit helps you fit your event around everyone's schedules. Simply create an event above and send the link to everyone that is participating. Results update live and you will be able to see a heat-map of when everyone is free.<br /><Link to="/how-to">Learn more about how to Crab Fit</Link>.</P>
|
||||
<P><Trans i18nKey="home:about.content.p1">Crab Fit helps you fit your event around everyone's schedules. Simply create an event above and send the link to everyone that is participating. Results update live and you will be able to see a heat-map of when everyone is free.<br /><Link to="/how-to">Learn more about how to Crab Fit</Link>.</Trans></P>
|
||||
{/* eslint-disable-next-line */}
|
||||
<P>Create a lot of Crab Fits? Get the <a href="https://chrome.google.com/webstore/detail/crab-fit/pnafiibmjbiljofcpjlbonpgdofjhhkj" target="_blank">Chrome extension</a> or <a href="https://addons.mozilla.org/en-US/firefox/addon/crab-fit/" target="_blank">Firefox extension</a> for your browser! You can also download the <a href="https://play.google.com/store/apps/details?id=fit.crab" target="_blank">Android app</a> to Crab Fit on the go.</P>
|
||||
<P><Trans i18nKey="home:about.content.p2">Create a lot of Crab Fits? Get the <a href="https://chrome.google.com/webstore/detail/crab-fit/pnafiibmjbiljofcpjlbonpgdofjhhkj" target="_blank">Chrome extension</a> or <a href="https://addons.mozilla.org/en-US/firefox/addon/crab-fit/" target="_blank">Firefox extension</a> for your browser! You can also download the <a href="https://play.google.com/store/apps/details?id=fit.crab" target="_blank">Android app</a> to Crab Fit on the go.</Trans></P>
|
||||
{/* eslint-disable-next-line */}
|
||||
<P>Created by <a href="https://bengrant.dev" target="_blank">Ben Grant</a>, Crab Fit is the modern-day solution to your group event planning debates.</P>
|
||||
<P>The code for Crab Fit is open source, if you find any issues or want to contribute, you can visit the <a href="https://github.com/GRA0007/crab.fit" target="_blank" rel="noreferrer">repository</a>. By using Crab Fit you agree to the <Link to="/privacy">privacy policy</Link>.</P>
|
||||
<P>Crab Fit costs more than <strong>$100 per month</strong> to run. Consider donating below if it helped you out so it can stay free for everyone. 🦀</P>
|
||||
<P><Trans i18nKey="home:about.content.p3">Created by <a href="https://bengrant.dev" target="_blank">Ben Grant</a>, Crab Fit is the modern-day solution to your group event planning debates.</Trans></P>
|
||||
<P><Trans i18nKey="home:about.content.p4">The code for Crab Fit is open source, if you find any issues or want to contribute, you can visit the <a href="https://github.com/GRA0007/crab.fit" target="_blank" rel="noreferrer">repository</a>. By using Crab Fit you agree to the <Link to="/privacy">privacy policy</Link>.</Trans></P>
|
||||
<P><Trans i18nKey="home:about.content.p5">Crab Fit costs more than <strong>$100 per month</strong> to run. Consider donating below if it helped you out so it can stay free for everyone. 🦀</Trans></P>
|
||||
</StyledMain>
|
||||
</AboutSection>
|
||||
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ export const TitleSmall = styled.span`
|
|||
font-weight: 400;
|
||||
color: ${props => props.theme.primaryDark};
|
||||
line-height: 1em;
|
||||
text-transform: uppercase;
|
||||
`;
|
||||
|
||||
export const TitleLarge = styled.h1`
|
||||
|
|
@ -30,6 +31,7 @@ export const TitleLarge = styled.h1`
|
|||
font-weight: 400;
|
||||
text-shadow: 0 4px 0 ${props => props.theme.primaryDark};
|
||||
line-height: 1em;
|
||||
text-transform: uppercase;
|
||||
|
||||
@media (max-width: 350px) {
|
||||
font-size: 3.5rem;
|
||||
|
|
@ -85,38 +87,3 @@ export const OfflineMessage = styled.div`
|
|||
text-align: center;
|
||||
margin: 50px 0 20px;
|
||||
`;
|
||||
|
||||
export const Recent = styled.a`
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 5px 0;
|
||||
flex-wrap: wrap;
|
||||
|
||||
& .name {
|
||||
font-weight: 700;
|
||||
font-size: 1.1em;
|
||||
color: ${props => props.theme.primaryDark};
|
||||
flex: 1;
|
||||
display: block;
|
||||
}
|
||||
& .date {
|
||||
font-weight: 400;
|
||||
opacity: .8;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
&:hover .name {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
@media (max-width: 500px) {
|
||||
display: block;
|
||||
|
||||
& .date {
|
||||
white-space: normal;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { useEffect } from 'react';
|
||||
import { Link, useHistory } from 'react-router-dom';
|
||||
import { useTranslation, Trans } from 'react-i18next';
|
||||
|
||||
import {
|
||||
Button,
|
||||
|
|
@ -19,10 +20,11 @@ import logo from 'res/logo.svg';
|
|||
|
||||
const Privacy = () => {
|
||||
const { push } = useHistory();
|
||||
const { t } = useTranslation(['common', 'privacy']);
|
||||
|
||||
useEffect(() => {
|
||||
document.title = 'Privacy Policy - Crab Fit';
|
||||
}, []);
|
||||
document.title = `${t('privacy:name')} - Crab Fit`;
|
||||
}, [t]);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
|
@ -32,65 +34,65 @@ const Privacy = () => {
|
|||
<Logo src={logo} alt="" />
|
||||
<Title>CRAB FIT</Title>
|
||||
</Center>
|
||||
<Center style={{ textDecoration: 'underline', fontSize: 14, paddingTop: 6 }}>Create your own</Center>
|
||||
<Center style={{ textDecoration: 'underline', fontSize: 14, paddingTop: 6 }}>{t('common:tagline')}</Center>
|
||||
</Link>
|
||||
</StyledMain>
|
||||
|
||||
<StyledMain>
|
||||
<h1>Privacy Policy</h1>
|
||||
<h1>{t('privacy:name')}</h1>
|
||||
<h3>Crab Fit</h3>
|
||||
<P>This SERVICE is provided by Benjamin Grant at no cost and is intended for use as is.</P>
|
||||
<P>This page is used to inform visitors regarding the policies of the collection, use, and disclosure of Personal Information if using the Service.</P>
|
||||
<P>If you choose to use the Service, then you agree to the collection and use of information in relation to this policy. The Personal Information that is collected is used for providing and improving the Service. Your information will not be used or shared with anyone except as described in this Privacy Policy.</P>
|
||||
<P>{t('privacy:p1')}</P>
|
||||
<P>{t('privacy:p2')}</P>
|
||||
<P>{t('privacy:p3')}</P>
|
||||
|
||||
<h2>Information Collection and Use</h2>
|
||||
<P>The Service uses third party services that may collect information used to identify you.</P>
|
||||
<P>Links to privacy policies of the third party service providers used by the Service:</P>
|
||||
<h2>{t('privacy:h1')}</h2>
|
||||
<P>{t('privacy:p4')}</P>
|
||||
<P>{t('privacy:p5')}</P>
|
||||
<P>
|
||||
<ul>
|
||||
<li><a href="https://www.google.com/policies/privacy/" target="blank">Google Play Services</a></li>
|
||||
<li><a href="https://www.google.com/policies/privacy/" target="blank">{t('privacy:link')}</a></li>
|
||||
</ul>
|
||||
</P>
|
||||
|
||||
<h2>Log Data</h2>
|
||||
<P>When you use the Service, in the case of an error, data and information is collected to improve the Service, which may include your IP address, device name, operating system version, app configuration and the time and date of the error.</P>
|
||||
<h2>{t('privacy:h2')}</h2>
|
||||
<P>{t('privacy:p6')}</P>
|
||||
|
||||
<h2>Cookies</h2>
|
||||
<P>Cookies are files with a small amount of data that are commonly used as anonymous unique identifiers. These are sent to your browser from the websites that you visit and are stored on your device's internal memory.</P>
|
||||
<P>Cookies are used by Google Analytics to track you across the web and provide anonymous statistics to improve the Service.</P>
|
||||
<h2>{t('privacy:h3')}</h2>
|
||||
<P>{t('privacy:p7')}</P>
|
||||
<P>{t('privacy:p8')}</P>
|
||||
|
||||
<h2>Service Providers</h2>
|
||||
<P>Third-party companies may be employed for the following reasons:</P>
|
||||
<h2>{t('privacy:h4')}</h2>
|
||||
<P>{t('privacy:p9')}</P>
|
||||
<P>
|
||||
<ul>
|
||||
<li>To facilitate the Service</li>
|
||||
<li>To provide the Service on our behalf</li>
|
||||
<li>To perform Service-related services</li>
|
||||
<li>To assist in analyzing how the Service is used</li>
|
||||
<li>{t('privacy:l1')}</li>
|
||||
<li>{t('privacy:l2')}</li>
|
||||
<li>{t('privacy:l3')}</li>
|
||||
<li>{t('privacy:l4')}</li>
|
||||
</ul>
|
||||
</P>
|
||||
<P>To perform these tasks, the third parties may have access to your Personal Information, but are obligated not to disclose or use this information for any purpose except the above.</P>
|
||||
<P>{t('privacy:p10')}</P>
|
||||
|
||||
<h2>Security</h2>
|
||||
<P>Personal Information that is shared via the Service is protected, however remember that no method of transmission over the internet, or method of electronic storage is 100% secure and reliable, so take care when sharing Personal Information.</P>
|
||||
<h2>{t('privacy:h5')}</h2>
|
||||
<P>{t('privacy:p11')}</P>
|
||||
|
||||
<h2>Links to Other Sites</h2>
|
||||
<P>The Service may contain links to other sites. If you click on a third-party link, you will be directed to that site. Note that these external sites are not operated by the Service. Therefore, you are advised to review the Privacy Policy of these websites.</P>
|
||||
<h2>{t('privacy:h6')}</h2>
|
||||
<P>{t('privacy:p12')}</P>
|
||||
|
||||
<h2>Children's Privacy</h2>
|
||||
<P>The Service does not address anyone under the age of 13. Personally identifiable information is not knowingly collected from children under 13. If discovered that a child under 13 has provided the Service with personal information, such information will be immediately deleted from the servers. If you are a parent or guardian and you are aware that your child has provided the Service with personal information, please <a href="mailto:benjamin.grantGRA0007+crabfit@gmail.com">contact us</a> so that this information can be removed.</P>
|
||||
<h2>{t('privacy:h7')}</h2>
|
||||
<P><Trans i18nKey="privacy:p13">The Service does not address anyone under the age of 13. Personally identifiable information is not knowingly collected from children under 13. If discovered that a child under 13 has provided the Service with personal information, such information will be immediately deleted from the servers. If you are a parent or guardian and you are aware that your child has provided the Service with personal information, please <a href="mailto:benjamin.grantGRA0007+crabfit@gmail.com">contact us</a> so that this information can be removed.</Trans></P>
|
||||
|
||||
<h2>Changes to This Privacy Policy</h2>
|
||||
<P>This Privacy Policy may be updated from time to time. Thus, you are advised to review this page periodically for any changes.</P>
|
||||
<P>This policy is effective as of 2021-04-20</P>
|
||||
<h2>{t('privacy:h8')}</h2>
|
||||
<P>{t('privacy:p14')}</P>
|
||||
<P>{t('privacy:p15')}</P>
|
||||
|
||||
<h2>Contact Us</h2>
|
||||
<P>If you have any questions or suggestions about the Privacy Policy, do not hesitate to contact us at <a href="mailto:benjamin.grantGRA0007+crabfit@gmail.com">benjamin.grantGRA0007+crabfit@gmail.com</a>.</P>
|
||||
<h2>{t('privacy:h9')}</h2>
|
||||
<P><Trans i18nKey="privacy:p16">If you have any questions or suggestions about the Privacy Policy, do not hesitate to contact us at <a href="mailto:benjamin.grantGRA0007+crabfit@gmail.com">benjamin.grantGRA0007+crabfit@gmail.com</a>.</Trans></P>
|
||||
</StyledMain>
|
||||
|
||||
<AboutSection id="about">
|
||||
<StyledMain>
|
||||
<Center><Button buttonWidth="230px" onClick={() => push('/')}>Create your own Crab Fit!</Button></Center>
|
||||
<Center><Button buttonWidth="230px" onClick={() => push('/')}>{t('common:cta')}</Button></Center>
|
||||
</StyledMain>
|
||||
</AboutSection>
|
||||
|
||||
|
|
|
|||
|
|
@ -1105,6 +1105,13 @@
|
|||
dependencies:
|
||||
regenerator-runtime "^0.13.4"
|
||||
|
||||
"@babel/runtime@^7.12.0", "@babel/runtime@^7.13.6":
|
||||
version "7.14.0"
|
||||
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.14.0.tgz#46794bc20b612c5f75e62dd071e24dfd95f1cbe6"
|
||||
integrity sha512-JELkvo/DlpNdJ7dlyw/eY7E0suy5i5GQH+Vlxaq1nsNJ+H7f4Vtv3jMeCEgRhZZQFXTjldYfQgv2qmM6M1v5wA==
|
||||
dependencies:
|
||||
regenerator-runtime "^0.13.4"
|
||||
|
||||
"@babel/template@^7.10.4", "@babel/template@^7.12.13", "@babel/template@^7.3.3":
|
||||
version "7.12.13"
|
||||
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.12.13.tgz#530265be8a2589dbb37523844c5bcb55947fb327"
|
||||
|
|
@ -3677,6 +3684,13 @@ create-hmac@^1.1.0, create-hmac@^1.1.4, create-hmac@^1.1.7:
|
|||
safe-buffer "^5.0.1"
|
||||
sha.js "^2.4.8"
|
||||
|
||||
cross-fetch@3.1.4:
|
||||
version "3.1.4"
|
||||
resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.1.4.tgz#9723f3a3a247bf8b89039f3a380a9244e8fa2f39"
|
||||
integrity sha512-1eAtFWdIubi6T4XPy6ei9iUFoKpUkIF971QLN8lIvvvwueI65+Nw5haMNKUwfJxabqlIIDODJKGrQ66gxC0PbQ==
|
||||
dependencies:
|
||||
node-fetch "2.6.1"
|
||||
|
||||
cross-spawn@7.0.3, cross-spawn@^7.0.0, cross-spawn@^7.0.2:
|
||||
version "7.0.3"
|
||||
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6"
|
||||
|
|
@ -5637,6 +5651,13 @@ html-minifier-terser@^5.0.1:
|
|||
relateurl "^0.2.7"
|
||||
terser "^4.6.3"
|
||||
|
||||
html-parse-stringify@^3.0.1:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz#dfc1017347ce9f77c8141a507f233040c59c55d2"
|
||||
integrity sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==
|
||||
dependencies:
|
||||
void-elements "3.1.0"
|
||||
|
||||
html-webpack-plugin@4.5.0:
|
||||
version "4.5.0"
|
||||
resolved "https://registry.yarnpkg.com/html-webpack-plugin/-/html-webpack-plugin-4.5.0.tgz#625097650886b97ea5dae331c320e3238f6c121c"
|
||||
|
|
@ -5744,6 +5765,27 @@ human-signals@^1.1.1:
|
|||
resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-1.1.1.tgz#c5b1cd14f50aeae09ab6c59fe63ba3395fe4dfa3"
|
||||
integrity sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==
|
||||
|
||||
i18next-browser-languagedetector@^6.1.1:
|
||||
version "6.1.1"
|
||||
resolved "https://registry.yarnpkg.com/i18next-browser-languagedetector/-/i18next-browser-languagedetector-6.1.1.tgz#fc4c6606bb3f7afc331737cf7c41e50919d55542"
|
||||
integrity sha512-hckgbBdCpJPhkGUANe6tsvD52k9R7GuYskG0EaIw89pZz3owUvUEwXHqM5pX1Pn93jz+O65Y09ikwJrMkqtq2Q==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.5.5"
|
||||
|
||||
i18next-http-backend@^1.2.4:
|
||||
version "1.2.4"
|
||||
resolved "https://registry.yarnpkg.com/i18next-http-backend/-/i18next-http-backend-1.2.4.tgz#2be7d5e569557c22e4dd50df92425ee6c73f3296"
|
||||
integrity sha512-ewvodowF2oBP0/vVAerpVF6aaIdAqH594K/ThA4Kl2A5Gm4QvUQuakvrFV5KMaKOggykGd9MuQ4xMcTFayVF1w==
|
||||
dependencies:
|
||||
cross-fetch "3.1.4"
|
||||
|
||||
i18next@^20.2.4:
|
||||
version "20.2.4"
|
||||
resolved "https://registry.yarnpkg.com/i18next/-/i18next-20.2.4.tgz#972220f19dfef0075a70890d3e8b1f7cf64c5bd6"
|
||||
integrity sha512-goE1LCA/IZOGG26PkkqoOl2KWR7YP606SvokVQZ29J6QwE02KycrzNetoMUJeqYrTxs4rmiiZgZp+q8qofQL6Q==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.12.0"
|
||||
|
||||
iconv-lite@0.4.24:
|
||||
version "0.4.24"
|
||||
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b"
|
||||
|
|
@ -7523,6 +7565,11 @@ no-case@^3.0.4:
|
|||
lower-case "^2.0.2"
|
||||
tslib "^2.0.3"
|
||||
|
||||
node-fetch@2.6.1:
|
||||
version "2.6.1"
|
||||
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052"
|
||||
integrity sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==
|
||||
|
||||
node-forge@^0.10.0:
|
||||
version "0.10.0"
|
||||
resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.10.0.tgz#32dea2afb3e9926f02ee5ce8794902691a676bf3"
|
||||
|
|
@ -9153,6 +9200,14 @@ react-hook-form@^6.15.4:
|
|||
resolved "https://registry.yarnpkg.com/react-hook-form/-/react-hook-form-6.15.4.tgz#328003e1ccc096cd158899ffe7e3b33735a9b024"
|
||||
integrity sha512-K+Sw33DtTMengs8OdqFJI3glzNl1wBzSefD/ksQw/hJf9CnOHQAU6qy82eOrh0IRNt2G53sjr7qnnw1JDjvx1w==
|
||||
|
||||
react-i18next@^11.8.15:
|
||||
version "11.8.15"
|
||||
resolved "https://registry.yarnpkg.com/react-i18next/-/react-i18next-11.8.15.tgz#89450d585298f18d4a8eb1628b0868863f3a4767"
|
||||
integrity sha512-ZbKcbYYKukgDL0MiUWKJTEsEftjSTNVZv67/V+SjPqTRwuF/aL4NbUtuEcb4WjHk0HyZ1M+2wGd07Fp0RUNHKA==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.13.6"
|
||||
html-parse-stringify "^3.0.1"
|
||||
|
||||
react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.1:
|
||||
version "16.13.1"
|
||||
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
|
||||
|
|
@ -11127,6 +11182,11 @@ vm-browserify@^1.0.1:
|
|||
resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-1.1.2.tgz#78641c488b8e6ca91a75f511e7a3b32a86e5dda0"
|
||||
integrity sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==
|
||||
|
||||
void-elements@3.1.0:
|
||||
version "3.1.0"
|
||||
resolved "https://registry.yarnpkg.com/void-elements/-/void-elements-3.1.0.tgz#614f7fbf8d801f0bb5f0661f5b2f5785750e4f09"
|
||||
integrity sha1-YU9/v42AHwu18GYfWy9XhXUOTwk=
|
||||
|
||||
w3c-hr-time@^1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz#0a89cdf5cc15822df9c360543676963e0cc308cd"
|
||||
|
|
|
|||
Loading…
Reference in a new issue