Skip to content

Commit

Permalink
feat: Add calendar alert (#1204)
Browse files Browse the repository at this point in the history
* feat: Calendar alert

* minor fix

* requested changes
  • Loading branch information
ncomerci committed Aug 28, 2023
1 parent 6cf303f commit 301d712
Show file tree
Hide file tree
Showing 15 changed files with 294 additions and 35 deletions.
2 changes: 1 addition & 1 deletion src/components/BidRequest/BidRequestFundingSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ export default function BidRequestFundingSection({ onValidation, isFormDisabled,
max={schema.projectDuration.maximum}
onChange={(value) => setValue('projectDuration', Number(value))}
label={t('page.submit_bid.funding_section.project_duration_label')}
unitLabel={t('page.submit_bid.funding_section.project_duration_unit')}
unit="months"
/>
</div>
</div>
Expand Down
2 changes: 1 addition & 1 deletion src/components/GrantRequest/AddBudgetBreakdownModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -159,8 +159,8 @@ export default function AddBudgetBreakdownModal({
max={projectDuration}
onChange={(value) => setValue('duration', Number(value))}
label={t('page.submit_grant.due_diligence.budget_breakdown_modal.duration_label')}
unitLabel={t('page.submit_grant.due_diligence.budget_breakdown_modal.duration_unit_label')}
subtitle={t('page.submit_grant.due_diligence.budget_breakdown_modal.duration_subtitle')}
unit="months"
/>
</ContentSection>
<ContentSection className="ProjectRequestSection__Field">
Expand Down
2 changes: 1 addition & 1 deletion src/components/GrantRequest/GrantRequestFundingSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,7 @@ export default function GrantRequestFundingSection({
max={availableDurations[availableDurations.length - 1]}
onChange={(value) => setValue('projectDuration', Number(value))}
label={t('page.submit_grant.funding_section.project_duration_title')}
unitLabel={t('page.submit_grant.funding_section.project_duration_unit')}
unit="months"
/>
</div>
</div>
Expand Down
26 changes: 26 additions & 0 deletions src/components/Icon/CalendarAdd.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import React from 'react'

interface Props {
size?: string
className?: string
}

function CalendarAdd({ size = '24', className }: Props) {
return (
<svg
className={className}
width={size}
height={size}
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M5.75 0.75C5.75 0.334375 5.41563 0 5 0C4.58437 0 4.25 0.334375 4.25 0.75V2H3C1.89688 2 1 2.89687 1 4V4.5V6V14C1 15.1031 1.89688 16 3 16H13C14.1031 16 15 15.1031 15 14V6V4.5V4C15 2.89687 14.1031 2 13 2H11.75V0.75C11.75 0.334375 11.4156 0 11 0C10.5844 0 10.25 0.334375 10.25 0.75V2H5.75V0.75ZM2.5 6H13.5V14C13.5 14.275 13.275 14.5 13 14.5H3C2.725 14.5 2.5 14.275 2.5 14V6ZM8 7.25C7.58437 7.25 7.25 7.58437 7.25 8V9.5H5.75C5.33437 9.5 5 9.83438 5 10.25C5 10.6656 5.33437 11 5.75 11H7.25V12.5C7.25 12.9156 7.58437 13.25 8 13.25C8.41563 13.25 8.75 12.9156 8.75 12.5V11H10.25C10.6656 11 11 10.6656 11 10.25C11 9.83438 10.6656 9.5 10.25 9.5H8.75V8C8.75 7.58437 8.41563 7.25 8 7.25Z"
fill="black"
/>
</svg>
)
}

export default CalendarAdd
7 changes: 7 additions & 0 deletions src/components/Modal/CalendarAlertModal.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
.CalendarAlertModal__SelectorContainer {
margin-bottom: 25px;
}

.ui.button.basic.CalendarAlertModal__CancelButton {
color: var(--black-600) !important;
}
96 changes: 96 additions & 0 deletions src/components/Modal/CalendarAlertModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import React, { useEffect, useMemo, useState } from 'react'

import { UnitTypeLongPlural } from 'dayjs'
import { Button } from 'decentraland-ui/dist/components/Button/Button'
import { Close } from 'decentraland-ui/dist/components/Close/Close'
import { Header } from 'decentraland-ui/dist/components/Header/Header'
import { Modal, ModalProps } from 'decentraland-ui/dist/components/Modal/Modal'

import { ProposalAttributes } from '../../entities/Proposal/types'
import { proposalUrl } from '../../entities/Proposal/utils'
import useFormatMessage from '../../hooks/useFormatMessage'
import Time from '../../utils/date/Time'
import { getGoogleCalendarUrl } from '../../utils/projects'
import NumberSelector from '../ProjectRequest/NumberSelector'

import './CalendarAlertModal.css'

type CalendarAlertModalProps = Omit<ModalProps, 'children'> & {
proposal: Pick<ProposalAttributes, 'id' | 'title' | 'finish_at'>
}

const UNITS: UnitTypeLongPlural[] = ['seconds', 'minutes', 'hours', 'days']
const MAX_TIME_VALUE = 300

function getAlertDate(finishAt: ProposalAttributes['finish_at'], value: number, unit: UnitTypeLongPlural): Date | null {
const proposalFinishDate = Time.from(finishAt)
const currentDate = Time()
const alertDate = proposalFinishDate.subtract(value, unit)
if (alertDate.isBefore(currentDate)) {
return null
}
return alertDate.toDate()
}

function CalendarAlertModal({ proposal, onClose, ...props }: CalendarAlertModalProps) {
const t = useFormatMessage()
const { finish_at, title, id } = proposal
const [timeValue, setTimeValue] = useState(0)
const [unit, setUnit] = useState<UnitTypeLongPlural | undefined>()
const [isDisabled, setIsDisabled] = useState(false)

const handleClose = () => {
onClose()
setTimeValue(0)
setUnit(undefined)
}

const alertDate = useMemo(
() => (unit ? getAlertDate(finish_at, timeValue, unit) : null),
[finish_at, unit, timeValue]
)

useEffect(() => {
setIsDisabled(!alertDate)
}, [alertDate])

const alertUrl = getGoogleCalendarUrl({
title: t(`modal.calendar_alert.calendar_title`, { title }),
details: t(`modal.calendar_alert.calendar_details`, {
finish_date: Time.from(finish_at).format('MMM DD, YYYY HH:mm'),
url: proposalUrl(id),
}),
startAt: alertDate!,
})

return (
<Modal {...props} onClose={handleClose} size="tiny" closeIcon={<Close />}>
<Modal.Content>
<div className="ProposalModal__Title">
<Header>{t('modal.calendar_alert.title')}</Header>
</div>
<div className="CalendarAlertModal__SelectorContainer">
<NumberSelector
value={timeValue}
min={0}
max={MAX_TIME_VALUE}
onChange={setTimeValue}
label={t('modal.calendar_alert.label')}
unit={UNITS}
onUnitChange={setUnit}
/>
</div>
<div className="ProposalModal__Actions">
<Button fluid primary disabled={isDisabled} as="a" href={alertUrl} target="_blank">
{t('modal.calendar_alert.add_to_calendar')}
</Button>
<Button className="CalendarAlertModal__CancelButton" fluid basic onClick={handleClose}>
{t('modal.calendar_alert.cancel')}
</Button>
</div>
</Modal.Content>
</Modal>
)
}

export default CalendarAlertModal
21 changes: 19 additions & 2 deletions src/components/ProjectRequest/NumberSelector.css
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
.NumberSelector {
max-width: 210px;
.NumberSelector__Container {
max-width: fit-content;
}

.NumberSelector__InputContainer {
Expand All @@ -23,6 +23,23 @@
border-left: 1px solid var(--alpha-black-400);
}

.ui.dropdown.NumberSelector__Dropdown {
min-width: unset;
border: unset;
border-left: 1px solid var(--alpha-black-400);
}

.ui.dropdown.NumberSelector__Dropdown .text,
.ui.dropdown.active.NumberSelector__Dropdown .text {
text-transform: unset;
font-weight: var(--weight-normal);
color: var(--black-600) !important;
}

.ui.dropdown.NumberSelector__Dropdown > .dropdown.icon {
color: var(--black-600);
}

.NumberSelector__Input {
display: flex;
align-items: center;
Expand Down
46 changes: 41 additions & 5 deletions src/components/ProjectRequest/NumberSelector.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import React, { useCallback } from 'react'
import React, { useCallback, useEffect } from 'react'

import { UnitTypeLongPlural } from 'dayjs'
import { Dropdown } from 'decentraland-ui/dist/components/Dropdown/Dropdown'

import useFormatMessage from '../../hooks/useFormatMessage'
import Label from '../Common/Typography/Label'
import Markdown from '../Common/Typography/Markdown'
import Add from '../Icon/Add'
Expand All @@ -13,11 +17,13 @@ interface Props {
min: number
max: number
label: string
unitLabel: string
unit: UnitTypeLongPlural | UnitTypeLongPlural[]
subtitle?: string
onUnitChange?: (unit: UnitTypeLongPlural) => void
}

const NumberSelector = ({ value, onChange, min, max, label, unitLabel, subtitle }: Props) => {
const NumberSelector = ({ value, onChange, min, max, label, unit, onUnitChange, subtitle }: Props) => {
const t = useFormatMessage()
const handleAddClick = useCallback(() => {
if (value === max) {
return
Expand All @@ -34,10 +40,30 @@ const NumberSelector = ({ value, onChange, min, max, label, unitLabel, subtitle
onChange(value - 1)
}, [onChange, min, value])

const handleUnitChange = useCallback(
(unit: UnitTypeLongPlural) => {
if (onUnitChange) {
onUnitChange(unit)
}
},
[onUnitChange]
)
const isUnitArray = Array.isArray(unit)

useEffect(() => {
if (isUnitArray && unit.length > 0) {
handleUnitChange(unit[0])
}
}, [handleUnitChange, isUnitArray, unit])

const getUnitLabel = (unit: UnitTypeLongPlural) => t(`general.time_units.${unit}`)
const getUnitOptions = (units: UnitTypeLongPlural[]) =>
units.map((unit) => ({ key: unit, value: unit, text: getUnitLabel(unit) }))

return (
<div className="NumberSelector">
<Label>{label}</Label>
<div>
<div className="NumberSelector__Container">
<div className="NumberSelector__InputContainer">
<div className="NumberSelector__Input">
<button className="NumberSelector__Button" onClick={handleRemoveClick}>
Expand All @@ -48,7 +74,17 @@ const NumberSelector = ({ value, onChange, min, max, label, unitLabel, subtitle
<Add />
</button>
</div>
<div className="NumberSelector__Description">{unitLabel}</div>
{!isUnitArray ? (
<div className="NumberSelector__Description">{getUnitLabel(unit)}</div>
) : (
<Dropdown
className="NumberSelector__Dropdown"
selection
defaultValue={unit[0]}
options={getUnitOptions(unit)}
onChange={(_, { value }) => handleUnitChange(value as UnitTypeLongPlural)}
/>
)}
</div>
</div>
{subtitle && (
Expand Down
19 changes: 18 additions & 1 deletion src/components/Proposal/ProposalSidebar.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useMemo } from 'react'
import React, { useMemo, useState } from 'react'

import useAuthContext from 'decentraland-gatsby/dist/context/Auth/useAuthContext'

Expand All @@ -10,7 +10,9 @@ import { isProposalStatusWithUpdates } from '../../entities/Updates/utils'
import { SelectedVoteChoice, Vote } from '../../entities/Votes/types'
import { calculateResult } from '../../entities/Votes/utils'
import { ProposalPageState } from '../../pages/proposal'
import CalendarAlertModal from '../Modal/CalendarAlertModal'

import CalendarAlertButton from './View/CalendarAlertButton'
import ForumButton from './View/ForumButton'
import ProposalCoAuthorStatus from './View/ProposalCoAuthorStatus'
import ProposalDetailSection from './View/ProposalDetailSection'
Expand Down Expand Up @@ -80,6 +82,8 @@ export default function ProposalSidebar({
const choices: string[] = proposal?.snapshot_proposal?.choices || EMPTY_VOTE_CHOICES
const partialResults = useMemo(() => calculateResult(choices, highQualityVotes || {}), [choices, highQualityVotes])

const [isCalendarModalOpen, setIsCalendarModalOpen] = useState(false)

const handleVoteClick = (selectedChoice: SelectedVoteChoice) => {
if (voteWithSurvey) {
updatePageState({
Expand Down Expand Up @@ -107,6 +111,7 @@ export default function ProposalSidebar({
)

const showVestingContract = proposal?.vesting_addresses && proposal?.vesting_addresses.length > 0
const isCalendarButtonDisabled = !proposal || proposal.status !== ProposalStatus.Active

return (
<>
Expand Down Expand Up @@ -145,8 +150,20 @@ export default function ProposalSidebar({
subscribed={subscribed}
onClick={() => subscribe(!subscribed)}
/>
<CalendarAlertButton
loading={proposalLoading}
disabled={isCalendarButtonDisabled}
onClick={() => setIsCalendarModalOpen(true)}
/>
{proposal && <ProposalDetailSection proposal={proposal} />}
{proposal && <ProposalActions proposal={proposal} deleting={deleting} updatePageState={updatePageState} />}
{proposal && (
<CalendarAlertModal
proposal={proposal}
open={isCalendarModalOpen}
onClose={() => setIsCalendarModalOpen(false)}
/>
)}
</div>
</>
)
Expand Down
24 changes: 24 additions & 0 deletions src/components/Proposal/View/CalendarAlertButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import React from 'react'

import useFormatMessage from '../../../hooks/useFormatMessage'
import CalendarAdd from '../../Icon/CalendarAdd'

import SidebarButton from './SidebarButton'

interface Props {
loading?: boolean
disabled?: boolean
onClick: () => void
}

function CalendarAlertButton({ loading, disabled, onClick }: Props) {
const t = useFormatMessage()
return (
<SidebarButton loading={loading} disabled={disabled} onClick={onClick}>
<CalendarAdd size="20" />
<span>{t('page.proposal_detail.calendar_button')}</span>
</SidebarButton>
)
}

export default CalendarAlertButton
5 changes: 3 additions & 2 deletions src/components/Proposal/View/SectionButton.css
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,14 @@

.SectionButton.SectionButton--disabled span,
.SectionButton.SectionButton--disabled img,
.SectionButton.SectionButton--disabled svg,
.SectionButton.SectionButton--loading span,
.SectionButton.SectionButton--loading svg,
.SectionButton.SectionButton--loading img {
opacity: 0.5;
}

.SectionButton.SectionButton--disabled span,
.SectionButton.SectionButton--disabled img {
.SectionButton.SectionButton--disabled {
pointer-events: none;
}

Expand Down
31 changes: 31 additions & 0 deletions src/components/Proposal/View/SidebarButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import React from 'react'

import classNames from 'classnames'

import './DetailsSection.css'
import './SectionButton.css'

interface Props {
loading?: boolean
disabled?: boolean
onClick?: () => void
children?: React.ReactNode
}

function SidebarButton({ loading, disabled, onClick, children }: Props) {
return (
<button
onClick={onClick}
className={classNames(
'DetailsSection',
'SectionButton',
loading && 'SectionButton--loading',
disabled && 'SectionButton--disabled'
)}
>
<div className="SectionButton__Container">{children}</div>
</button>
)
}

export default SidebarButton
Loading

0 comments on commit 301d712

Please sign in to comment.