Commit 2d89ce4c authored by Yi (Flora) Liu's avatar Yi (Flora) Liu Committed by Christopher Guindon
Browse files

initial trial with search and list and readmore (#87)


Signed-off-by: Yi Liu's avatarYi Liu <yi.liu@eclipse-foundation.org>
parent ce093028
{
"search.exclude": {
"**/node_modules": false
}
}
\ No newline at end of file
module.exports = {
presets: ['@babel/preset-env', '@babel/preset-react'],
};
\ No newline at end of file
......@@ -3,6 +3,10 @@ title: "Upcoming Eclipse Events"
seo_title: "Upcoming Eclipse Events"
description: "The site provides date, time and place for the various events (conferences, demo camps, special days, hackathons and trainings) for the Eclipse ecosystem, displayed on a convenient map of the world."
hide_sidebar: true
headline: Eclipse Foundation Events
header_wrapper_class: events-jumbotron-class
# subtitle: "Join the world’s leading technologists and open source leaders at Eclipse Foundation events to share ideas, learn and collaborate."
tagline: Join the world’s leading technologists and open source leaders at Eclipse Foundation events to share ideas, learn and collaborate.
#hide_breadcrumb: true
date: 2020-03-01T16:09:45-04:00
layout: "single"
......
// These must be the first lines in src/index.js
import 'react-app-polyfill/ie11';
import 'react-app-polyfill/stable';
import React from 'react';
import ReactDOM from 'react-dom'
import Events from './components/Events'
import ReactDOM from 'react-dom';
import EventsDataFetcher from './components/EventsDataFetcher';
const App = () => (
<div className="container">
<h1>Upcoming Events</h1>
<Events/>
<EventsDataFetcher />
</div>
);
......
import React from 'react';
const Alerts = ({ alertType, message }) => {
switch(alertType) {
case 'ERROR':
return (
<div className="alert alert-danger" role="alert">
Oops, an error occurred: {message}
</div>
)
case 'WARNING':
return (
<div className="alert alert-warning" role="alert">
Warning! {message}
</div>
)
case 'INFO':
return (
<div className="alert alert-info" role="alert">
{message}
</div>
)
case 'SUCCESS':
return (
<div className="alert alert-success" role="alert">
{message}
</div>
)
default:
return ''
}
}
export default Alerts
\ No newline at end of file
export const alertTypes = {
ERROR: 'ERROR',
WARNING: 'WARNING',
SUCCESS: 'SUCCESS',
INFO: 'INFO'
}
\ No newline at end of file
import React from 'react';
import PropTypes from 'prop-types';
const Checkbox = ({ type = 'checkbox', name, checked = false, onChange }) => (
<input type={type} name={name} checked={checked} onChange={onChange} id="event-filter-checkbox-margin" data-testid={name} />
);
Checkbox.propTypes = {
type: PropTypes.string,
name: PropTypes.string.isRequired,
checked: PropTypes.bool,
onChange: PropTypes.func.isRequired,
}
export default Checkbox;
\ No newline at end of file
import React, { useState } from 'react';
import Checkbox from './Checkbox';
import { EVENT_TYPES, WORKING_GROUPS, checkFilterHasEvents } from './EventHelpers';
import PropTypes from 'prop-types';
const CheckboxFilters = ({ checkedTypes, setCheckedTypes, checkedWorkingGroups, setCheckedWorkingGroups, events }) => {
const determineInitialState = () => {
return window.innerWidth > 991
}
const [showTypes, setShowTypes] = useState(determineInitialState())
const [showWorkingGroups, setShowWorkingGroups] = useState(determineInitialState())
const handleChange = (e) => {
if (checkedWorkingGroups && setCheckedWorkingGroups) {
if (e.target.checked) {
setCheckedWorkingGroups({
...checkedWorkingGroups,
[e.target.name]: e.target.checked
});
} else {
setCheckedWorkingGroups({
...checkedWorkingGroups,
[e.target.name]: undefined
})
}
}
if (checkedTypes && setCheckedTypes) {
if (e.target.checked) {
setCheckedTypes({
...checkedTypes,
[e.target.name]: e.target.checked
});
} else {
setCheckedTypes({
...checkedTypes,
[e.target.name]: undefined
})
}
}
}
const toggleTypes = () => {
setShowTypes(!showTypes)
}
const toggleWorkingGroups = () => {
setShowWorkingGroups(!showWorkingGroups)
}
const WorkingGroups = () => {
if (checkedWorkingGroups && setCheckedWorkingGroups) {
return (
<>
<button
onClick={toggleWorkingGroups}
className="event-filter-title"
>
CATEGORIES
<i className="fa fa-angle-down event-filter-expandable-icon" aria-hidden="true"></i>
</button>
{ showWorkingGroups &&
<ul className="event-filter-checkbox-list">
{ checkFilterHasEvents(WORKING_GROUPS, "WORKINGGROUPS", events).map(item => (
<li key={item.id}>
<label key={item.id}>
<Checkbox
name={item.id}
checked={checkedWorkingGroups[item.id]}
onChange={handleChange}
/>
{item.name}
</label>
</li>
))}
</ul>
}
</>
)
}
}
const EventTypes = () => {
if (checkedTypes && setCheckedTypes) {
return (
<>
<button
onClick={toggleTypes}
className="event-filter-title"
>
EVENT TYPE
<i className="fa fa-angle-down event-filter-expandable-icon" aria-hidden="true"></i>
</button>
{ showTypes &&
<ul className="event-filter-checkbox-list">
{ checkFilterHasEvents(EVENT_TYPES, "EVENTTYPE", events).map(item => (
<li key={item.id}>
<label key={item.id}>
<Checkbox
name={item.id}
checked={checkedTypes[item.id]}
onChange={handleChange}
/>
{item.name}
</label>
</li>
)) }
</ul>
}
</>
)
}
}
return (
<div className="margin-bottom-10">
{WorkingGroups()}
{EventTypes()}
</div>
)
}
CheckboxFilters.propTypes = {
checkedTypes: PropTypes.object,
setCheckedTypes: PropTypes.func,
checkedWorkingGroups: PropTypes.object,
setCheckedWorkingGroups: PropTypes.func,
}
export default CheckboxFilters
\ No newline at end of file
import React from 'react';
import PropTypes from 'prop-types';
const CustomSearch = ({ setSearchValue }) => {
const handleSearchOnChange = (e) => {
setSearchValue(e.target.value)
}
return (
<div className="inner-addon left-addon">
<i className="fa fa-search"></i>
<input
type="text"
onChange={e => handleSearchOnChange(e)}
className="margin-bottom-10 margin-top-15 form-control"
placeholder="Search"
/>
</div>
)
}
CustomSearch.propTypes = {
setSearchValue: PropTypes.func.isRequired,
}
export default CustomSearch
\ No newline at end of file
import React, { useState } from 'react';
import PropTypes from 'prop-types';
import { hasAddress, generateDates, generateTimes } from './EventHelpers';
const EventCard = ({ event }) => {
const [showDetails, setShowDetails] = useState(false)
return (
<>
<div className={`event-card type-${event.type}`}>
<div className="all-margin-auto event-card-title-wrapper-display-IE">
<div className="event-card-decoration-dash"></div>
<h3 className="event-card-title">{event.title}</h3>
<p>
<i className={`fa fa-calendar-o fa-lg event-card-calendar-icon event-card-calendar-icon-${event.type}`} aria-hidden="true" />
{ generateDates(new Date(event.date), new Date(event['end-date'])) }
</p>
<p>
<i className={`fa fa-clock-o fa-lg event-card-calendar-icon event-card-calendar-icon-${event.type}`} aria-hidden="true"></i>
{ generateTimes(new Date(event.date), new Date(event['end-date'])) }
</p>
</div>
<button className="btn event-card-button" onClick={() => setShowDetails(!showDetails)}>Learn More</button>
</div>
{ showDetails ? <EventDetails event={event} /> : null }
</>
)
}
const EventDetails = ({ event }) => {
return (
<div className="bordered-box event-details">
<div className="margin-bottom-20">{event.description}</div>
{ hasAddress(event) && <div className="margin-bottom-20">Address: { event.address.city + " " + event.address.country } </div>}
<div className="text-center">
{/* <a className="btn btn-primary" href={event.infoLink}>More</a> */}
<a className="btn btn-primary" href={event.infoLink}>Register</a>
</div>
</div>
)
}
EventCard.propTypes = {
event: PropTypes.object.isRequired,
}
EventDetails.propTypes = {
event: PropTypes.object.isRequired,
}
export default EventCard
\ No newline at end of file
export const WORKING_GROUPS = [
{
id: "ascii_doc",
name: "AsciiDoc"
},
{
id: "ecd_tools",
name: "Eclipse Cloud Development Tools"
},
{
id: "edge_native",
name: "Edge Native"
},
{
id: "gemoc_rc",
name: "GEMOC RC"
},
{
id: "eclipse_iot",
name: "Eclipse IoT"
},
{
id: "jakarta_ee",
name: "Jakarta EE"
},
{
id: "openadx",
name: "OpenADx"
},
{
id: "opengenesis",
name: "OpenGENESIS"
},
{
id: "openhwgroup",
name: "OpenHW Group"
},
{
id: "openmdm",
name: "OpenMDM"
},
{
id: "openmobility",
name: "OpenMobility"
},
{
id: "openpass",
name: "OpenPass"
},
{
id: "science",
name: "Science"
},
{
id: "sparkplug",
name: "Sparkplug"
},
{
id: "tangle_ee",
name: "Tangle EE"
},
{
id: "eclipse_ide",
name: "Eclipse IDE"
},
{
id: "eclipse_org",
name: "Other"
}
]
export const EVENT_TYPES = [
{
id: "ec",
name: "EclipseCon"
},
{
id: "ve",
name: "Virtual Event"
},
{
id: "dc",
name: "Demo Camps & Stammtisch"
},
{
id: "wg",
name: "Working Group Events"
},
{
id: "et",
name: "Training Series"
},
{
id: "ee",
name: "Other interesting Events"
},
{
id: "unknown",
name: "Unknown"
},
]
export function checkEventWorkingGroups(events, filter) {
for (let i=0; i<events.length; i++) {
if (events[i].publish_to.includes(filter.id)) { // as long as find one event has the working group
return true
}
}
}
export function checkEventTypes(events, filter) {
for (let i=0; i<events.length; i++) {
if (events[i].type == filter.id) { // as long as find one event is of the type
return true
}
}
}
export function checkFilterHasEvents(filters, filterType, events) {
switch(filterType) {
case "WORKINGGROUPS":
return filters.filter(el => checkEventWorkingGroups(events, el))
case "EVENTTYPE":
return filters.filter(el => checkEventTypes(events, el))
default:
return
}
}
export function getSelectedItems(checkedItems) {
let selected = []
if (checkedItems) {
for (const property in checkedItems) {
if (checkedItems[property]) {
selected.push(property)
}
}
return selected
}
}
export function getEventsByWorkingGroups(checkedItems, events) {
let checked = getSelectedItems(checkedItems)
if (checked && checked.length > 0) {
let result = events.filter( el => checked.some(item => el.publish_to.includes(item)) )
return result
} else return events
}
export function getEventsByType(checkedItems, events) {
let checked = getSelectedItems(checkedItems)
if (checked && checked.length > 0) {
let result = events.filter( el => checked.includes(el.type) )
return result
} else return events
}
export function getSearchedEvents(events, searchValue) {
if (searchValue && searchValue != '') {
let result = events.filter(el => el.title.toLowerCase().includes(searchValue.toLowerCase()))
return result
} else {
return events
}
}
export function getFilteredEvents(events, searchValue, checkedWorkingGroups, checkedTypes) {
let selectedByWorkingGroups = getEventsByWorkingGroups(checkedWorkingGroups, events)
let selectedByTypes = getEventsByType(checkedTypes, selectedByWorkingGroups)
return getSearchedEvents(selectedByTypes, searchValue)
}
export function hasAddress(event) {
if (event.address && (event.address.city || event.address.country)) {
return true
} else return false
}
export function checkSameMonth(startDate, endDate) {
return startDate.getMonth() === endDate.getMonth()
}
export function checkSameDay(startDate, endDate) {
return checkSameMonth(startDate, endDate) && startDate.getDate() === endDate.getDate()
}
export function generateDate(date) {
if (date) {
return date.toLocaleDateString(undefined, { weekday: 'short', month: 'short', day: 'numeric' })
}
}
export function generateDates(startDate, endDate) {
if (endDate && !checkSameDay(startDate, endDate)) {
return generateDate(startDate) + " - " + generateDate(endDate) + ", " + startDate.getFullYear()
}
else {
return generateDate(startDate)
}
}
export function generateTime(time) {
if (time) {
return time.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
}
}
export function generateTimes(startDate, endDate) {
if (endDate && checkSameDay(startDate, endDate)) {
return generateTime(startDate) + " - " + generateTime(endDate)
}
else {
return generateTime(startDate)
}
}
import React, { useState, useEffect } from 'react';
import React, { useState } from 'react';
import EventCard from './EventCard';
import CustomSearch from './CustomSearch';
import CheckboxFilters from './CheckboxFilters';
import { getFilteredEvents } from './EventHelpers';
import PropTypes from 'prop-types';
const Events = () => {
// Based off https://reactjs.org/docs/faq-ajax.html
const Events = ({ events }) => {
const [error, setError] = useState(null);
const [isLoaded, setIsLoaded] = useState(false);
const [events, setEvents] = useState([]);
const [searchValue, setSearchValue] = useState('')
const [checkedWorkingGroups, setCheckedWorkingGroups] = useState({})
const [checkedTypes, setCheckedTypes] = useState({})
// Note: the empty deps array [] means
// this useEffect will run once
// similar to componentDidMount()
useEffect(() => {
fetch('https://newsroom.eclipse.org/api/events?parameters[upcoming_only]=1')
.then((res) => res.json())
.then(
(result) => {
setIsLoaded(true);
setEvents(result.events);
},
// Note: it's important to handle errors here
// instead of a catch() block so that we don't swallow
// exceptions from actual bugs in components.
(error) => {
setIsLoaded(true);
setError(error);
}
);
}, []);
return (
<div className="container">
<div className="row margin-bottom-20">
<div className="col-md-6">
{/* Filters will be here */}
<CustomSearch searchValue={searchValue} setSearchValue={setSearchValue} />
<CheckboxFilters checkedTypes={checkedTypes} setCheckedTypes={setCheckedTypes} events={events} />
<CheckboxFilters checkedWorkingGroups={checkedWorkingGroups} setCheckedWorkingGroups={setCheckedWorkingGroups} events={events} />
<a className="btn btn-primary" href="https://newsroom.eclipse.org/node/add/events">Submit Your Event</a>
</div>
if (error) {
return <div>Error: {error.message}</div>;