Skip to content

Commit e45f43a

Browse files
committed
Merge branch 'seven' into 6673-seven-header-slots
2 parents 5b9ab13 + b5f7996 commit e45f43a

17 files changed

Lines changed: 424 additions & 5 deletions

File tree

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import type { EventCT } from '@plone/types';
2+
import { getDate, isSameDay } from '../../helpers';
3+
import { useTranslation } from 'react-i18next';
4+
5+
interface EventDateProps {
6+
content: EventCT;
7+
locale: string;
8+
}
9+
10+
const getDateTime = (date: string, locale: string): string => {
11+
const dateObject = new Date(date);
12+
const dateTimeFormat = Intl.DateTimeFormat([locale], {
13+
timeStyle: 'short',
14+
hourCycle: 'h12',
15+
});
16+
return dateTimeFormat.format(dateObject);
17+
};
18+
19+
export default function EventDate({ locale, content }: EventDateProps) {
20+
const { t } = useTranslation();
21+
22+
if (!locale || !content) return null;
23+
24+
const { start, end, whole_day, open_end } = content;
25+
26+
const startDate = getDate(start, locale);
27+
const startTime = getDateTime(start, locale);
28+
const endDate = getDate(end, locale);
29+
const endTime = getDateTime(end, locale);
30+
31+
return (
32+
<dd>
33+
{isSameDay(start, end) ? (
34+
<>
35+
{whole_day
36+
? startDate
37+
: open_end
38+
? t('layout.views.event.time.sameDayOpenEnd', {
39+
date: startDate,
40+
fromTime: startTime,
41+
})
42+
: t('layout.views.event.time.sameDayRange', {
43+
date: startDate,
44+
fromTime: startTime,
45+
toTime: endTime,
46+
})}
47+
</>
48+
) : whole_day ? (
49+
open_end ? (
50+
t('layout.views.event.time.multiDayWholeDayOpen', {
51+
fromDate: startDate,
52+
})
53+
) : (
54+
t('layout.views.event.time.multiDayWholeDay', {
55+
fromDate: startDate,
56+
toDate: endDate,
57+
})
58+
)
59+
) : open_end ? (
60+
t('layout.views.event.time.multiDayRangeOpen', {
61+
fromDate: startDate,
62+
fromTime: startTime,
63+
})
64+
) : (
65+
t('layout.views.event.time.multiDayRange', {
66+
fromDate: startDate,
67+
fromTime: startTime,
68+
toDate: endDate,
69+
toTime: endTime,
70+
})
71+
)}
72+
</dd>
73+
);
74+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
@layer custom {
2+
.event-details {
3+
padding: 1rem;
4+
border: 1px solid lightgray;
5+
box-shadow: 0 1px 2px 0 lightgray;
6+
7+
dt {
8+
display: block;
9+
padding-bottom: 0.1rem;
10+
border-bottom: 1px solid gray;
11+
font-weight: bold;
12+
}
13+
14+
dd {
15+
display: block;
16+
margin-bottom: 1rem;
17+
}
18+
19+
:global(.download-event) {
20+
display: flex;
21+
flex-direction: row;
22+
align-content: center;
23+
24+
a {
25+
align-self: center;
26+
margin-bottom: 0;
27+
margin-left: 4px;
28+
}
29+
}
30+
}
31+
}
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
import EventDate from '../EventDate/EventDate';
2+
import { Link } from '@plone/components/quanta';
3+
import * as RRuleLib from 'rrule';
4+
import type * as RRuleTypes from 'rrule';
5+
import type { EventCT, RootData } from '@plone/types';
6+
import { getDate } from '../../helpers';
7+
import Calendar from '@plone/components/icons/calendar.svg?react';
8+
import { useTranslation } from 'react-i18next';
9+
import styles from './EventDetails.module.css';
10+
11+
interface EventDetailsProps {
12+
data?: RootData<EventCT>;
13+
}
14+
15+
interface RecurrenceProps {
16+
recurrence: string;
17+
start: string;
18+
locale: string;
19+
}
20+
21+
export default function EventDetails({ data }: EventDetailsProps) {
22+
const { t } = useTranslation();
23+
24+
if (!data) return;
25+
26+
const { content, locale } = data;
27+
28+
return (
29+
<aside className={styles['event-details']}>
30+
<dl>
31+
{content.subjects?.length > 0 && (
32+
<>
33+
<dt>{t('layout.views.event.what')}</dt>
34+
<dd>
35+
<ul>
36+
{content.subjects.map((subject, index) => (
37+
<li key={index}>{subject}</li>
38+
))}
39+
</ul>
40+
</dd>
41+
</>
42+
)}
43+
<dt>{t('layout.views.event.when')}</dt>
44+
<EventDate content={content} locale={locale} />
45+
{content.recurrence && (
46+
<>
47+
<dt>{t('layout.views.event.allDates')}</dt>
48+
<Recurrence
49+
recurrence={content.recurrence}
50+
start={content.start}
51+
locale={locale}
52+
/>
53+
</>
54+
)}
55+
{content.location && (
56+
<>
57+
<dt>{t('layout.views.event.where')}</dt>
58+
<dd>{content.location}</dd>
59+
</>
60+
)}
61+
{content.contact_name && (
62+
<>
63+
<dt>{t('layout.views.event.contact.name')}</dt>
64+
<dd>
65+
{content.contact_email ? (
66+
<Link href={`mailto:${content.contact_email}`}>
67+
{content.contact_name}
68+
</Link>
69+
) : (
70+
content.contact_name
71+
)}
72+
</dd>
73+
</>
74+
)}
75+
{content.contact_phone && (
76+
<>
77+
<dt>{t('layout.views.event.contact.phone')}</dt>
78+
<dd>{content.contact_phone}</dd>
79+
</>
80+
)}
81+
{content.attendees?.length > 0 && (
82+
<>
83+
<dt>{t('layout.views.event.attendees')}</dt>
84+
<dd>
85+
<ul>
86+
{content.attendees.map((attendee, index) => (
87+
<li key={index}>{attendee}</li>
88+
))}
89+
</ul>
90+
</dd>
91+
</>
92+
)}
93+
{content.event_url && (
94+
<>
95+
<dt>{t('layout.views.event.website')}</dt>
96+
<dd>
97+
<Link
98+
href={content.event_url}
99+
target="_blank"
100+
rel="noopener noreferrer"
101+
>
102+
{t('layout.views.event.visitWebsite')}
103+
</Link>
104+
</dd>
105+
</>
106+
)}
107+
</dl>
108+
<span className="download-event">
109+
<Calendar />
110+
<Link
111+
href={`${content['@id']}/ics_view`}
112+
target="_blank"
113+
rel="noreferrer"
114+
>
115+
{t('layout.views.event.downloadEvent')}
116+
</Link>
117+
</span>
118+
</aside>
119+
);
120+
}
121+
122+
export const Recurrence = ({ recurrence, start, locale }: RecurrenceProps) => {
123+
const { RRule, rrulestr } = ((RRuleLib as any).default ||
124+
RRuleLib) as typeof RRuleTypes;
125+
126+
const newRecurrence = !recurrence.includes('DTSTART')
127+
? RRule.optionsToString({ dtstart: new Date(start) }) + '\n' + recurrence
128+
: recurrence;
129+
130+
const rule = rrulestr(newRecurrence, { unfold: true, forceset: true });
131+
const ruleItems = rule.all().map((date) => getDate(date, locale));
132+
133+
return (
134+
<dd>
135+
<ul>
136+
{ruleItems.map((date, index) => (
137+
<li key={index}>{date}</li>
138+
))}
139+
</ul>
140+
</dd>
141+
);
142+
};

packages/layout/helpers/index.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,3 +67,24 @@ export const getBlockStyleFieldConfigs = (
6767

6868
return styleFields as Record<string, StyleFieldConfig>;
6969
};
70+
71+
export function isSameDay(start: string, end: string): boolean {
72+
const startDate = new Date(start);
73+
const endDate = new Date(end);
74+
75+
return (
76+
startDate.getDate() === endDate.getDate() &&
77+
startDate.getMonth() === endDate.getMonth() &&
78+
startDate.getFullYear() === endDate.getFullYear()
79+
);
80+
}
81+
82+
export function getDate(date: string | Date, locale: string): string {
83+
const dateObject = typeof date === 'string' ? new Date(date) : date;
84+
85+
const dateTimeFormat = Intl.DateTimeFormat([locale], {
86+
dateStyle: 'medium',
87+
});
88+
89+
return dateTimeFormat.format(dateObject);
90+
}

packages/layout/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import DefaultView from './views/DefaultView';
66
import FileView from './views/FileView';
77
import ImageView from './views/ImageView';
88
import LinkView from './views/LinkView';
9+
import EventView from './views/EventView';
910

1011
export default function install(config: ConfigType) {
1112
// Translation factory
@@ -20,8 +21,10 @@ export default function install(config: ConfigType) {
2021
File: FileView,
2122
Image: ImageView,
2223
Link: LinkView,
24+
Event: EventView,
2325
...config.views.contentTypesViews,
2426
};
27+
2528
config.views.layoutViews = { ...config.views.layoutViews };
2629

2730
installSettings(config);

packages/layout/locales/de/common.json

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,28 @@
3535
"link": {
3636
"linkLabel": "Die Linkadresse lautet:",
3737
"externalLinkLabel": "Die externe Linkadresse lautet:"
38+
},
39+
"event": {
40+
"what": "Was",
41+
"when": "Datum",
42+
"time": {
43+
"sameDayOpenEnd": "{{date}} ab {{fromTime}}",
44+
"sameDayRange": "{{date}} von {{fromTime}} bis {{toTime}}",
45+
"multiDayWholeDayOpen": "{{fromDate}}",
46+
"multiDayWholeDay": "{{fromDate}} bis {{toDate}}",
47+
"multiDayRangeOpen": "{{fromDate}} {{fromTime}}",
48+
"multiDayRange": "{{fromDate}} {{fromTime}} bis {{toDate}} {{toTime}}"
49+
},
50+
"allDates": "Alle Termine",
51+
"where": "Ort",
52+
"contact": {
53+
"name": "Kontaktname",
54+
"phone": "Kontakttelefon"
55+
},
56+
"attendees": "Teilnehmer",
57+
"website": "Webseite",
58+
"visitWebsite": "Webseite besuchen",
59+
"downloadEvent": "Event herunterladen"
3860
}
3961
}
4062
}

