diff --git a/README.md b/README.md index 125367b..e23e460 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ If you speak a language other than English and you want to help translate Crab F 1. Clone the repo. 2. Run `yarn` in both backend and frontend folders. -3. Run `node index.js` in the backend folder to start the API. +3. Run `node index.js` in the backend folder to start the API. **Note:** you will need a google cloud app set up with datastore enabled and set your `GOOGLE_APPLICATION_CREDENTIALS` environment variable to your service key path. 4. Run `yarn start` in the frontend folder to start the front end. ### 🔌 Browser extension @@ -34,6 +34,7 @@ The browser extension in `crabfit-browser-extension` can be tested by first runn ### ⚙️ Backend 1. In the backend folder `cd crabfit-backend` 2. Deploy the backend `gcloud app deploy --project=crabfit --version=v1` +3. To deploy cron jobs (i.e. monthly cleanup of old events), run `gcloud app deploy cron.yaml` ### 🔌 Browser extension Compress everything inside the `crabfit-browser-extension` folder and use that zip to deploy using Chrome web store and Mozilla Add-on store. diff --git a/crabfit-backend/cron.yaml b/crabfit-backend/cron.yaml new file mode 100644 index 0000000..321927d --- /dev/null +++ b/crabfit-backend/cron.yaml @@ -0,0 +1,7 @@ +cron: + - description: "clean up old events" + url: /tasks/cleanup + schedule: every monday 09:00 + - description: "clean up old events without a visited date" + url: /tasks/legacyCleanup + schedule: every tuesday 09:00 diff --git a/crabfit-backend/index.js b/crabfit-backend/index.js index 91b2c0b..7dc0b21 100644 --- a/crabfit-backend/index.js +++ b/crabfit-backend/index.js @@ -14,6 +14,9 @@ const createPerson = require('./routes/createPerson'); const login = require('./routes/login'); const updatePerson = require('./routes/updatePerson'); +const taskCleanup = require('./routes/taskCleanup'); +const taskLegacyCleanup = require('./routes/taskLegacyCleanup'); + const app = express(); const port = 8080; const corsOptions = { @@ -30,6 +33,7 @@ app.use((req, res, next) => { req.types = { event: process.env.NODE_ENV === 'production' ? 'Event' : 'DevEvent', person: process.env.NODE_ENV === 'production' ? 'Person' : 'DevPerson', + stats: process.env.NODE_ENV === 'production' ? 'Stats' : 'DevStats', }; next(); }); @@ -46,6 +50,10 @@ app.post('/event/:eventId/people', createPerson); app.post('/event/:eventId/people/:personName', login); app.patch('/event/:eventId/people/:personName', updatePerson); +// Tasks +app.get('/tasks/cleanup', taskCleanup); +app.get('/tasks/legacyCleanup', taskLegacyCleanup); + app.listen(port, () => { console.log(`Crabfit API listening at http://localhost:${port} in ${process.env.NODE_ENV === 'production' ? 'prod' : 'dev'} mode`) }); diff --git a/crabfit-backend/routes/createEvent.js b/crabfit-backend/routes/createEvent.js index 9fa0fe1..9cf035d 100644 --- a/crabfit-backend/routes/createEvent.js +++ b/crabfit-backend/routes/createEvent.js @@ -62,6 +62,18 @@ module.exports = async (req, res) => { times: event.times, timezone: event.timezone, }); + + // Update stats + let eventCountResult = (await req.datastore.get(req.datastore.key([req.types.stats, 'eventCount'])))[0] || null; + if (eventCountResult) { + eventCountResult.value++; + await req.datastore.upsert(eventCountResult); + } else { + await req.datastore.insert({ + key: req.datastore.key([req.types.stats, 'eventCount']), + data: { value: 1 }, + }); + } } catch (e) { console.error(e); res.sendStatus(400); diff --git a/crabfit-backend/routes/createPerson.js b/crabfit-backend/routes/createPerson.js index 81846d3..d082b4e 100644 --- a/crabfit-backend/routes/createPerson.js +++ b/crabfit-backend/routes/createPerson.js @@ -36,6 +36,18 @@ module.exports = async (req, res) => { await req.datastore.insert(entity); res.sendStatus(201); + + // Update stats + let personCountResult = (await req.datastore.get(req.datastore.key([req.types.stats, 'personCount'])))[0] || null; + if (personCountResult) { + personCountResult.value++; + await req.datastore.upsert(personCountResult); + } else { + await req.datastore.insert({ + key: req.datastore.key([req.types.stats, 'personCount']), + data: { value: 1 }, + }); + } } else { res.sendStatus(400); } diff --git a/crabfit-backend/routes/getEvent.js b/crabfit-backend/routes/getEvent.js index b3a464d..f76e20d 100644 --- a/crabfit-backend/routes/getEvent.js +++ b/crabfit-backend/routes/getEvent.js @@ -1,14 +1,20 @@ +const dayjs = require('dayjs'); + module.exports = async (req, res) => { const { eventId } = req.params; try { - const event = (await req.datastore.get(req.datastore.key([req.types.event, eventId])))[0]; + let event = (await req.datastore.get(req.datastore.key([req.types.event, eventId])))[0]; if (event) { res.send({ id: eventId, ...event, }); + + // Update last visited time + event.visited = dayjs().unix(); + await req.datastore.upsert(event); } else { res.sendStatus(404); } diff --git a/crabfit-backend/routes/stats.js b/crabfit-backend/routes/stats.js index 35d405a..a7e59e2 100644 --- a/crabfit-backend/routes/stats.js +++ b/crabfit-backend/routes/stats.js @@ -4,19 +4,24 @@ module.exports = async (req, res) => { let eventCount = null; let personCount = null; - try { - const eventQuery = req.datastore.createQuery(['__Stat_Kind__']).filter('kind_name', req.types.event); - const personQuery = req.datastore.createQuery(['__Stat_Kind__']).filter('kind_name', req.types.person); + try { + const eventResult = (await req.datastore.get(req.datastore.key([req.types.stats, 'eventCount'])))[0] || null; + const personResult = (await req.datastore.get(req.datastore.key([req.types.stats, 'personCount'])))[0] || null; + + if (eventResult) { + eventCount = eventResult.value; + } + if (personResult) { + personCount = personResult.value; + } - eventCount = (await req.datastore.runQuery(eventQuery))[0][0].count; - personCount = (await req.datastore.runQuery(personQuery))[0][0].count; } catch (e) { console.error(e); } - res.send({ - eventCount: eventCount || null, - personCount: personCount || null, - version: package.version, - }); + res.send({ + eventCount, + personCount, + version: package.version, + }); }; diff --git a/crabfit-backend/routes/taskCleanup.js b/crabfit-backend/routes/taskCleanup.js new file mode 100644 index 0000000..abf41c9 --- /dev/null +++ b/crabfit-backend/routes/taskCleanup.js @@ -0,0 +1,46 @@ +const dayjs = require('dayjs'); + +module.exports = async (req, res) => { + if (req.header('X-Appengine-Cron') === undefined) { + return res.status(400).send('This task can only be run from a cron job'); + } + + const threeMonthsAgo = dayjs().subtract(3, 'month').unix(); + + console.log('Running cleanup task at', dayjs().format('h:mma D MMM YYYY')); + + try { + // Fetch events that haven't been visited in over 3 months + const eventQuery = req.datastore.createQuery(req.types.event).filter('visited', '<', threeMonthsAgo); + let oldEvents = (await req.datastore.runQuery(eventQuery))[0]; + + if (oldEvents && oldEvents.length > 0) { + let oldEventIds = oldEvents.map(e => e[req.datastore.KEY].name); + console.log('Found', oldEventIds.length, 'events to remove'); + + // Fetch availabilities linked to the events discovered + let peopleDiscovered = 0; + await Promise.all(oldEventIds.map(async (eventId) => { + const peopleQuery = req.datastore.createQuery(req.types.person).filter('eventId', eventId); + let oldPeople = (await req.datastore.runQuery(peopleQuery))[0]; + + if (oldPeople && oldPeople.length > 0) { + peopleDiscovered += oldPeople.length; + await req.datastore.delete(oldPeople.map(person => person[req.datastore.KEY])); + } + })); + + await req.datastore.delete(oldEvents.map(event => event[req.datastore.KEY])); + + console.log('Cleanup successful:', oldEventIds.length, 'events and', peopleDiscovered, 'people removed'); + + res.sendStatus(200); + } else { + console.log('Found', 0, 'events to remove, ending cleanup'); + res.sendStatus(404); + } + } catch (e) { + console.error(e); + res.sendStatus(404); + } +}; diff --git a/crabfit-backend/routes/taskLegacyCleanup.js b/crabfit-backend/routes/taskLegacyCleanup.js new file mode 100644 index 0000000..4ca1d5b --- /dev/null +++ b/crabfit-backend/routes/taskLegacyCleanup.js @@ -0,0 +1,68 @@ +const dayjs = require('dayjs'); + +module.exports = async (req, res) => { + if (req.header('X-Appengine-Cron') === undefined) { + return res.status(400).send('This task can only be run from a cron job'); + } + + const threeMonthsAgo = dayjs().subtract(3, 'month').unix(); + + console.log('Running LEGACY cleanup task at', dayjs().format('h:mma D MMM YYYY')); + + try { + // Fetch events that haven't been visited in over 3 months + const eventQuery = req.datastore.createQuery(req.types.event).order('created'); + let oldEvents = (await req.datastore.runQuery(eventQuery))[0]; + + oldEvents = oldEvents.filter(event => !event.hasOwnProperty('visited')); + + if (oldEvents && oldEvents.length > 0) { + console.log('Found', oldEvents.length, 'events that were missing a visited date'); + + // Filter events that are older than 3 months and missing a visited date + oldEvents = oldEvents.filter(event => event.created < threeMonthsAgo); + + if (oldEvents && oldEvents.length > 0) { + let oldEventIds = oldEvents.map(e => e[req.datastore.KEY].name); + + // Fetch availabilities linked to the events discovered + let eventsRemoved = 0; + let peopleRemoved = 0; + await Promise.all(oldEventIds.map(async (eventId) => { + const peopleQuery = req.datastore.createQuery(req.types.person).filter('eventId', eventId); + let oldPeople = (await req.datastore.runQuery(peopleQuery))[0]; + + let deleteEvent = true; + if (oldPeople && oldPeople.length > 0) { + oldPeople.forEach(person => { + if (person.created >= threeMonthsAgo) { + deleteEvent = false; + } + }); + } + if (deleteEvent) { + if (oldPeople && oldPeople.length > 0) { + peopleRemoved += oldPeople.length; + await req.datastore.delete(oldPeople.map(person => person[req.datastore.KEY])); + } + eventsRemoved++; + await req.datastore.delete(req.datastore.key([req.types.event, eventId])); + } + })); + + console.log('Legacy cleanup successful:', eventsRemoved, 'events and', peopleRemoved, 'people removed'); + + res.sendStatus(200); + } else { + console.log('Found', 0, 'events that are older than 3 months and missing a visited date, ending LEGACY cleanup'); + res.sendStatus(404); + } + } else { + console.error('Found no events that are missing a visited date, ending LEGACY cleanup [DISABLE ME!]'); + res.sendStatus(404); + } + } catch (e) { + console.error(e); + res.sendStatus(404); + } +}; diff --git a/crabfit-backend/swagger.yaml b/crabfit-backend/swagger.yaml index e64c0c9..7e15592 100644 --- a/crabfit-backend/swagger.yaml +++ b/crabfit-backend/swagger.yaml @@ -217,3 +217,29 @@ paths: description: "Not found" 400: description: "Invalid data" + "/tasks/cleanup": + get: + summary: "Delete events inactive for more than 3 months" + operationId: "taskCleanup" + tags: + - tasks + responses: + 200: + description: "OK" + 404: + description: "Not found" + 400: + description: "Not called from a cron job" + "/tasks/legacyCleanup": + get: + summary: "Delete events inactive for more than 3 months that don't have a visited date" + operationId: "taskLegacyCleanup" + tags: + - tasks + responses: + 200: + description: "OK" + 404: + description: "Not found" + 400: + description: "Not called from a cron job" diff --git a/crabfit-frontend/public/i18n/en/event.json b/crabfit-frontend/public/i18n/en/event.json index c281bed..4b563d0 100644 --- a/crabfit-frontend/public/i18n/en/event.json +++ b/crabfit-frontend/public/i18n/en/event.json @@ -17,11 +17,13 @@ "name": "Your name", "password": "Password (optional)", "button": "Login", + "logout_button": "Sign out", "info": "These details are only for this event. Use a password to prevent others from changing your availability.", "timezone": "Your time zone", "errors": { + "name_required": "Your name is needed to store your availability.", "password_incorrect": "Password is incorrect. Check your name is spelled right.", "unknown": "Failed to login. Please try again." }, @@ -35,7 +37,7 @@ }, "error": { "title": "Event not found", - "body": "Check that the url you entered is correct." + "body": "Check that the url you entered is correct. Note that to protect your privacy, events are deleted after 3 months of inactivity." }, "tabs": { diff --git a/crabfit-frontend/public/i18n/en/home.json b/crabfit-frontend/public/i18n/en/home.json index 98db888..689b637 100644 --- a/crabfit-frontend/public/i18n/en/home.json +++ b/crabfit-frontend/public/i18n/en/home.json @@ -50,6 +50,7 @@ "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.", "p3": "Created by <1>Ben Grant, 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. By using Crab Fit you agree to the <3>privacy policy.", + "p6": "To protect your privacy, events are deleted after 3 months of inactivity, and all passwords are securely hashed.", "p5": "Consider donating below if it helped you out so Crab Fit can stay free for everyone. 🦀" }, "chrome_extension": "Get the Chrome Extension", diff --git a/crabfit-frontend/public/i18n/en/privacy.json b/crabfit-frontend/public/i18n/en/privacy.json index a83ae48..b9daa88 100644 --- a/crabfit-frontend/public/i18n/en/privacy.json +++ b/crabfit-frontend/public/i18n/en/privacy.json @@ -1,52 +1,4 @@ { "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 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>crabfit@bengrant.dev." + "translate": "View in your language" } diff --git a/crabfit-frontend/src/components/Button/buttonStyle.ts b/crabfit-frontend/src/components/Button/buttonStyle.ts index 766769a..bc75e35 100644 --- a/crabfit-frontend/src/components/Button/buttonStyle.ts +++ b/crabfit-frontend/src/components/Button/buttonStyle.ts @@ -83,7 +83,7 @@ export const Pressable = styled.button` left: calc(50% - 12px); height: 18px; width: 18px; - border: 3px solid #FFF; + border: 3px solid ${props.primaryColor ? '#FFF' : props.theme.background}; border-left-color: transparent; border-radius: 100px; animation: load .5s linear infinite; @@ -92,7 +92,7 @@ export const Pressable = styled.button` @media (prefers-reduced-motion: reduce) { &:after { content: 'loading...'; - color: #FFF; + color: ${props.primaryColor ? '#FFF' : props.theme.background}; animation: none; width: initial; height: initial; diff --git a/crabfit-frontend/src/components/GoogleCalendar/GoogleCalendar.tsx b/crabfit-frontend/src/components/GoogleCalendar/GoogleCalendar.tsx index 0c14f07..335f779 100644 --- a/crabfit-frontend/src/components/GoogleCalendar/GoogleCalendar.tsx +++ b/crabfit-frontend/src/components/GoogleCalendar/GoogleCalendar.tsx @@ -13,6 +13,7 @@ import { Options, Title, Icon, + LinkButton, } from './googleCalendarStyle'; import googleLogo from 'res/google.svg'; @@ -111,26 +112,23 @@ const GoogleCalendar = ({ timeZone, timeMin, timeMax, onImport }) => { <Icon src={googleLogo} alt="" /> <strong>{t('event:you.google_cal.login')}</strong> - {/* eslint-disable-next-line */} - (<a href="#" onClick={e => { + (<LinkButton type="button" onClick={e => { e.preventDefault(); signOut(); - }}>{t('event:you.google_cal.logout')}</a>) + }}>{t('event:you.google_cal.logout')}</LinkButton>) {calendars !== undefined && !calendars.every(c => c.checked) && ( - /* eslint-disable-next-line */ - { + { e.preventDefault(); setCalendars(calendars.map(c => ({...c, checked: true}))); - }}>{t('event:you.google_cal.select_all')} + }}>{t('event:you.google_cal.select_all')} )} {calendars !== undefined && calendars.every(c => c.checked) && ( - /* eslint-disable-next-line */ - { + { e.preventDefault(); setCalendars(calendars.map(c => ({...c, checked: false}))); - }}>{t('event:you.google_cal.select_none')} + }}>{t('event:you.google_cal.select_none')} )} {calendars !== undefined ? calendars.map(calendar => ( diff --git a/crabfit-frontend/src/components/GoogleCalendar/googleCalendarStyle.ts b/crabfit-frontend/src/components/GoogleCalendar/googleCalendarStyle.ts index ca7c0f8..00fd0f8 100644 --- a/crabfit-frontend/src/components/GoogleCalendar/googleCalendarStyle.ts +++ b/crabfit-frontend/src/components/GoogleCalendar/googleCalendarStyle.ts @@ -119,3 +119,16 @@ export const Icon = styled.img` filter: invert(1); `} `; + +export const LinkButton = styled.button` + font: inherit; + color: ${props => props.theme.primary}; + border: 0; + background: none; + text-decoration: underline; + padding: 0; + margin: 0; + display: inline; + cursor: pointer; + appearance: none; +`; diff --git a/crabfit-frontend/src/components/OutlookCalendar/OutlookCalendar.tsx b/crabfit-frontend/src/components/OutlookCalendar/OutlookCalendar.tsx index ebc590b..05242ee 100644 --- a/crabfit-frontend/src/components/OutlookCalendar/OutlookCalendar.tsx +++ b/crabfit-frontend/src/components/OutlookCalendar/OutlookCalendar.tsx @@ -14,6 +14,7 @@ import { Options, Title, Icon, + LinkButton, } from '../GoogleCalendar/googleCalendarStyle'; import outlookLogo from 'res/outlook.svg'; @@ -178,26 +179,23 @@ const OutlookCalendar = ({ timeZone, timeMin, timeMax, onImport }) => { <Icon src={outlookLogo} alt="" /> <strong>{t('event:you.outlook_cal')}</strong> - {/* eslint-disable-next-line */} - (<a href="#" onClick={e => { + (<LinkButton type="button" onClick={e => { e.preventDefault(); signOut(); - }}>{t('event:you.google_cal.logout')}</a>) + }}>{t('event:you.google_cal.logout')}</LinkButton>) {calendars !== undefined && !calendars.every(c => c.checked) && ( - /* eslint-disable-next-line */ - { + { e.preventDefault(); setCalendars(calendars.map(c => ({...c, checked: true}))); - }}>{t('event:you.google_cal.select_all')} + }}>{t('event:you.google_cal.select_all')} )} {calendars !== undefined && calendars.every(c => c.checked) && ( - /* eslint-disable-next-line */ - { + { e.preventDefault(); setCalendars(calendars.map(c => ({...c, checked: false}))); - }}>{t('event:you.google_cal.select_none')} + }}>{t('event:you.google_cal.select_none')} )} {calendars !== undefined ? calendars.map(calendar => ( diff --git a/crabfit-frontend/src/pages/Event/Event.tsx b/crabfit-frontend/src/pages/Event/Event.tsx index 5f4ce82..c9dfa4b 100644 --- a/crabfit-frontend/src/pages/Event/Event.tsx +++ b/crabfit-frontend/src/pages/Event/Event.tsx @@ -51,7 +51,7 @@ const Event = (props) => { const { t } = useTranslation(['common', 'event']); - const { register, handleSubmit, setFocus } = useForm(); + const { register, handleSubmit, setFocus, reset } = useForm(); const { id } = props.match.params; const { offline } = props; const [timezone, setTimezone] = useState(Intl.DateTimeFormat().resolvedOptions().timeZone); @@ -226,8 +226,14 @@ const Event = (props) => { }, [timezone]); const onSubmit = async data => { + if (!data.name || data.name.length === 0) { + setFocus('name'); + return setError(t('event:form.errors.name_required')); + } + setIsLoginLoading(true); setError(null); + try { const response = await api.post(`/event/${id}/people/${data.name}`, { person: { @@ -270,6 +276,7 @@ const Event = (props) => { gtag('event', 'login', { 'event_category': 'event', }); + reset(); } }; @@ -321,7 +328,14 @@ const Event = (props) => { {user ? ( -

