Merge pull request #280 from GRA0007/feat/cache-availability
Save availability and sync with other events
This commit is contained in:
commit
2ea9679974
|
|
@ -167,6 +167,7 @@ const EventAvailabilities = ({ event }: EventAvailabilitiesProps) => {
|
|||
people={people}
|
||||
table={table}
|
||||
/> : user && <AvailabilityEditor
|
||||
eventId={event?.id}
|
||||
times={expandedTimes}
|
||||
timezone={timezone}
|
||||
value={user.availability}
|
||||
|
|
@ -174,6 +175,15 @@ const EventAvailabilities = ({ event }: EventAvailabilitiesProps) => {
|
|||
if (!event) return
|
||||
const oldAvailability = [...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)
|
||||
.catch(e => {
|
||||
console.warn(e)
|
||||
|
|
|
|||
|
|
@ -135,3 +135,11 @@ a:focus-visible {
|
|||
*::-webkit-scrollbar-thumb:active {
|
||||
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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,16 +2,18 @@ import { Fragment, useCallback, useEffect, useRef, useState } from 'react'
|
|||
|
||||
import Button from '/src/components/Button/Button'
|
||||
import Content from '/src/components/Content/Content'
|
||||
import GoogleCalendar from '/src/components/GoogleCalendar/GoogleCalendar'
|
||||
import { usePalette } from '/src/hooks/usePalette'
|
||||
import { useTranslation } from '/src/i18n/client'
|
||||
import { calculateTable, makeClass, parseSpecificDate } from '/src/utils'
|
||||
|
||||
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 Skeleton from '../AvailabilityViewer/components/Skeleton/Skeleton'
|
||||
|
||||
interface AvailabilityEditorProps {
|
||||
eventId?: string
|
||||
times: string[]
|
||||
timezone: string
|
||||
value: string[]
|
||||
|
|
@ -19,7 +21,7 @@ interface AvailabilityEditorProps {
|
|||
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')
|
||||
|
||||
// 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}
|
||||
onImport={onChange}
|
||||
/>
|
||||
<RecentEvents
|
||||
eventId={eventId}
|
||||
times={times}
|
||||
onImport={onChange}
|
||||
/>
|
||||
</div>
|
||||
</Content>}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -4,7 +4,7 @@ import { Temporal } from '@js-temporal/polyfill'
|
|||
|
||||
import Button from '/src/components/Button/Button'
|
||||
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 styles from './GoogleCalendar.module.scss'
|
||||
|
|
@ -119,14 +119,14 @@ const GoogleCalendar = ({ timezone, timeStart, timeEnd, times, onImport }: Googl
|
|||
isLoading={canLoad}
|
||||
surfaceColor="#4286F5"
|
||||
shadowColor="#3367BD"
|
||||
icon={<img aria-hidden="true" src={googleLogo.src} alt="" />}
|
||||
icon={<GoogleIcon aria-hidden="true" />}
|
||||
>
|
||||
{t('you.google_cal')}
|
||||
</Button>}
|
||||
|
||||
{calendars && <div className={styles.wrapper}>
|
||||
<p className={styles.title}>
|
||||
<img src={googleLogo.src} alt="" className={styles.icon} />
|
||||
<GoogleIcon className={styles.icon} />
|
||||
<strong>{t('you.google_cal')}</strong>
|
||||
(<button
|
||||
className={styles.linkButton}
|
||||
|
|
@ -148,17 +148,15 @@ const GoogleCalendar = ({ timezone, timeStart, timeEnd, times, onImport }: Googl
|
|||
>{t('you.select_none')}</button>}
|
||||
</div>
|
||||
|
||||
{calendars.map(calendar => <div key={calendar.id}>
|
||||
{calendars.map(calendar => <div key={calendar.id} className={styles.item}>
|
||||
<input
|
||||
className={styles.checkbox}
|
||||
type="checkbox"
|
||||
id={calendar.id}
|
||||
color={calendar.color}
|
||||
style={{ accentColor: calendar.color }}
|
||||
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.calendarName} htmlFor={calendar.id} title={calendar.description}>{allowUrlToWrap(calendar.name)}</label>
|
||||
<label className={styles.name} htmlFor={calendar.id} title={calendar.description}>{allowUrlToWrap(calendar.name)}</label>
|
||||
</div>)}
|
||||
|
||||
<div className={styles.info}>{t('you.integration.info')}</div>
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
/* TODO:
|
||||
import { PublicClientApplication } from '@azure/msal-browser'
|
||||
import { Client } from '@microsoft/microsoft-graph-client'
|
||||
import { useEffect, useState } from 'react'
|
||||
|
|
@ -226,3 +227,4 @@ const OutlookCalendar = ({ timeZone, timeMin, timeMax, onImport }) => {
|
|||
}
|
||||
|
||||
export default OutlookCalendar
|
||||
*/
|
||||
|
|
@ -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
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -41,6 +41,7 @@
|
|||
|
||||
.logo {
|
||||
width: 2.5rem;
|
||||
height: 2.5rem;
|
||||
margin-right: 16px;
|
||||
}
|
||||
|
||||
|
|
@ -113,6 +114,7 @@
|
|||
|
||||
.bigLogo {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
transition: transform .15s;
|
||||
animation: jelly .5s 1 .05s;
|
||||
user-select: none;
|
||||
|
|
|
|||
|
|
@ -27,12 +27,12 @@ const Header = async ({ isFull, isSmall }: HeaderProps) => {
|
|||
|
||||
return <header className={styles.header} data-small={isSmall}>
|
||||
{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>
|
||||
<h1 className={makeClass(styles.bigTitle, molot.className)}>CRAB FIT</h1>
|
||||
</> : <Link href="/" className={styles.link}>
|
||||
<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>
|
||||
</div>
|
||||
<span className={styles.tagline}>{t('common:tagline')}</span>
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ const Recents = () => {
|
|||
return recents?.length ? <Section id="recents">
|
||||
<Content>
|
||||
<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}>
|
||||
<span className={styles.name}>{event.name}</span>
|
||||
<span
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@
|
|||
|
||||
img {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
display: block;
|
||||
border-radius: inherit;
|
||||
background-color: #CCC;
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
import { useState } from 'react'
|
||||
|
||||
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'
|
||||
|
||||
|
|
@ -34,7 +34,7 @@ const Video = () => {
|
|||
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>
|
||||
</a>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -60,8 +60,10 @@
|
|||
|
||||
"google_cal": "Sync with Google Calendar",
|
||||
"outlook_cal": "Sync with Outlook Calendar",
|
||||
"recent_event": "Sync with a recent event",
|
||||
"integration": {
|
||||
"logout": "log out",
|
||||
"close": "close",
|
||||
"info": "Importing will overwrite your current availability",
|
||||
"button": "Import availability"
|
||||
}
|
||||
|
|
|
|||
16
frontend/src/res/CrabIcon.tsx
Normal file
16
frontend/src/res/CrabIcon.tsx
Normal 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
|
||||
11
frontend/src/res/GoogleIcon.tsx
Normal file
11
frontend/src/res/GoogleIcon.tsx
Normal 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
|
||||
20
frontend/src/res/OutlookIcon.tsx
Normal file
20
frontend/src/res/OutlookIcon.tsx
Normal 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
|
||||
|
|
@ -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 |
|
|
@ -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 |
BIN
frontend/src/res/video_thumb.webp
Normal file
BIN
frontend/src/res/video_thumb.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 17 KiB |
|
|
@ -1,10 +1,14 @@
|
|||
import { create } from 'zustand'
|
||||
import { persist } from 'zustand/middleware'
|
||||
|
||||
interface RecentEvent {
|
||||
export interface RecentEvent {
|
||||
id: string
|
||||
name: string
|
||||
created_at: number
|
||||
user?: {
|
||||
name: string
|
||||
availability: string[]
|
||||
}
|
||||
}
|
||||
|
||||
interface RecentsStore {
|
||||
|
|
@ -19,16 +23,12 @@ const useRecentsStore = create<RecentsStore>()(persist(
|
|||
set => ({
|
||||
recents: [],
|
||||
|
||||
addRecent: event => set(state => {
|
||||
const recents = state.recents.filter(e => e.id !== event.id)
|
||||
recents.unshift(event)
|
||||
recents.length = Math.min(recents.length, 5)
|
||||
return { recents }
|
||||
}),
|
||||
removeRecent: id => set(state => {
|
||||
const recents = state.recents.filter(e => e.id !== id)
|
||||
return { recents }
|
||||
}),
|
||||
addRecent: event => set(state => ({
|
||||
recents: [{ ...state.recents.find(e => e.id === event.id), ...event }, ...state.recents.filter(e => e.id !== event.id)],
|
||||
})),
|
||||
removeRecent: id => set(state => ({
|
||||
recents: state.recents.filter(e => e.id !== id),
|
||||
})),
|
||||
clearRecents: () => set({ recents: [] }),
|
||||
}),
|
||||
{
|
||||
|
|
|
|||
Loading…
Reference in a new issue