Skip to content

Commit efc15aa

Browse files
committed
Split conferences and CFPs
On the /conferences/ page, split out views for conferences (for attendees) and CFPs (for speakers)
1 parent 1d6659b commit efc15aa

5 files changed

Lines changed: 423 additions & 24 deletions

File tree

‎public/conferences/index.php‎

Lines changed: 123 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
<?php
22

3+
use phpweb\News\ConferenceSchedule;
34
use phpweb\News\NewsHandler;
45

56
$_SERVER['BASE_PAGE'] = 'conferences/index.php';
@@ -14,33 +15,141 @@
1415
'css' => ['home.css'],
1516
]);
1617

17-
$content = "<div class='home-content'>";
18-
$panels = '<p class="prepend"><a href="https://wiki.php.net/conferences">Want to see your conference appear here?</a></p>';
18+
$today = gmdate('Y-m-d');
19+
$schedule = new ConferenceSchedule((new NewsHandler())->getConferences(), $today);
20+
21+
// The page has two views, switched via ?view=: the default view, which lists
22+
// conferences only (targeted at attendees/sponsors), and the call-for-papers
23+
// view, which lists CFPs only, split into open and closed (targeting speakers).
24+
$view = ($_GET['view'] ?? null) === 'cfps' ? 'cfps' : 'conferences';
25+
26+
// Build nav for switching between views
27+
$viewLinks = [
28+
'conferences' => ['label' => 'Conferences', 'href' => '/conferences/'],
29+
'cfps' => ['label' => 'Calls for papers', 'href' => '/conferences/?view=cfps'],
30+
];
31+
32+
$viewSwitcher = '<nav class="conference-views" aria-label="Conference views">';
33+
foreach ($viewLinks as $key => $viewLink) {
34+
$isActive = $key === $view;
35+
$viewSwitcher .= sprintf(
36+
'<a class="conference-view%s" href="%s"%s>%s</a>',
37+
$isActive ? ' is-active' : '',
38+
$viewLink['href'],
39+
$isActive ? ' aria-current="page"' : '',
40+
$viewLink['label'],
41+
);
42+
}
43+
$viewSwitcher .= '</nav>';
1944