packages/layout/locales/en/common.json

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,28 @@
3535
"link": {
3636
"linkLabel": "The link address is:",
3737
"externalLinkLabel": "The external link address is:"
38+
},
39+
"event": {
40+
"what": "What",
41+
"when": "When",
42+
"time": {
43+
"sameDayOpenEnd": "{{date}} from {{fromTime}}",
44+
"sameDayRange": "{{date}} from {{fromTime}} to {{toTime}}",
45+
"multiDayWholeDayOpen": "{{fromDate}}",
46+
"multiDayWholeDay": "{{fromDate}} to {{toDate}}",
47+
"multiDayRangeOpen": "{{fromDate}} {{fromTime}}",
48+
"multiDayRange": "{{fromDate}} {{fromTime}} to {{toDate}} {{toTime}}"
49+
},
50+
"allDates": "All dates",
51+
"where": "Where",
52+
"contact": {
53+
"name": "Contact Name",
54+
"phone": "Contact Phone"
55+
},
56+
"attendees": "Attendees",
57+
"website": "Website",
58+
"visitWebsite": "Visit website",
59+
"downloadEvent": "Download event"
3860
}
3961
}
4062
}

packages/layout/locales/it/common.json

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,28 @@
3535
"link": {
3636
"linkLabel": "L'indirizzo del link è:",
3737
"externalLinkLabel": "L'indirizzo del link esterno è:"
38+
},
39+
"event": {
40+
"what": "Cosa",
41+
"when": "Quando",
42+
"time": {
43+
"sameDayOpenEnd": "{{date}} dalle {{fromTime}}",
44+
"sameDayRange": "{{date}} dalle {{fromTime}} alle {{toTime}}",
45+
"multiDayWholeDayOpen": "{{fromDate}}",
46+
"multiDayWholeDay": "Dal {{fromDate}} al {{toDate}}",
47+
"multiDayRangeOpen": "{{fromDate}} {{fromTime}}",
48+
"multiDayRange": "Dal {{fromDate}} {{fromTime}} al {{toDate}} {{toTime}}"
49+
},
50+
"allDates": "Tutte le date",
51+
"where": "Dove",
52+
"contact": {
53+
"name": "Nome del contatto",
54+
"phone": "Telefono del contatto"
55+
},
56+
"attendees": "Partecipanti",
57+
"website": "Sito web",
58+
"visitWebsite": "Visita il sito web",
59+
"downloadEvent": "Scarica evento"
3860
}
3961
}
4062
}

packages/layout/news/6708.feature

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Added the view for the Event content type @arybakov05

packages/layout/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,8 @@
6565
"pretty-bytes": "^7.1.0",
6666
"react-aria-components": "catalog:",
6767
"react-i18next": "catalog:",
68-
"react-router": "catalog:"
68+
"react-router": "catalog:",
69+
"rrule": "^2.8.1"
6970
},
7071
"devDependencies": {
7172
"@plone/types": "workspace:*",

0 commit comments

Comments
 (0)