|
|
@ -7,3 +7,7 @@ cron:
|
|||
url: /tasks/legacyCleanup
|
||||
schedule: every tuesday 09:00
|
||||
target: api
|
||||
- description: "remove people with an event id that no longer exists"
|
||||
url: /tasks/removeOrphans
|
||||
schedule: 1st wednesday of month 09:00
|
||||
target: api
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ const updatePerson = require('./routes/updatePerson');
|
|||
|
||||
const taskCleanup = require('./routes/taskCleanup');
|
||||
const taskLegacyCleanup = require('./routes/taskLegacyCleanup');
|
||||
const taskRemoveOrphans = require('./routes/taskRemoveOrphans');
|
||||
|
||||
const app = express();
|
||||
const port = 8080;
|
||||
|
|
@ -53,6 +54,7 @@ app.patch('/event/:eventId/people/:personName', updatePerson);
|
|||
// Tasks
|
||||
app.get('/tasks/cleanup', taskCleanup);
|
||||
app.get('/tasks/legacyCleanup', taskLegacyCleanup);
|
||||
app.get('/tasks/removeOrphans', taskRemoveOrphans);
|
||||
|
||||
app.listen(port, () => {
|
||||
console.log(`Crabfit API listening at http://localhost:${port} in ${process.env.NODE_ENV === 'production' ? 'prod' : 'dev'} mode`)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,10 @@
|
|||
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')}`);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,10 @@
|
|||
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')}`);
|
||||
|
|
|
|||
46
crabfit-backend/routes/taskRemoveOrphans.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 orphan removal task at ${dayjs().format('h:mma D MMM YYYY')}`);
|
||||
|
||||
try {
|
||||
// Fetch people that are older than 3 months
|
||||
const peopleQuery = req.datastore.createQuery(req.types.person).filter('created', '<', threeMonthsAgo);
|
||||
let oldPeople = (await req.datastore.runQuery(peopleQuery))[0];
|
||||
|
||||
if (oldPeople && oldPeople.length > 0) {
|
||||
console.log(`Found ${oldPeople.length} people older than 3 months, checking for events`);
|
||||
|
||||
// Fetch events linked to the people discovered
|
||||
let peopleWithoutEvents = 0;
|
||||
await Promise.all(oldPeople.map(async (person) => {
|
||||
let event = (await req.datastore.get(req.datastore.key([req.types.event, person.eventId])))[0];
|
||||
|
||||
if (!event) {
|
||||
peopleWithoutEvents++;
|
||||
await req.datastore.delete(person[req.datastore.KEY]);
|
||||
}
|
||||
}));
|
||||
|
||||
if (peopleWithoutEvents > 0) {
|
||||
console.log(`Orphan removal successful: ${oldEventIds.length} events and ${peopleDiscovered} people removed`);
|
||||
res.sendStatus(200);
|
||||
} else {
|
||||
console.log(`Found 0 people without events, ending orphan removal`);
|
||||
res.sendStatus(404);
|
||||
}
|
||||
} else {
|
||||
console.log(`Found 0 people older than 3 months, ending orphan removal`);
|
||||
res.sendStatus(404);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
res.sendStatus(404);
|
||||
}
|
||||
};
|
||||
|
|
@ -243,3 +243,16 @@ paths:
|
|||
description: "Not found"
|
||||
400:
|
||||
description: "Not called from a cron job"
|
||||
"/tasks/removeOrphans":
|
||||
get:
|
||||
summary: "Deletes people if the event they were created under no longer exists"
|
||||
operationId: "taskRemoveOrphans"
|
||||
tags:
|
||||
- tasks
|
||||
responses:
|
||||
200:
|
||||
description: "OK"
|
||||
404:
|
||||
description: "Not found"
|
||||
400:
|
||||
description: "Not called from a cron job"
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "Crab Fit",
|
||||
"description": "Enter your availability to find a time that works for everyone!",
|
||||
"version": "1.1",
|
||||
"version": "1.2",
|
||||
"manifest_version": 2,
|
||||
|
||||
"author": "Ben Grant",
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 4.4 KiB After Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 620 B After Width: | Height: | Size: 416 B |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 841 B |
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 104 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 4.5 KiB After Width: | Height: | Size: 4.6 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 14 KiB |
|
|
@ -93,6 +93,7 @@ const App = () => {
|
|||
styles={theme => ({
|
||||
html: {
|
||||
scrollBehavior: 'smooth',
|
||||
'-webkit-print-color-adjust': 'exact',
|
||||
},
|
||||
body: {
|
||||
backgroundColor: theme.background,
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import relativeTime from 'dayjs/plugin/relativeTime';
|
|||
|
||||
import { useSettingsStore, useLocaleUpdateStore } from 'stores';
|
||||
|
||||
import { Legend, Center } from 'components';
|
||||
import { Legend } from 'components';
|
||||
import {
|
||||
Wrapper,
|
||||
ScrollWrapper,
|
||||
|
|
@ -29,6 +29,7 @@ import {
|
|||
People,
|
||||
Person,
|
||||
StyledMain,
|
||||
Info,
|
||||
} from './availabilityViewerStyle';
|
||||
|
||||
import locales from 'res/dayjs_locales';
|
||||
|
|
@ -162,10 +163,10 @@ const AvailabilityViewer = ({
|
|||
total={people.filter(p => p.availability.length > 0).length}
|
||||
onSegmentFocus={count => setFocusCount(count)}
|
||||
/>
|
||||
<Center style={{textAlign: 'center'}}>{t('event:group.info1')}</Center>
|
||||
<Info>{t('event:group.info1')}</Info>
|
||||
{people.length > 1 && (
|
||||
<>
|
||||
<Center style={{textAlign: 'center'}}>{t('event:group.info2')}</Center>
|
||||
<Info>{t('event:group.info2')}</Info>
|
||||
<People>
|
||||
{people.map((person, i) =>
|
||||
<Person
|
||||
|
|
|
|||
|
|
@ -220,3 +220,12 @@ export const Person = styled.button`
|
|||
border-color: ${props.theme.primary};
|
||||
`}
|
||||
`;
|
||||
|
||||
export const Info = styled.span`
|
||||
display: block;
|
||||
text-align: center;
|
||||
|
||||
@media print {
|
||||
display: none;
|
||||
}
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -121,4 +121,14 @@ export const Pressable = styled.button`
|
|||
transform: none;
|
||||
}
|
||||
`}
|
||||
|
||||
@media print {
|
||||
${props => !props.secondary && `
|
||||
box-shadow: 0 4px 0 0 ${props.secondaryColor || props.theme.primaryDark};
|
||||
`}
|
||||
|
||||
&::before {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -19,4 +19,8 @@ export const Wrapper = styled.footer`
|
|||
margin-bottom: 20px;
|
||||
}
|
||||
`}
|
||||
|
||||
@media print {
|
||||
display: none;
|
||||
}
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -62,4 +62,8 @@ export const Tagline = styled.span`
|
|||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
@media print {
|
||||
display: none;
|
||||
}
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import dayjs from 'dayjs';
|
|||
import relativeTime from 'dayjs/plugin/relativeTime';
|
||||
|
||||
import { AboutSection, StyledMain } from '../../pages/Home/homeStyle';
|
||||
import { Recent } from './recentsStyle';
|
||||
import { Wrapper, Recent } from './recentsStyle';
|
||||
|
||||
dayjs.extend(relativeTime);
|
||||
|
||||
|
|
@ -14,6 +14,7 @@ const Recents = ({ target }) => {
|
|||
const { t } = useTranslation(['home', 'common']);
|
||||
|
||||
return !!recents.length && (
|
||||
<Wrapper>
|
||||
<AboutSection id="recents">
|
||||
<StyledMain>
|
||||
<h2>{t('home:recently_visited')}</h2>
|
||||
|
|
@ -25,6 +26,7 @@ const Recents = ({ target }) => {
|
|||
))}
|
||||
</StyledMain>
|
||||
</AboutSection>
|
||||
</Wrapper>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,11 @@
|
|||
import styled from '@emotion/styled';
|
||||
|
||||
export const Wrapper = styled.div`
|
||||
@media print {
|
||||
display: none;
|
||||
}
|
||||
`;
|
||||
|
||||
export const Recent = styled.a`
|
||||
text-decoration: none;
|
||||
display: flex;
|
||||
|
|
|
|||
|
|
@ -33,6 +33,9 @@ export const OpenButton = styled.button`
|
|||
@media (prefers-reduced-motion: reduce) {
|
||||
transition: none;
|
||||
}
|
||||
@media print {
|
||||
display: none;
|
||||
}
|
||||
`;
|
||||
|
||||
export const Cover = styled.div`
|
||||
|
|
@ -81,6 +84,9 @@ export const Modal = styled.div`
|
|||
@media (prefers-reduced-motion: reduce) {
|
||||
transition: none;
|
||||
}
|
||||
@media print {
|
||||
display: none;
|
||||
}
|
||||
`;
|
||||
|
||||
export const Heading = styled.span`
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ export const Wrapper = styled.div`
|
|||
border-radius: 3px;
|
||||
width: 400px;
|
||||
box-sizing: border-box;
|
||||
max-width: calc(100% - 20px);
|
||||
max-width: calc(100% - 40px);
|
||||
box-shadow: 0 3px 6px 0 rgba(0,0,0,.3);
|
||||
|
||||
& h2 {
|
||||
|
|
@ -31,4 +31,5 @@ export const ButtonWrapper = styled.div`
|
|||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 16px;
|
||||
flex-wrap: wrap;
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -47,6 +47,7 @@ const Event = (props) => {
|
|||
const weekStart = useSettingsStore(state => state.weekStart);
|
||||
|
||||
const addRecent = useRecentsStore(state => state.addRecent);
|
||||
const removeRecent = useRecentsStore(state => state.removeRecent);
|
||||
const locale = useLocaleUpdateStore(state => state.locale);
|
||||
|
||||
const { t } = useTranslation(['common', 'event']);
|
||||
|
|
@ -86,13 +87,16 @@ const Event = (props) => {
|
|||
document.title = `${response.data.name} | Crab Fit`;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
if (e.status === 404) {
|
||||
removeRecent(id);
|
||||
}
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchEvent();
|
||||
}, [id, addRecent]);
|
||||
}, [id, addRecent, removeRecent]);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchPeople = async () => {
|
||||
|
|
@ -302,7 +306,7 @@ const Event = (props) => {
|
|||
}
|
||||
title={!!navigator.clipboard ? t('event:nav.title') : ''}
|
||||
>{copied ?? `https://crab.fit/${id}`}</ShareInfo>
|
||||
<ShareInfo isLoading={isLoading}>
|
||||
<ShareInfo isLoading={isLoading} className="instructions">
|
||||
{!!event?.name &&
|
||||
<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>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,6 +38,12 @@ export const EventDate = styled.span`
|
|||
border-radius: 3px;
|
||||
}
|
||||
`}
|
||||
|
||||
@media print {
|
||||
&::after {
|
||||
content: ' - ' attr(title);
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const LoginForm = styled.form`
|
||||
|
|
@ -61,6 +67,10 @@ export const LoginForm = styled.form`
|
|||
export const LoginSection = styled.section`
|
||||
background-color: ${props => props.theme.primaryBackground};
|
||||
padding: 10px 0;
|
||||
|
||||
@media print {
|
||||
display: none;
|
||||
}
|
||||
`;
|
||||
|
||||
export const Info = styled.p`
|
||||
|
|
@ -93,6 +103,12 @@ export const ShareInfo = styled.p`
|
|||
color: ${props.theme.mode === 'light' ? props.theme.primaryDark : props.theme.primaryLight};
|
||||
}
|
||||
`}
|
||||
|
||||
@media print {
|
||||
&.instructions {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const Tabs = styled.div`
|
||||
|
|
@ -100,6 +116,10 @@ export const Tabs = styled.div`
|
|||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 30px 0 20px;
|
||||
|
||||
@media print {
|
||||
display: none;
|
||||
}
|
||||
`;
|
||||
|
||||
export const Tab = styled.a`
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import {
|
|||
Step,
|
||||
FakeCalendar,
|
||||
FakeTimeRange,
|
||||
ButtonArea,
|
||||
} from './helpStyle';
|
||||
|
||||
const Help = () => {
|
||||
|
|
@ -82,11 +83,13 @@ const Help = () => {
|
|||
/>
|
||||
</StyledMain>
|
||||
|
||||
<ButtonArea>
|
||||
<AboutSection id="about">
|
||||
<StyledMain>
|
||||
<Center><Button onClick={() => push('/')}>{t('common:cta')}</Button></Center>
|
||||
</StyledMain>
|
||||
</AboutSection>
|
||||
</ButtonArea>
|
||||
|
||||
<Footer />
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -108,3 +108,9 @@ export const FakeTimeRange = styled.div`
|
|||
border-radius: 2px;
|
||||
}
|
||||
`;
|
||||
|
||||
export const ButtonArea = styled.div`
|
||||
@media print {
|
||||
display: none;
|
||||
}
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ import {
|
|||
|
||||
import api from 'services';
|
||||
import { detect_browser } from 'utils';
|
||||
import { useTWAStore } from 'stores';
|
||||
|
||||
import logo from 'res/logo.svg';
|
||||
import timezones from 'res/timezones.json';
|
||||
|
|
@ -63,6 +64,7 @@ const Home = ({ offline }) => {
|
|||
const [browser, setBrowser] = useState(undefined);
|
||||
const { push } = useHistory();
|
||||
const { t } = useTranslation(['common', 'home']);
|
||||
const isTWA = useTWAStore(state => state.TWA);
|
||||
|
||||
useEffect(() => {
|
||||
const fetch = async () => {
|
||||
|
|
@ -231,6 +233,7 @@ const Home = ({ offline }) => {
|
|||
</Stat>
|
||||
</Stats>
|
||||
<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" rel="help">Learn more about how to Crab Fit</Link>.</Trans></P>
|
||||
{isTWA !== true && (
|
||||
<ButtonArea>
|
||||
{['chrome', 'firefox', 'safari'].includes(browser) && (
|
||||
<Button
|
||||
|
|
@ -263,6 +266,7 @@ const Home = ({ offline }) => {
|
|||
secondary
|
||||
>{t('home:about.android_app')}</Button>
|
||||
</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>{t('home:about.content.p6')}</P>
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ import {
|
|||
AboutSection,
|
||||
P,
|
||||
} from '../Home/homeStyle';
|
||||
import { Note } from './privacyStyle';
|
||||
import { Note, ButtonArea } 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.';
|
||||
|
||||
|
|
@ -58,9 +58,9 @@ const Privacy = () => {
|
|||
<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>
|
||||
<P as="ul">
|
||||
<li><a href="https://www.google.com/policies/privacy/" target="blank">Google Play Services</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>
|
||||
|
|
@ -71,12 +71,12 @@ const Privacy = () => {
|
|||
|
||||
<h2>Service Providers</h2>
|
||||
<P>Third-party companies may be employed for the following reasons:</P>
|
||||
<ul>
|
||||
<P as="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>
|
||||
</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>
|
||||
|
||||
<h2>Security</h2>
|
||||
|
|
@ -98,11 +98,13 @@ const Privacy = () => {
|
|||
</div>
|
||||
</StyledMain>
|
||||
|
||||
<AboutSection id="about">
|
||||
<ButtonArea>
|
||||
<AboutSection>
|
||||
<StyledMain>
|
||||
<Center><Button onClick={() => push('/')}>{t('common:cta')}</Button></Center>
|
||||
</StyledMain>
|
||||
</AboutSection>
|
||||
</ButtonArea>
|
||||
|
||||
<Footer />
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -7,8 +7,16 @@ export const Note = styled.p`
|
|||
padding: 12px 16px;
|
||||
margin: 16px 0;
|
||||
box-sizing: border-box;
|
||||
font-weight: 500;
|
||||
line-height: 1.6em;
|
||||
|
||||
& a {
|
||||
color: ${props => props.theme.mode === 'light' ? props.theme.primaryDark : props.theme.primaryLight};
|
||||
}
|
||||
`;
|
||||
|
||||
export const ButtonArea = styled.div`
|
||||
@media print {
|
||||
display: none;
|
||||
}
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -1,43 +1 @@
|
|||
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" width="512" height="512" viewBox="0 0 512 512">
|
||||
<defs>
|
||||
<style>
|
||||
.cls-1 {
|
||||
fill: #f4bb60;
|
||||
}
|
||||
|
||||
.cls-2 {
|
||||
fill: #f79e00;
|
||||
}
|
||||
|
||||
.cls-3 {
|
||||
fill: #f47f00;
|
||||
opacity: 0.5;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<g>
|
||||
<path class="cls-1" d="M183.85,311.62a123,123,0,0,1-123-123h0a123,123,0,0,1,123-123h64.32L214.4,99.33h32.46c0,79.47-108.36,32.47-136.43,50.75C76.39,172.24,183.85,311.62,183.85,311.62Z"/>
|
||||
<g>
|
||||
<rect class="cls-2" x="267.75" y="200.99" width="20.89" height="57.44" rx="10.44"/>
|
||||
<rect class="cls-2" x="225.97" y="200.99" width="20.89" height="57.44" rx="10.44"/>
|
||||
</g>
|
||||
<g>
|
||||
<rect class="cls-1" x="21" y="326.33" width="107.06" height="23.5" rx="11.75"/>
|
||||
<rect class="cls-1" x="28.83" y="289.77" width="107.06" height="23.5" rx="11.75" transform="translate(91.34 -10.92) rotate(16.92)"/>
|
||||
<rect class="cls-1" x="26.22" y="362.88" width="107.06" height="23.5" rx="11.75" transform="matrix(0.95, -0.31, 0.31, 0.95, -110.74, 42.33)"/>
|
||||
<rect class="cls-1" x="45.81" y="394.21" width="107.06" height="23.5" rx="11.75" transform="translate(-215.65 131.26) rotate(-35.16)"/>
|
||||
</g>
|
||||
<g>
|
||||
<rect class="cls-1" x="383.94" y="326.33" width="107.06" height="23.5" rx="11.75" transform="translate(874.94 676.15) rotate(-180)"/>
|
||||
<rect class="cls-1" x="376.11" y="289.77" width="107.06" height="23.5" rx="11.75" transform="translate(928.45 464.91) rotate(163.08)"/>
|
||||
<rect class="cls-1" x="378.72" y="362.88" width="107.06" height="23.5" rx="11.75" transform="translate(729.24 863.49) rotate(-162.19)"/>
|
||||
<rect class="cls-1" x="359.14" y="394.21" width="107.06" height="23.5" rx="11.75" transform="translate(516.28 975.5) rotate(-144.84)"/>
|
||||
</g>
|
||||
<path class="cls-2" d="M214.4,99.33V65.56H183.85a123,123,0,0,0-123,123h0C74.53,116.13,113.69,99.33,214.4,99.33Z"/>
|
||||
<path class="cls-1" d="M331.42,311.62a123,123,0,0,0,123-123h0a123,123,0,0,0-123-123H267.1l33.77,33.77H268.4c0,79.47,108.36,32.47,136.43,50.75C438.87,172.24,331.42,311.62,331.42,311.62Z"/>
|
||||
<path class="cls-2" d="M300.87,99.33V65.56h30.55a123,123,0,0,1,123,123h0C440.74,116.13,401.57,99.33,300.87,99.33Z"/>
|
||||
<rect class="cls-2" x="83.67" y="229.71" width="347.28" height="216.72" rx="108.36"/>
|
||||
<circle class="cls-3" cx="219.44" cy="338.08" r="69.19"/>
|
||||
<circle class="cls-3" cx="295.17" cy="338.08" r="69.19"/>
|
||||
</g>
|
||||
</svg>
|
||||
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><defs><style>.cls-1,.cls-4{fill:#f79e00;}.cls-2,.cls-4{isolation:isolate;}.cls-3{fill:#f48600;}</style></defs><path class="cls-1" d="M181.11,311a123,123,0,0,1-123-123h0a123,123,0,0,1,123-123h64.32L211.66,98.73h32.46c0,79.47-108.36,32.47-136.43,50.75C73.65,171.64,181.11,311,181.11,311Z"/><path class="cls-1" d="M404,149.48C375.94,131.2,267.58,178.2,267.58,98.73H300L266.27,65h64.32a123,123,0,0,1,123,123h0a123,123,0,0,1-123,123S438.05,171.64,404,149.48Z"/><rect class="cls-1" x="266.27" y="200.39" width="20.89" height="57.44" rx="10.44"/><rect class="cls-1" x="224.49" y="200.39" width="20.89" height="57.44" rx="10.44"/><path class="cls-1" d="M190.55,229.11H321.11A108.35,108.35,0,0,1,429.47,337.47h0A108.36,108.36,0,0,1,321.11,445.83H190.55A108.37,108.37,0,0,1,82.19,337.47h0A108.36,108.36,0,0,1,190.55,229.11Z"/><g class="cls-2"><path class="cls-3" d="M293.69,268.29a68.83,68.83,0,0,0-37.86,11.29,69.19,69.19,0,1,0,0,115.8,69.19,69.19,0,1,0,37.86-127.09Z"/></g><ellipse class="cls-4" cx="255.83" cy="337.48" rx="31.32" ry="57.9"/><path class="cls-1" d="M402.77,386.23c36,12.31,66,40.07,59.44,60.75C440,431.17,413.11,416.91,389,409.83Z"/><path class="cls-1" d="M420.05,345.89c37.37,7.15,71,30.45,67.35,51.85-24.24-12.55-52.82-22.92-77.67-26.57Z"/><path class="cls-1" d="M417.26,303.44c38.05.39,75.26,17.34,75.5,39-26.08-8-56.05-13.16-81.15-12.32Z"/><path class="cls-1" d="M109.23,386.23c-36,12.31-66,40.07-59.44,60.75C72,431.17,98.89,416.91,123,409.83Z"/><path class="cls-1" d="M92,345.89C54.58,353,21,376.34,24.6,397.74c24.24-12.55,52.82-22.92,77.67-26.57Z"/><path class="cls-1" d="M94.74,303.44c-38,.39-75.26,17.34-75.5,39,26.08-8,56.05-13.16,81.15-12.32Z"/></svg>
|
||||
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 1.7 KiB |