Merge pull request #34 from GRA0007/dev
Delete events older than 3 months
This commit is contained in:
commit
868c82da6a
|
|
@ -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.
|
||||
|
|
|
|||
7
crabfit-backend/cron.yaml
Normal file
7
crabfit-backend/cron.yaml
Normal file
|
|
@ -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
|
||||
|
|
@ -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`)
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,18 +5,23 @@ module.exports = async (req, res) => {
|
|||
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);
|
||||
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,
|
||||
eventCount,
|
||||
personCount,
|
||||
version: package.version,
|
||||
});
|
||||
};
|
||||
|
|
|
|||
46
crabfit-backend/routes/taskCleanup.js
Normal file
46
crabfit-backend/routes/taskCleanup.js
Normal file
|
|
@ -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);
|
||||
}
|
||||
};
|
||||
68
crabfit-backend/routes/taskLegacyCleanup.js
Normal file
68
crabfit-backend/routes/taskLegacyCleanup.js
Normal file
|
|
@ -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);
|
||||
}
|
||||
};
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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": {
|
||||
|
|
|
|||
|
|
@ -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</2>.",
|
||||
"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>.",
|
||||
"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",
|
||||
|
|
|
|||
|
|
@ -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</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>crabfit@bengrant.dev</1>."
|
||||
"translate": "View in your language"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 }) => {
|
|||
<Title>
|
||||
<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>)
|
||||
</Title>
|
||||
<Options>
|
||||
{calendars !== undefined && !calendars.every(c => c.checked) && (
|
||||
/* eslint-disable-next-line */
|
||||
<a href="#" onClick={e => {
|
||||
<LinkButton type="button" onClick={e => {
|
||||
e.preventDefault();
|
||||
setCalendars(calendars.map(c => ({...c, checked: true})));
|
||||
}}>{t('event:you.google_cal.select_all')}</a>
|
||||
}}>{t('event:you.google_cal.select_all')}</LinkButton>
|
||||
)}
|
||||
{calendars !== undefined && calendars.every(c => c.checked) && (
|
||||
/* eslint-disable-next-line */
|
||||
<a href="#" onClick={e => {
|
||||
<LinkButton type="button" onClick={e => {
|
||||
e.preventDefault();
|
||||
setCalendars(calendars.map(c => ({...c, checked: false})));
|
||||
}}>{t('event:you.google_cal.select_none')}</a>
|
||||
}}>{t('event:you.google_cal.select_none')}</LinkButton>
|
||||
)}
|
||||
</Options>
|
||||
{calendars !== undefined ? calendars.map(calendar => (
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -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 }) => {
|
|||
<Title>
|
||||
<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>)
|
||||
</Title>
|
||||
<Options>
|
||||
{calendars !== undefined && !calendars.every(c => c.checked) && (
|
||||
/* eslint-disable-next-line */
|
||||
<a href="#" onClick={e => {
|
||||
<LinkButton type="button" onClick={e => {
|
||||
e.preventDefault();
|
||||
setCalendars(calendars.map(c => ({...c, checked: true})));
|
||||
}}>{t('event:you.google_cal.select_all')}</a>
|
||||
}}>{t('event:you.google_cal.select_all')}</LinkButton>
|
||||
)}
|
||||
{calendars !== undefined && calendars.every(c => c.checked) && (
|
||||
/* eslint-disable-next-line */
|
||||
<a href="#" onClick={e => {
|
||||
<LinkButton type="button" onClick={e => {
|
||||
e.preventDefault();
|
||||
setCalendars(calendars.map(c => ({...c, checked: false})));
|
||||
}}>{t('event:you.google_cal.select_none')}</a>
|
||||
}}>{t('event:you.google_cal.select_none')}</LinkButton>
|
||||
)}
|
||||
</Options>
|
||||
{calendars !== undefined ? calendars.map(calendar => (
|
||||
|
|
|
|||
|
|
@ -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) => {
|
|||
<LoginSection id="login">
|
||||
<StyledMain>
|
||||
{user ? (
|
||||
<h2>{t('event:form.signed_in', { name: user.name })}</h2>
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', margin: '20px 0', flexWrap: 'wrap', gap: '10px' }}>
|
||||
<h2 style={{ margin: 0 }}>{t('event:form.signed_in', { name: user.name })}</h2>
|
||||
<Button small onClick={() => {
|
||||
setTab('group');
|
||||
setUser(null);
|
||||
setPassword(null);
|
||||
}}>{t('event:form.logout_button')}</Button>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<h2>{t('event:form.signed_out')}</h2>
|
||||
|
|
|
|||
|
|
@ -265,7 +265,8 @@ const Home = ({ offline }) => {
|
|||
</ButtonArea>
|
||||
<P><Trans i18nKey="home:about.content.p3">Created by <a href="https://bengrant.dev" target="_blank" rel="noreferrer noopener author">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 noopener">repository</a>. By using Crab Fit you agree to the <Link to="/privacy" rel="license">privacy policy</Link>.</Trans></P>
|
||||
<P><Trans i18nKey="home:about.content.p5">Consider donating below if it helped you out so it can stay free for everyone. 🦀</Trans></P>
|
||||
<P>{t('home:about.content.p6')}</P>
|
||||
<P>{t('home:about.content.p5')}</P>
|
||||
</StyledMain>
|
||||
</AboutSection>
|
||||
|
||||
|
|
|
|||
|
|
@ -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 = () => {
|
|||
|
||||
<StyledMain>
|
||||
<h1>{t('privacy:name')}</h1>
|
||||
|
||||
{!i18n.language.startsWith('en') && (
|
||||
<p>
|
||||
<a
|
||||
href={`https://translate.google.com/?sl=en&tl=${i18n.language.substring(0, 2)}&text=${encodeURIComponent(`${translationDisclaimer}\n\n${contentRef.current?.innerText}`)}&op=translate`}
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
>{t('privacy:translate')}</a>
|
||||
</p>
|
||||
)}
|
||||
|
||||
<h3>Crab Fit</h3>
|
||||
<P>{t('privacy:p1')}</P>
|
||||
<P>{t('privacy:p2')}</P>
|
||||
<P>{t('privacy:p3')}</P>
|
||||
<div ref={contentRef}>
|
||||
<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>
|
||||
|
||||
<h2>{t('privacy:h1')}</h2>
|
||||
<P>{t('privacy:p4')}</P>
|
||||
<P>{t('privacy:p5')}</P>
|
||||
<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>
|
||||
<ul>
|
||||
<li><a href="https://www.google.com/policies/privacy/" target="blank">{t('privacy:link')}</a></li>
|
||||
<li><a href="https://www.google.com/policies/privacy/" target="blank">Google Play Services</a></li>
|
||||
</ul>
|
||||
</P>
|
||||
|
||||
<h2>{t('privacy:h2')}</h2>
|
||||
<P>{t('privacy:p6')}</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:h3')}</h2>
|
||||
<P>{t('privacy:p7')}</P>
|
||||
<P>{t('privacy:p8')}</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:h4')}</h2>
|
||||
<P>{t('privacy:p9')}</P>
|
||||
<P>
|
||||
<h2>Service Providers</h2>
|
||||
<P>Third-party companies may be employed for the following reasons:</P>
|
||||
<ul>
|
||||
<li>{t('privacy:l1')}</li>
|
||||
<li>{t('privacy:l2')}</li>
|
||||
<li>{t('privacy:l3')}</li>
|
||||
<li>{t('privacy:l4')}</li>
|
||||
<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>
|
||||
</ul>
|
||||
</P>
|
||||
<P>{t('privacy:p10')}</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>
|
||||
|
||||
<h2>{t('privacy:h5')}</h2>
|
||||
<P>{t('privacy:p11')}</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>
|
||||
<Note>Events that are created will be automatically permanently erased from storage after <strong>3 months</strong> of inactivity.</Note>
|
||||
|
||||
<h2>{t('privacy:h6')}</h2>
|
||||
<P>{t('privacy:p12')}</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: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:crabfit@bengrant.dev">contact us</a> so that this information can be removed.</Trans></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 contact us using the details below so that this information can be removed.</P>
|
||||
|
||||
<h2>{t('privacy:h8')}</h2>
|
||||
<P>{t('privacy:p14')}</P>
|
||||
<P>{t('privacy:p15')}</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>Last updated: 2021-06-16</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:crabfit@bengrant.dev">crabfit@bengrant.dev</a>.</Trans></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:contact@crab.fit">contact@crab.fit</a>.</P>
|
||||
</div>
|
||||
</StyledMain>
|
||||
|
||||
<AboutSection id="about">
|
||||
|
|
|
|||
14
crabfit-frontend/src/pages/Privacy/privacyStyle.ts
Normal file
14
crabfit-frontend/src/pages/Privacy/privacyStyle.ts
Normal file
|
|
@ -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};
|
||||
}
|
||||
`;
|
||||
Loading…
Reference in a new issue