20-
foreach ((new NewsHandler())->getConferences() as $entry) {
45+
$renderEntry = static function (array $entry) use ($MYSITE, $today): string {
2146
$link = preg_replace('~^(http://php.net/|https://www.php.net/)~', '', $entry["id"]);
2247
$id = parse_url($entry["id"], PHP_URL_FRAGMENT);
23-
$date = date_format(date_create($entry["updated"] ?? $entry["published"]), 'Y-m-d');
24-
$content .= '<div class="newsentry">';
25-
$content .= '<h3 class="newstitle title"><a href="' . $MYSITE . $link . '" id="' . $id . '">' . $entry["title"] . '</a></h3>';
26-
$content .= '<div class="newsimage">';
48+
49+
$html = '<div class="newsentry">';
50+
$html .= '<h3 class="newstitle title"><a href="' . $MYSITE . $link . '" id="' . $id . '">' . $entry["title"] . '</a></h3>';
51+
52+
if (isset($entry["finalTeaserDate"])) {
53+
$isPast = $entry["finalTeaserDate"] < $today;
54+
if (ConferenceSchedule::termFor($entry) === 'cfp') {
55+
$label = $isPast ? 'CFP closed' : 'CFP closes';
56+
} else {
57+
$label = $isPast ? 'Started' : 'Starts';
58+
}
59+
$html .= '<p class="newsdate">' . $label . ' <time datetime="' . $entry["finalTeaserDate"] . '">' . date_format(date_create($entry["finalTeaserDate"]), 'd M Y') . '</time></p>';
60+
}
61+
62+
$html .= '<div class="newsimage">';
2763

2864
if (isset($entry["newsImage"])) {
29-
$content .= sprintf('<a href="%s"><img src="/images/news/%s"></a>', $entry["newsImage"]["link"], $entry["newsImage"]["content"]);
65+
$html .= sprintf('<a href="%s"><img src="/images/news/%s"></a>', $entry["newsImage"]["link"], $entry["newsImage"]["content"]);
3066
}
3167

32-
$content .= '</div>';
33-
$content .= '<div class="newscontent">';
34-
$content .= $entry["content"];
35-
$content .= '</div>';
36-
$content .= '</div>';
68+
$html .= '</div>';
69+
$html .= '<div class="newscontent">';
70+
$html .= $entry["content"];
71+
$html .= '</div>';
72+
$html .= '</div>';
73+
74+
return $html;
75+
};
76+
77+
$content = "<div class='home-content'>";
78+
$content .= $viewSwitcher;
79+
80+
if ($view === 'cfps') {
81+
$content .= '<h2>Open calls for papers</h2>';
82+
83+
if ($schedule->openCallsForPapers === []) {
84+
$content .= '<p>No open calls for papers right now.</p>';
85+
} else {
86+
foreach ($schedule->openCallsForPapers as $entry) {
87+
$content .= $renderEntry($entry);
88+
}
89+
}
90+
91+
if ($schedule->closedCallsForPapers !== []) {
92+
$content .= '<h2 class="conferences-divider">Closed calls for papers</h2>';
93+
94+
foreach ($schedule->closedCallsForPapers as $entry) {
95+
$content .= $renderEntry($entry);
96+
}
97+
}
98+
} else {
99+
$content .= '<h2>Upcoming conferences</h2>';
37100

38-
$panels .= sprintf('<p class="panel"><a href="%s">%s</a></p>', $entry["newsImage"]["link"] ?? $link, $entry["title"]);
101+
if ($schedule->upcomingConferences === []) {
102+
$content .= '<p>No upcoming conferences right now.</p>';
103+
} else {
104+
foreach ($schedule->upcomingConferences as $entry) {
105+
$content .= $renderEntry($entry);
106+
}
107+
}
108+
109+
if ($schedule->pastConferences !== []) {
110+
$content .= '<h2 class="conferences-divider">Past conferences</h2>';
111+
112+
foreach ($schedule->pastConferences as $entry) {
113+
$content .= $renderEntry($entry);
114+
}
115+
}
39116
}
117+
40118
$content .= "</div>";
41119

42120
echo $content;
43121

122+
// Build sidebar mirroring the active view: conferences grouped into upcoming/past, or CFPs into open/closed.
123+
$panels = '<p class="prepend"><a href="https://wiki.php.net/conferences">Want to see your conference appear here?</a></p>';
124+
125+
$renderPanels = static function (array $entries): string {
126+
$html = '';
127+
foreach ($entries as $entry) {
128+
$link = preg_replace('~^(http://php.net/|https://www.php.net/)~', '', $entry["id"]);
129+
$html .= sprintf('<p class="panel"><a href="%s">%s</a></p>', $entry["newsImage"]["link"] ?? $link, $entry["title"]);
130+
}
131+
132+
return $html;
133+
};
134+
135+
if ($view === 'cfps') {
136+
if ($schedule->openCallsForPapers !== []) {
137+
$panels .= '<h3>Open</h3>' . $renderPanels($schedule->openCallsForPapers);
138+
}
139+
140+
if ($schedule->closedCallsForPapers !== []) {
141+
$panels .= '<h3>Closed</h3>' . $renderPanels($schedule->closedCallsForPapers);
142+
}
143+
} else {
144+
if ($schedule->upcomingConferences !== []) {
145+
$panels .= '<h3>Upcoming</h3>' . $renderPanels($schedule->upcomingConferences);
146+
}
147+
148+
if ($schedule->pastConferences !== []) {
149+
$panels .= '<h3>Past</h3>' . $renderPanels($schedule->pastConferences);
150+
}
151+
}
152+
44153
site_footer([
45154
"atom" => "/feed.atom",
46155
"elephpants" => true,

‎public/styles/home.css‎

Lines changed: 48 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -90,22 +90,22 @@
9090
}
9191

9292
.hero__versions a.notes {
93-
font-size:.875rem;
94-
white-space:nowrap;
93+
font-size: .875rem;
94+
white-space: nowrap;
9595
}
9696

9797
.hero__versions a {
9898
color: var(--background-text-color);
99-
border:0;
99+
border: 0;
100100
}
101101

102102
.hero__versions a:hover,
103103
.hero__versions a:focus {
104-
border-bottom:1px dotted;
104+
border-bottom: 1px dotted;
105105
}
106106

107107
.hero__version-link {
108-
color:#fff !important;
108+
color: #fff !important;
109109
display: inline-block;
110110
}
111111

@@ -151,11 +151,12 @@
151151
/* Layout */
152152

153153
#layout-content {
154-
border-right:.25rem solid #666;
154+
border-right: .25rem solid #666;
155155
}
156+
156157
.home .newsentry .newstitle a:after {
157-
content:"\20 \00bb";
158-
color:#666;
158+
content: "\20 \00bb";
159+
color: #666;
159160
}
160161

161162
p.archive {
@@ -180,7 +181,7 @@ p.archive {
180181
}
181182

182183
#layout-content {
183-
width:70%;
184+
width: 70%;
184185
}
185186
}
186187

