Merge pull request #280 from GRA0007/feat/cache-availability

Save availability and sync with other events
This commit is contained in:
Benji Grant 2023-06-18 23:24:18 +10:00 committed by GitHub
commit 2ea9679974
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 258 additions and 224 deletions

View file

@ -167,6 +167,7 @@ const EventAvailabilities = ({ event }: EventAvailabilitiesProps) => {
people={people} people={people}
table={table} table={table}
/> : user && <AvailabilityEditor /> : user && <AvailabilityEditor
eventId={event?.id}
times={expandedTimes} times={expandedTimes}
timezone={timezone} timezone={timezone}
value={user.availability} value={user.availability}
@ -174,6 +175,15 @@ const EventAvailabilities = ({ event }: EventAvailabilitiesProps) => {
if (!event) return if (!event) return
const oldAvailability = [...user.availability] const oldAvailability = [...user.availability]
setUser({ ...user, availability }) setUser({ ...user, availability })
addRecent({
id: event.id,
name: event.name,
created_at: event.created_at,
user: availability.length > 0 ? {
name: user.name,
availability,
} : undefined,
})
updatePerson(event.id, user.name, { availability }, password) updatePerson(event.id, user.name, { availability }, password)
.catch(e => { .catch(e => {
console.warn(e) console.warn(e)

View file

@ -135,3 +135,11 @@ a:focus-visible {
*::-webkit-scrollbar-thumb:active { *::-webkit-scrollbar-thumb:active {
background: var(--secondary); background: var(--secondary);
} }
input[type=checkbox], input[type=radio] {
accent-color: var(--primary);
}
input[type=checkbox]:focus-visible, input[type=radio]:focus-visible {
outline: var(--focus-ring);
outline-offset: 2px;
}

View file

@ -2,16 +2,18 @@ import { Fragment, useCallback, useEffect, useRef, useState } from 'react'
import Button from '/src/components/Button/Button' import Button from '/src/components/Button/Button'
import Content from '/src/components/Content/Content' import Content from '/src/components/Content/Content'
import GoogleCalendar from '/src/components/GoogleCalendar/GoogleCalendar'
import { usePalette } from '/src/hooks/usePalette' import { usePalette } from '/src/hooks/usePalette'
import { useTranslation } from '/src/i18n/client' import { useTranslation } from '/src/i18n/client'
import { calculateTable, makeClass, parseSpecificDate } from '/src/utils' import { calculateTable, makeClass, parseSpecificDate } from '/src/utils'
import styles from './AvailabilityEditor.module.scss' import styles from './AvailabilityEditor.module.scss'
import GoogleCalendar from './components/GoogleCalendar/GoogleCalendar'
import RecentEvents from './components/RecentEvents/RecentEvents'
import viewerStyles from '../AvailabilityViewer/AvailabilityViewer.module.scss' import viewerStyles from '../AvailabilityViewer/AvailabilityViewer.module.scss'
import Skeleton from '../AvailabilityViewer/components/Skeleton/Skeleton' import Skeleton from '../AvailabilityViewer/components/Skeleton/Skeleton'
interface AvailabilityEditorProps { interface AvailabilityEditorProps {
eventId?: string
times: string[] times: string[]
timezone: string timezone: string
value: string[] value: string[]
@ -19,7 +21,7 @@ interface AvailabilityEditorProps {
table?: ReturnType<typeof calculateTable> table?: ReturnType<typeof calculateTable>
} }
const AvailabilityEditor = ({ times, timezone, value = [], onChange, table }: AvailabilityEditorProps) => { const AvailabilityEditor = ({ eventId, times, timezone, value = [], onChange, table }: AvailabilityEditorProps) => {
const { t } = useTranslation('event') const { t } = useTranslation('event')
// Ref and state required to rerender but also access static version in callbacks // Ref and state required to rerender but also access static version in callbacks
@ -74,6 +76,11 @@ const AvailabilityEditor = ({ times, timezone, value = [], onChange, table }: Av
times={times} times={times}
onImport={onChange} onImport={onChange}
/> />
<RecentEvents
eventId={eventId}
times={times}
onImport={onChange}
/>
</div> </div>
</Content>} </Content>}

View file

@ -0,0 +1,63 @@
.wrapper {
width: 100%;
}
.title {
display: flex;
align-items: center;
& strong {
margin-right: 1ex;
}
}
.icon {
height: 24px;
width: 24px;
margin-right: 12px;
}
.linkButton {
font: inherit;
color: var(--primary);
border: 0;
background: none;
text-decoration: underline;
padding: 0;
margin: 0;
display: inline;
cursor: pointer;
appearance: none;
border-radius: .2em;
&:focus-visible {
outline: var(--focus-ring);
outline-offset: 2px;
}
}
.options {
font-size: 14px;
padding: 0 0 5px;
}
.item {
display: flex;
margin-block: .5em;
}
.name {
margin-left: .6em;
font-size: 15px;
font-weight: 500;
display: flex;
flex-direction: column;
align-items: flex-start;
}
.info {
font-size: 14px;
opacity: .6;
font-weight: 500;
padding: 14px 0 10px;
}

View file

@ -4,7 +4,7 @@ import { Temporal } from '@js-temporal/polyfill'
import Button from '/src/components/Button/Button' import Button from '/src/components/Button/Button'
import { useTranslation } from '/src/i18n/client' import { useTranslation } from '/src/i18n/client'
import googleLogo from '/src/res/google.svg' import GoogleIcon from '/src/res/GoogleIcon'
import { allowUrlToWrap, parseSpecificDate } from '/src/utils' import { allowUrlToWrap, parseSpecificDate } from '/src/utils'
import styles from './GoogleCalendar.module.scss' import styles from './GoogleCalendar.module.scss'
@ -119,14 +119,14 @@ const GoogleCalendar = ({ timezone, timeStart, timeEnd, times, onImport }: Googl
isLoading={canLoad} isLoading={canLoad}
surfaceColor="#4286F5" surfaceColor="#4286F5"
shadowColor="#3367BD" shadowColor="#3367BD"
icon={<img aria-hidden="true" src={googleLogo.src} alt="" />} icon={<GoogleIcon aria-hidden="true" />}
> >
{t('you.google_cal')} {t('you.google_cal')}
</Button>} </Button>}
{calendars && <div className={styles.wrapper}> {calendars && <div className={styles.wrapper}>
<p className={styles.title}> <p className={styles.title}>
<img src={googleLogo.src} alt="" className={styles.icon} /> <GoogleIcon className={styles.icon} />
<strong>{t('you.google_cal')}</strong> <strong>{t('you.google_cal')}</strong>
(<button (<button
className={styles.linkButton} className={styles.linkButton}
@ -148,17 +148,15 @@ const GoogleCalendar = ({ timezone, timeStart, timeEnd, times, onImport }: Googl
>{t('you.select_none')}</button>} >{t('you.select_none')}</button>}
</div> </div>
{calendars.map(calendar => <div key={calendar.id}> {calendars.map(calendar => <div key={calendar.id} className={styles.item}>
<input <input
className={styles.checkbox}
type="checkbox" type="checkbox"
id={calendar.id} id={calendar.id}
color={calendar.color} style={{ accentColor: calendar.color }}
checked={calendar.isChecked} checked={calendar.isChecked}
onChange={() => setCalendars(calendars.map(c => c.id === calendar.id ? { ...c, isChecked: !c.isChecked } : c))} onChange={() => setCalendars(calendars.map(c => c.id === calendar.id ? { ...c, isChecked: !c.isChecked } : c))}
/> />
<label htmlFor={calendar.id} style={{ '--cal-color': calendar.color } as React.CSSProperties} /> <label className={styles.name} htmlFor={calendar.id} title={calendar.description}>{allowUrlToWrap(calendar.name)}</label>
<label className={styles.calendarName} htmlFor={calendar.id} title={calendar.description}>{allowUrlToWrap(calendar.name)}</label>
</div>)} </div>)}
<div className={styles.info}>{t('you.integration.info')}</div> <div className={styles.info}>{t('you.integration.info')}</div>

View file

@ -1,3 +1,4 @@
/* TODO:
import { PublicClientApplication } from '@azure/msal-browser' import { PublicClientApplication } from '@azure/msal-browser'
import { Client } from '@microsoft/microsoft-graph-client' import { Client } from '@microsoft/microsoft-graph-client'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
@ -226,3 +227,4 @@ const OutlookCalendar = ({ timeZone, timeMin, timeMax, onImport }) => {
} }
export default OutlookCalendar export default OutlookCalendar
*/

View file

@ -0,0 +1,91 @@
import { useCallback, useMemo, useState } from 'react'
import { Temporal } from '@js-temporal/polyfill'
import Button from '/src/components/Button/Button'
import { useTranslation } from '/src/i18n/client'
import CrabIcon from '/src/res/CrabIcon'
import { useStore } from '/src/stores'
import useRecentsStore, { RecentEvent } from '/src/stores/recentsStore'
import { relativeTimeFormat } from '/src/utils'
import styles from '../GoogleCalendar/GoogleCalendar.module.scss'
interface RecentEventsProps {
eventId?: string
times: string[]
onImport: (availability: string[]) => void
}
const hasAvailability = (event: RecentEvent): event is Required<RecentEvent> => event.user !== undefined
const RecentEvents = ({ eventId, times, onImport }: RecentEventsProps) => {
const { t, i18n } = useTranslation('event')
const allRecents = useStore(useRecentsStore, state => state.recents)
const recents = useMemo(() =>
allRecents
?.filter(hasAvailability)
.filter(e => e.id !== eventId && e.user.availability.some(a => times.includes(a))) ?? [],
[allRecents]
)
const [isOpen, setIsOpen] = useState(false)
const [selected, setSelected] = useState<string>()
const importAvailability = useCallback(() => {
if (selected === undefined || recents.length === 0) return
const selectedRecent = recents.find(r => r.id === selected)
if (!selectedRecent) return
onImport(selectedRecent.user.availability.filter(a => times.includes(a)))
}, [selected, recents])
// No recents
if (recents.length === 0) return null
return <>
{!isOpen && <Button
onClick={() => setIsOpen(true)}
icon={<CrabIcon aria-hidden="true" />}
>
{t('you.recent_event')}
</Button>}
{isOpen && <div className={styles.wrapper}>
<p className={styles.title}>
<CrabIcon className={styles.icon} />
<strong>{t('you.recent_event')}</strong>
(<button
className={styles.linkButton}
type="button"
onClick={() => setIsOpen(false)}
>{t('you.integration.close')}</button>)
</p>
{recents.map(recent => <div
key={recent.id}
title={Temporal.Instant.fromEpochSeconds(recent.created_at).toLocaleString(i18n.language, { dateStyle: 'long' })}
className={styles.item}
>
<input type="radio" name="recents" value={recent.id} id={recent.id} onChange={() => setSelected(recent.id)} checked={selected === recent.id} />
<label
htmlFor={recent.id}
className={styles.name}
>
<span>{recent.name} ({recent.user.name})</span>
<span style={{ opacity: .7, fontSize: '.7em' }}>{relativeTimeFormat(Temporal.Instant.fromEpochSeconds(recent.created_at), i18n.language)}</span>
</label>
</div>)}
<div className={styles.info}>{t('you.integration.info')}</div>
<Button
isSmall
disabled={selected === undefined}
onClick={importAvailability}
>{t('you.integration.button')}</Button>
</div>}
</>
}
export default RecentEvents

View file

@ -1,134 +0,0 @@
.wrapper {
width: 100%;
& > div {
display: flex;
margin-block: 2px;
}
}
.title {
display: flex;
align-items: center;
& strong {
margin-right: 1ex;
}
}
.icon {
height: 24px;
width: 24px;
margin-right: 12px;
@media (prefers-color-scheme: light) {
filter: invert(1);
}
:global(.light) & {
filter: invert(1);
}
}
.linkButton {
font: inherit;
color: var(--primary);
border: 0;
background: none;
text-decoration: underline;
padding: 0;
margin: 0;
display: inline;
cursor: pointer;
appearance: none;
border-radius: .2em;
&:focus-visible {
outline: var(--focus-ring);
outline-offset: 2px;
}
}
.options {
font-size: 14px;
padding: 0 0 5px;
}
.checkbox {
height: 0px;
width: 0px;
margin: 0;
padding: 0;
border: 0;
background: 0;
font-size: 0;
transform: scale(0);
position: absolute;
&:checked + label::after {
opacity: 1;
transform: scale(1);
}
&[disabled] + label {
opacity: .6;
}
&[disabled] + label::after {
border: 2px solid var(--text);
background-color: var(--text);
}
& + label {
display: inline-block;
height: 24px;
width: 24px;
min-width: 24px;
position: relative;
border-radius: 3px;
transition: background-color 0.2s, box-shadow 0.2s;
cursor: pointer;
&::before {
content: '';
display: inline-block;
height: 14px;
width: 14px;
border: 2px solid var(--text);
border-radius: 2px;
position: absolute;
top: 3px;
left: 3px;
}
&::after {
content: '';
display: inline-block;
height: 14px;
width: 14px;
border: 2px solid var(--cal-color, var(--primary));
background-color: var(--cal-color, var(--primary));
border-radius: 2px;
position: absolute;
top: 3px;
left: 3px;
background-image: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZlcnNpb249IjEuMSIgIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBmaWxsPSIjRkZGRkZGIiBkPSJNMjEsN0w5LDE5TDMuNSwxMy41TDQuOTEsMTIuMDlMOSwxNi4xN0wxOS41OSw1LjU5TDIxLDdaIiAvPjwvc3ZnPg==');
background-size: 16px;
background-position: center;
background-repeat: no-repeat;
opacity: 0;
transform: scale(.5);
transition: opacity 0.15s, transform 0.15s;
}
}
}
.calendarName {
margin-left: .6em;
font-size: 15px;
font-weight: 500;
line-height: 24px;
}
.info {
font-size: 14px;
opacity: .6;
font-weight: 500;
padding: 14px 0 10px;
}

View file

@ -41,6 +41,7 @@
.logo { .logo {
width: 2.5rem; width: 2.5rem;
height: 2.5rem;
margin-right: 16px; margin-right: 16px;
} }
@ -113,6 +114,7 @@
.bigLogo { .bigLogo {
width: 80px; width: 80px;
height: 80px;
transition: transform .15s; transition: transform .15s;
animation: jelly .5s 1 .05s; animation: jelly .5s 1 .05s;
user-select: none; user-select: none;

View file

@ -27,12 +27,12 @@ const Header = async ({ isFull, isSmall }: HeaderProps) => {
return <header className={styles.header} data-small={isSmall}> return <header className={styles.header} data-small={isSmall}>
{isFull ? <> {isFull ? <>
{!isSmall && <img className={styles.bigLogo} src={logo.src} alt="" />} {!isSmall && <img className={styles.bigLogo} src={logo.src} height={512} width={512} alt="" />}
<span className={makeClass(styles.subtitle, samuraiBob.className, !/^[A-Za-z ]+$/.test(t('home:create')) && styles.hasAltChars)}>{t('home:create')}</span> <span className={makeClass(styles.subtitle, samuraiBob.className, !/^[A-Za-z ]+$/.test(t('home:create')) && styles.hasAltChars)}>{t('home:create')}</span>
<h1 className={makeClass(styles.bigTitle, molot.className)}>CRAB FIT</h1> <h1 className={makeClass(styles.bigTitle, molot.className)}>CRAB FIT</h1>
</> : <Link href="/" className={styles.link}> </> : <Link href="/" className={styles.link}>
<div className={styles.top}> <div className={styles.top}>
<img className={styles.logo} src={logo.src} alt="" /> <img className={styles.logo} src={logo.src} height={512} width={512} alt="" />
<span className={makeClass(styles.title, molot.className)}>CRAB FIT</span> <span className={makeClass(styles.title, molot.className)}>CRAB FIT</span>
</div> </div>
<span className={styles.tagline}>{t('common:tagline')}</span> <span className={styles.tagline}>{t('common:tagline')}</span>

View file

@ -19,7 +19,7 @@ const Recents = () => {
return recents?.length ? <Section id="recents"> return recents?.length ? <Section id="recents">
<Content> <Content>
<h2>{t('home:recently_visited')}</h2> <h2>{t('home:recently_visited')}</h2>
{recents.map(event => ( {recents.slice(0, 5).map(event => (
<Link className={styles.recent} href={`/${event.id}`} key={event.id}> <Link className={styles.recent} href={`/${event.id}`} key={event.id}>
<span className={styles.name}>{event.name}</span> <span className={styles.name}>{event.name}</span>
<span <span

View file

@ -31,6 +31,7 @@
img { img {
width: 100%; width: 100%;
height: auto;
display: block; display: block;
border-radius: inherit; border-radius: inherit;
background-color: #CCC; background-color: #CCC;

View file

@ -3,7 +3,7 @@
import { useState } from 'react' import { useState } from 'react'
import { useTranslation } from '/src/i18n/client' import { useTranslation } from '/src/i18n/client'
import video_thumb from '/src/res/video_thumb.jpg' import video_thumb from '/src/res/video_thumb.webp'
import styles from './Video.module.scss' import styles from './Video.module.scss'
@ -34,7 +34,7 @@ const Video = () => {
setIsPlaying(true) setIsPlaying(true)
}} }}
> >
<img src={video_thumb.src} alt={t('video.button')} /> <img src={video_thumb.src} width={video_thumb.width} height={video_thumb.height} alt={t('video.button')} />
<span>{t('video.button')}</span> <span>{t('video.button')}</span>
</a> </a>
) )

View file

@ -60,8 +60,10 @@
"google_cal": "Sync with Google Calendar", "google_cal": "Sync with Google Calendar",
"outlook_cal": "Sync with Outlook Calendar", "outlook_cal": "Sync with Outlook Calendar",
"recent_event": "Sync with a recent event",
"integration": { "integration": {
"logout": "log out", "logout": "log out",
"close": "close",
"info": "Importing will overwrite your current availability", "info": "Importing will overwrite your current availability",
"button": "Import availability" "button": "Import availability"
} }

View file

@ -0,0 +1,16 @@
const CrabIcon = (props: React.ComponentProps<'svg'>) =>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" {...props}>
<path fill="currentColor" 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 fill="currentColor" 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 fill="currentColor" x="266.27" y="200.39" width="20.89" height="57.44" rx="10.44"/>
<rect fill="currentColor" x="224.49" y="200.39" width="20.89" height="57.44" rx="10.44"/>
<path fill="currentColor" 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"/>
<path fill="currentColor" d="M402.77,386.23c36,12.31,66,40.07,59.44,60.75C440,431.17,413.11,416.91,389,409.83Z"/>
<path fill="currentColor" 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 fill="currentColor" 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 fill="currentColor" 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 fill="currentColor" 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 fill="currentColor" 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>
export default CrabIcon

View file

@ -0,0 +1,11 @@
const GoogleIcon = (props: React.ComponentProps<'svg'>) =>
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2443 2500" {...props}>
<path fill="currentColor" d="M1245.8,1018.7V1481h686.5c-13.8,114.9-88.6,287.9-254.7,404.2v0c-105.2,73.4-246.4,124.6-431.8,124.6
c-329.4,0-609-217.3-708.7-517.7h0l0,0v0c-26.3-77.5-41.5-160.6-41.5-246.4c0-85.8,15.2-168.9,40.1-246.4v0
c101-300.4,380.6-517.7,710.1-517.7v0c233.9,0,391.7,101,481.7,185.5l351.6-343.3C1863.2,123.2,1582.2,0,1245.8,0
C758.6,0,337.8,279.6,133,686.5h0h0C48.6,855.4,0.1,1045,0.1,1245.7S48.6,1636,133,1804.9h0l0,0
c204.8,406.9,625.6,686.5,1112.8,686.5c336.3,0,618.7-110.7,824.9-301.7l0,0h0c235.3-217.3,370.9-537,370.9-916.3
c0-102.4-8.3-177.2-26.3-254.7H1245.8z" />
</svg>
export default GoogleIcon

View file

@ -0,0 +1,20 @@
const OutlookIcon = (props: React.ComponentProps<'svg'>) =>
<svg version="1.1" width="103.17322" height="104.31332" viewBox="0 0 103.17322 104.31332" {...props}>
<path
d="m 64.566509,22.116383 v 20.404273 l 7.130526,4.489881 c 0.188058,0.05485 0.595516,0.05877 0.783574,0 L 103.16929,26.320259 c 0,-2.44867 -2.28412,-4.203876 -3.573094,-4.203876 H 64.566509 z"
fill="currentColor" />
<path
d="m 64.566509,50.13308 6.507584,4.470291 c 0.916782,0.673874 2.021622,0 2.021622,0 -1.100922,0.673874 30.077495,-20.035993 30.077495,-20.035993 v 37.501863 c 0,4.082422 -2.61322,5.794531 -5.551621,5.794531 H 64.562591 V 50.13308 z"
fill="currentColor" />
<g transform="matrix(3.9178712,0,0,3.9178712,-13.481403,-41.384473)">
<path
d="m 11.321,20.958 c -0.566,0 -1.017,0.266 -1.35,0.797 -0.333,0.531 -0.5,1.234 -0.5,2.109 0,0.888 0.167,1.59 0.5,2.106 0.333,0.517 0.77,0.774 1.31,0.774 0.557,0 0.999,-0.251 1.325,-0.753 0.326,-0.502 0.49,-1.199 0.49,-2.09 0,-0.929 -0.158,-1.652 -0.475,-2.169 -0.317,-0.516 -0.75,-0.774 -1.3,-0.774 z"
fill="currentColor" />
<path
d="m 3.441,13.563 v 20.375 l 15.5,3.25 V 10.563 l -15.5,3 z m 10.372,13.632 c -0.655,0.862 -1.509,1.294 -2.563,1.294 -1.027,0 -1.863,-0.418 -2.51,-1.253 C 8.094,26.4 7.77,25.312 7.77,23.97 c 0,-1.417 0.328,-2.563 0.985,-3.438 0.657,-0.875 1.527,-1.313 2.61,-1.313 1.023,0 1.851,0.418 2.482,1.256 0.632,0.838 0.948,1.942 0.948,3.313 10e-4,1.409 -0.327,2.545 -0.982,3.407 z"
fill="currentColor" />
</g>
</svg>
export default OutlookIcon

View file

@ -1,9 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 2443 2500" style="enable-background:new 0 0 2443 2500;" xml:space="preserve">
<path style="fill:#FFFFFF" d="M1245.8,1018.7V1481h686.5c-13.8,114.9-88.6,287.9-254.7,404.2v0c-105.2,73.4-246.4,124.6-431.8,124.6
c-329.4,0-609-217.3-708.7-517.7h0l0,0v0c-26.3-77.5-41.5-160.6-41.5-246.4c0-85.8,15.2-168.9,40.1-246.4v0
c101-300.4,380.6-517.7,710.1-517.7v0c233.9,0,391.7,101,481.7,185.5l351.6-343.3C1863.2,123.2,1582.2,0,1245.8,0
C758.6,0,337.8,279.6,133,686.5h0h0C48.6,855.4,0.1,1045,0.1,1245.7S48.6,1636,133,1804.9h0l0,0
c204.8,406.9,625.6,686.5,1112.8,686.5c336.3,0,618.7-110.7,824.9-301.7l0,0h0c235.3-217.3,370.9-537,370.9-916.3
c0-102.4-8.3-177.2-26.3-254.7H1245.8z"/>
</svg>

Before

Width:  |  Height:  |  Size: 859 B

View file

@ -1,54 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Generator: Adobe Illustrator 15.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
version="1.1"
id="Layer_1"
x="0px"
y="0px"
width="103.17322"
height="104.31332"
viewBox="0 0 103.17322 104.31332"
enable-background="new 0 0 190 50"
xml:space="preserve"
inkscape:version="0.48.2 r9819"
sodipodi:docname="Outlook_logo.svg"><metadata
id="metadata45"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
id="defs43" />
<path
d="m 64.566509,22.116383 v 20.404273 l 7.130526,4.489881 c 0.188058,0.05485 0.595516,0.05877 0.783574,0 L 103.16929,26.320259 c 0,-2.44867 -2.28412,-4.203876 -3.573094,-4.203876 H 64.566509 z"
id="path3"
inkscape:connector-curvature="0"
style="fill:#FFFFFF" />
<path
d="m 64.566509,50.13308 6.507584,4.470291 c 0.916782,0.673874 2.021622,0 2.021622,0 -1.100922,0.673874 30.077495,-20.035993 30.077495,-20.035993 v 37.501863 c 0,4.082422 -2.61322,5.794531 -5.551621,5.794531 H 64.562591 V 50.13308 z"
id="path5"
inkscape:connector-curvature="0"
style="fill:#FFFFFF" />
<g
id="g23"
transform="matrix(3.9178712,0,0,3.9178712,-13.481403,-41.384473)">
<path
d="m 11.321,20.958 c -0.566,0 -1.017,0.266 -1.35,0.797 -0.333,0.531 -0.5,1.234 -0.5,2.109 0,0.888 0.167,1.59 0.5,2.106 0.333,0.517 0.77,0.774 1.31,0.774 0.557,0 0.999,-0.251 1.325,-0.753 0.326,-0.502 0.49,-1.199 0.49,-2.09 0,-0.929 -0.158,-1.652 -0.475,-2.169 -0.317,-0.516 -0.75,-0.774 -1.3,-0.774 z"
id="path25"
inkscape:connector-curvature="0"
style="fill:#FFFFFF" />
<path
d="m 3.441,13.563 v 20.375 l 15.5,3.25 V 10.563 l -15.5,3 z m 10.372,13.632 c -0.655,0.862 -1.509,1.294 -2.563,1.294 -1.027,0 -1.863,-0.418 -2.51,-1.253 C 8.094,26.4 7.77,25.312 7.77,23.97 c 0,-1.417 0.328,-2.563 0.985,-3.438 0.657,-0.875 1.527,-1.313 2.61,-1.313 1.023,0 1.851,0.418 2.482,1.256 0.632,0.838 0.948,1.942 0.948,3.313 10e-4,1.409 -0.327,2.545 -0.982,3.407 z"
id="path27"
inkscape:connector-curvature="0"
style="fill:#FFFFFF" />
</g>
</svg>

Before

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View file

@ -1,10 +1,14 @@
import { create } from 'zustand' import { create } from 'zustand'
import { persist } from 'zustand/middleware' import { persist } from 'zustand/middleware'
interface RecentEvent { export interface RecentEvent {
id: string id: string
name: string name: string
created_at: number created_at: number
user?: {
name: string
availability: string[]
}
} }
interface RecentsStore { interface RecentsStore {
@ -19,16 +23,12 @@ const useRecentsStore = create<RecentsStore>()(persist(
set => ({ set => ({
recents: [], recents: [],
addRecent: event => set(state => { addRecent: event => set(state => ({
const recents = state.recents.filter(e => e.id !== event.id) recents: [{ ...state.recents.find(e => e.id === event.id), ...event }, ...state.recents.filter(e => e.id !== event.id)],
recents.unshift(event) })),
recents.length = Math.min(recents.length, 5) removeRecent: id => set(state => ({
return { recents } recents: state.recents.filter(e => e.id !== id),
}), })),
removeRecent: id => set(state => {
const recents = state.recents.filter(e => e.id !== id)
return { recents }
}),
clearRecents: () => set({ recents: [] }), clearRecents: () => set({ recents: [] }),
}), }),
{ {