{t('event:form.signed_in', { name: user.name })}

+
+

{t('event:form.signed_in', { name: user.name })}

+ +
) : ( <>

{t('event:form.signed_out')}

diff --git a/crabfit-frontend/src/pages/Home/Home.tsx b/crabfit-frontend/src/pages/Home/Home.tsx index 2112135..c434b9e 100644 --- a/crabfit-frontend/src/pages/Home/Home.tsx +++ b/crabfit-frontend/src/pages/Home/Home.tsx @@ -265,7 +265,8 @@ const Home = ({ offline }) => {

Created by Ben Grant, Crab Fit is the modern-day solution to your group event planning debates.

The code for Crab Fit is open source, if you find any issues or want to contribute, you can visit the repository. By using Crab Fit you agree to the privacy policy.

-

Consider donating below if it helped you out so it can stay free for everyone. 🦀

+

{t('home:about.content.p6')}

+

{t('home:about.content.p5')}

diff --git a/crabfit-frontend/src/pages/Privacy/Privacy.tsx b/crabfit-frontend/src/pages/Privacy/Privacy.tsx index 3025cd6..3d8418e 100644 --- a/crabfit-frontend/src/pages/Privacy/Privacy.tsx +++ b/crabfit-frontend/src/pages/Privacy/Privacy.tsx @@ -1,6 +1,6 @@ -import { useEffect } from 'react'; +import { useEffect, useRef } from 'react'; import { useHistory } from 'react-router-dom'; -import { useTranslation, Trans } from 'react-i18next'; +import { useTranslation } from 'react-i18next'; import { Button, @@ -14,10 +14,14 @@ import { AboutSection, P, } from '../Home/homeStyle'; +import { Note } from './privacyStyle'; + +const translationDisclaimer = 'While the translated document is provided for your convenience, the English version as displayed at https://crab.fit is legally binding.'; const Privacy = () => { const { push } = useHistory(); - const { t } = useTranslation(['common', 'privacy']); + const { t, i18n } = useTranslation(['common', 'privacy']); + const contentRef = useRef(); useEffect(() => { document.title = `${t('privacy:name')} - Crab Fit`; @@ -31,54 +35,64 @@ const Privacy = () => {

{t('privacy:name')}

-

Crab Fit

-

{t('privacy:p1')}

-

{t('privacy:p2')}

-

{t('privacy:p3')}

-

{t('privacy:h1')}

-

{t('privacy:p4')}

-

{t('privacy:p5')}

-

+ {!i18n.language.startsWith('en') && ( +

+ {t('privacy:translate')} +

+ )} + +

Crab Fit

+
+

This SERVICE is provided by Benjamin Grant at no cost and is intended for use as is.

+

This page is used to inform visitors regarding the policies of the collection, use, and disclosure of Personal Information if using the Service.

+

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.

+ +

Information Collection and Use

+

The Service uses third party services that may collect information used to identify you.

+

Links to privacy policies of the third party service providers used by the Service:

-

-

{t('privacy:h2')}

-

{t('privacy:p6')}

+

Log Data

+

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.

-

{t('privacy:h3')}

-

{t('privacy:p7')}

-

{t('privacy:p8')}

+

Cookies

+

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.

+

Cookies are used by Google Analytics to track you across the web and provide anonymous statistics to improve the Service.

-

{t('privacy:h4')}

-

{t('privacy:p9')}

-

+

Service Providers

+

Third-party companies may be employed for the following reasons:

    -
  • {t('privacy:l1')}
  • -
  • {t('privacy:l2')}
  • -
  • {t('privacy:l3')}
  • -
  • {t('privacy:l4')}
  • +
  • To facilitate the Service
  • +
  • To provide the Service on our behalf
  • +
  • To perform Service-related services
  • +
  • To assist in analyzing how the Service is used
-

-

{t('privacy: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.

-

{t('privacy:h5')}

-

{t('privacy:p11')}

+

Security

+

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.

+ Events that are created will be automatically permanently erased from storage after 3 months of inactivity. -

{t('privacy:h6')}

-

{t('privacy:p12')}

+

Links to Other Sites

+

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.

-

{t('privacy:h7')}

-

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 contact us so that this information can be removed.

+

Children's Privacy

+

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 contact us using the details below so that this information can be removed.

-

{t('privacy:h8')}

-

{t('privacy:p14')}

-

{t('privacy:p15')}

+

Changes to This Privacy Policy

+

This Privacy Policy may be updated from time to time. Thus, you are advised to review this page periodically for any changes.

+

Last updated: 2021-06-16

-

{t('privacy:h9')}

-

If you have any questions or suggestions about the Privacy Policy, do not hesitate to contact us at crabfit@bengrant.dev.

+

Contact Us

+

If you have any questions or suggestions about the Privacy Policy, do not hesitate to contact us at contact@crab.fit.

+
diff --git a/crabfit-frontend/src/pages/Privacy/privacyStyle.ts b/crabfit-frontend/src/pages/Privacy/privacyStyle.ts new file mode 100644 index 0000000..7597810 --- /dev/null +++ b/crabfit-frontend/src/pages/Privacy/privacyStyle.ts @@ -0,0 +1,14 @@ +import styled from '@emotion/styled'; + +export const Note = styled.p` + background-color: ${props => props.theme.primaryBackground}; + border: 1px solid ${props => props.theme.primary}; + border-radius: 10px; + padding: 12px 16px; + margin: 16px 0; + box-sizing: border-box; + + & a { + color: ${props => props.theme.mode === 'light' ? props.theme.primaryDark : props.theme.primaryLight}; + } +`;