@@ -216,3 +217,41 @@ aside.tips .social-media .icon-twitter {
216217
background-color: var(--dark-magenta-color) !important;
217218
border-color: var(--dark-magenta-color) !important;
218219
}
220+
221+
/* Conference pages */
222+
.conference-views {
223+
display: flex;
224+
justify-content: flex-end;
225+
gap: 1.5rem;
226+
margin: 0 0 1.5rem;
227+
border-bottom: 1px solid #ccc;
228+
}
229+
230+
.conference-views .conference-view {
231+
padding: .5rem .25rem;
232+
margin-bottom: -1px;
233+
border-bottom: 3px solid transparent;
234+
color: #666;
235+
text-decoration: none;
236+
}
237+
238+
.conference-views .conference-view.is-active {
239+
border-bottom-color: var(--dark-blue-color);
240+
color: var(--dark-grey-color);
241+
font-weight: 600;
242+
}
243+
244+
.newsentry .newsdate {
245+
margin: -.75rem 0 .75rem;
246+
color: #666;
247+
font-size: .75rem;
248+
font-weight: 600;
249+
letter-spacing: .04em;
250+
text-transform: uppercase;
251+
}
252+
253+
h2.conferences-divider {
254+
margin-top: 2rem;
255+
padding-top: 1rem;
256+
border-top: 1px solid #ccc;
257+
}

‎src/News/ConferenceSchedule.php‎

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace phpweb\News;
6+
7+
use function usort;
8+
9+
/**
10+
* Groups and orders conference/CFP news entries for the /conferences page and its CFP view.
11+
*
12+
* Conferences and calls for papers are separate, unlinked news entries, each with a single
13+
* finalTeaserDate that means different things: for a conference it's the event's start date,
14+
* for a CFP it's the submission deadline. They live on different pages:
15+
*
16+
* - The conferences view shows conferences only, split by start date into upcoming (soonest
17+
* first) and past (most recent first). A conference with no finalTeaserDate falls into past.
18+
* - The CFP view shows CFPs only, split by deadline into open (deadline today or later, soonest
19+
* first) and closed (deadline passed, or no deadline, most recently closed first).
20+
*
21+
* finalTeaserDate nuance: for a conference it's start date, for a CFP it's end date
22+
*/
23+
final class ConferenceSchedule
24+
{
25+
/** @var list<array> Conferences starting today or later, soonest first. */
26+
public private(set) array $upcomingConferences = [];
27+
28+
/** @var list<array> Conferences already started (or undated), most recent first. */
29+
public private(set) array $pastConferences = [];
30+
31+
/** @var list<array> CFPs whose submission deadline has not passed, soonest deadline first. */
32+
public private(set) array $openCallsForPapers = [];
33+
34+
/** @var list<array> CFPs whose deadline has passed (or is unknown), most recently closed first. */
35+
public private(set) array $closedCallsForPapers = [];
36+
37+
/**
38+
* @param list<array> $entries conference/CFP entries, e.g. from NewsHandler::getConferences()
39+
* @param string $today today's date as 'Y-m-d'
40+
*/
41+
public function __construct(array $entries, string $today)
42+
{
43+
foreach ($entries as $entry) {
44+
$this->assignConferenceOrCfp($entry, $today);
45+
}
46+
47+
$this->upcomingConferences = self::sortedByDate($this->upcomingConferences, ascending: true);
48+
$this->openCallsForPapers = self::sortedByDate($this->openCallsForPapers, ascending: true);
49+
$this->pastConferences = self::sortedByDate($this->pastConferences, ascending: false);
50+
$this->closedCallsForPapers = self::sortedByDate($this->closedCallsForPapers, ascending: false);
51+
}
52+
53+
/**
54+
* The entry's primary term. A few posts announce a conference and open its CFP at once,
55+
* carrying both terms; anything that is a conference is treated as a conference (and so
56+
* placed by its date), leaving 'cfp' for CFP-only posts.
57+
*
58+
* @param array<array-key, mixed> $entry
59+
*/
60+
public static function termFor(array $entry): ?string
61+
{
62+
$found = null;
63+
foreach ($entry['category'] as $category) {
64+
if ($category['term'] === 'conferences') {
65+
return 'conferences';
66+
}
67+
68+
if ($category['term'] === 'cfp') {
69+
$found = 'cfp';
70+
}
71+
}
72+
73+
return $found;
74+
}
75+
76+
/**
77+
* @param array<array-key, mixed> $conferenceOrCfp
78+
*/
79+
private function assignConferenceOrCfp(array $conferenceOrCfp, string $today): void
80+
{
81+
$isFuture = isset($conferenceOrCfp['finalTeaserDate']) && $conferenceOrCfp['finalTeaserDate'] >= $today;
82+
$isCfp = self::termFor($conferenceOrCfp) === 'cfp';
83+
84+
match (true) {
85+
$isCfp && $isFuture => $this->openCallsForPapers[] = $conferenceOrCfp,
86+
$isCfp => $this->closedCallsForPapers[] = $conferenceOrCfp,
87+
$isFuture => $this->upcomingConferences[] = $conferenceOrCfp,
88+
default => $this->pastConferences[] = $conferenceOrCfp,
89+
};
90+
}
91+
92+
/**
93+
* Sort entries by finalTeaserDate ('Y-m-d'), keeping undated entries last.
94+
*
95+
* @param list<array> $entries
96+
*
97+
* @return list<array>
98+
*/
99+
private static function sortedByDate(array $entries, bool $ascending): array
100+
{
101+
usort($entries, static function (array $a, array $b) use ($ascending): int {
102+
$aDate = $a['finalTeaserDate'] ?? '';
103+
$bDate = $b['finalTeaserDate'] ?? '';
104+
105+
// Push undated entries to the end whichever way the dated ones are ordered.
106+
if ($aDate === '' || $bDate === '') {
107+
return ($aDate === '' ? 1 : 0) <=> ($bDate === '' ? 1 : 0);
108+
}
109+
110+
return $ascending ? $aDate <=> $bDate : $bDate <=> $aDate;
111+
});
112+
113+
return $entries;
114+
}
115+
}

‎src/News/Entry.php‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,12 @@ public function save(): self {
111111
$dom->preserveWhiteSpace = false;
112112
$item = $dom->createElementNs("http://www.w3.org/2005/Atom", "entry");
113113

114-
$href = self::WEBROOT . ($this->isConference() ? '/conferences/index.php' : '/index.php');
114+
// Build the appropriate deep link depending on whether it's news, conference, or CFP
115+
$href = self::WEBROOT . match (true) {
116+
in_array('conferences', $this->categories, true) => '/conferences/index.php',
117+
in_array('cfp', $this->categories, true) => '/conferences/index.php?view=cfps',
118+
default => '/index.php',
119+
};
115120
$archive = self::WEBROOT . "/archive/" . date('Y', $_SERVER['REQUEST_TIME']) . ".php#{$this->id}";
116121
$link = ($this->image['link'] ?? null) ?: $archive;
117122

0 commit comments

Comments
 (0)