forked from pdx-cocoaheads/pdxcocoaheads.com
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Initial pass at pulling events list from Meetup and displaying, addre…
…ssing pdx-cocoaheads#4. Droplet.client makes the request to the Meetup API, which returns a list in JSON. The JSON is unpacked into instances of `MeetupEvent`. `MeetupEvent` currently stores just the fields known to be needed for the template, and implements `MustacheBoxable` so that it can be included directly in the rendering context. Created Mustache template and stylesheet for the page, with two filters to make numerical data look good. Added link to new page to homepage nav. Added basic testing for new functionality: Mustache filters and `MeetupEvent`. Needs further structural refinement: currently too much is done inside the route handler.
- Loading branch information
Josh Caswell
committed
Aug 4, 2016
1 parent
f7b645b
commit c1f5a45
Showing
10 changed files
with
547 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
import Foundation | ||
import Mustache | ||
|
||
/** Namespace/vendor for FilterFunctions needed for the Events page. */ | ||
struct EventsMustacheFilters { | ||
|
||
/** | ||
* All filters used in the upcoming-events.mustache template, boxed and | ||
* ready to be added to the Mustache rendering context. | ||
*/ | ||
static var asContext: [String : MustacheBox] { | ||
return ["formattedDate" : Box(filter: formattedDateFilter), | ||
"pluralizedRSVP" : Box(filter: pluralizedRSVPFilter)] | ||
} | ||
|
||
/** Fallback for missing timezone info. */ | ||
// Note: this may be overly defensive; currently a `MeetupEvent` cannot | ||
// be created without a timezone. | ||
private static let PDXStandardUTCOffset = -25200000 | ||
|
||
/** | ||
* Format the event's millisecond timestamp into a nice description, using | ||
* the event's timezone information. | ||
*/ | ||
static var formattedDateFilter: FilterFunction = Filter { | ||
|
||
(time: Int?, info: RenderingInfo) in | ||
|
||
guard let milliseconds = time else { | ||
return Rendering("Unknown date") | ||
} | ||
|
||
// Context's MustacheBox wraps a MeetupEvent | ||
let offsetBox = info.context.mustacheBox(forKey: "utcOffset") | ||
let utcOffset = (offsetBox.value as? Int) ?? PDXStandardUTCOffset | ||
|
||
let date = Date(timeIntervalSince1970: Double(milliseconds/1000)) | ||
let formatter = DateFormatter() | ||
// UTC offset is also milliseconds | ||
formatter.timeZone = TimeZone(forSecondsFromGMT: utcOffset/1000) | ||
formatter.dateStyle = .full | ||
formatter.timeStyle = .short | ||
|
||
return Rendering(formatter.string(from: date)) | ||
} | ||
|
||
/** Change the inner text of the RSVP description based on the count. */ | ||
// This snazzy idea cribbed straight out of the Mustache docs. | ||
// See Mustache/Rendering/CoreFunctions.swift#L310 | ||
static var pluralizedRSVPFilter: FilterFunction = Filter { | ||
|
||
(count: Int?, info: RenderingInfo) in | ||
|
||
var peoples: String = "people are" | ||
if count == 1 { | ||
peoples = "person is" | ||
} | ||
return Rendering(peoples) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,82 @@ | ||
import Mustache | ||
import Vapor | ||
|
||
/** | ||
* Holder for values returned from Meetup API that we want to display on | ||
* "Upcoming Events" page. | ||
*/ | ||
struct MeetupEvent { | ||
/** Event title */ | ||
let name: String | ||
/** Event date in milliseconds since epoch */ | ||
let time: Int | ||
/** Event location timezone's milliseconds from UTC */ | ||
let utcOffset: Int | ||
/** Number of people who have said they're attending */ | ||
let rsvpCount: Int | ||
/** URL (as string) of event's page on Meetup.com */ | ||
let link: String | ||
} | ||
|
||
extension MeetupEvent: MustacheBoxable { | ||
|
||
/** Allow lookup of fields by name in Mustache rendering context */ | ||
func mustacheBox(forKey key: String) -> MustacheBox { | ||
|
||
switch key { | ||
case "name": return Box(boxable: self.name) | ||
case "time": return Box(boxable: self.time) | ||
case "utcOffset": return Box(boxable: self.utcOffset) | ||
case "rsvpCount": return Box(boxable: self.rsvpCount) | ||
case "link": return Box(boxable: self.link) | ||
default: return Box() | ||
} | ||
} | ||
|
||
var mustacheBox: MustacheBox { | ||
return MustacheBox(value: self, | ||
keyedSubscript: self.mustacheBox(forKey:)) | ||
} | ||
} | ||
|
||
/** Errors during unpacking of Meetup API's returned JSON data. */ | ||
enum MeetupEventUnpackError : ErrorProtocol { | ||
case NotAnArray(JSON) | ||
} | ||
|
||
/** | ||
* Transform an array of JSON-coded events returned from Meetup into | ||
* `MeetupEvent` instances, skipping any that are malformed. | ||
* | ||
* - Parameter fromJSON: Vapor.JSON wrapping an array of objects | ||
* | ||
* - Throws: `MeetupEventUnpackError.NotAnArray` if the `fromJSON` parameter | ||
* cannot be unwrapped into a Swift array | ||
* | ||
* - Returns: An array of `MeetupEvent`s in the order the event descriptions | ||
* were presented in the JSON | ||
*/ | ||
func unpackMeetupEvents(fromJSON json: JSON) throws -> [MeetupEvent] { | ||
|
||
guard let events: [JSON] = json.array else { | ||
throw MeetupEventUnpackError.NotAnArray(json) | ||
} | ||
|
||
return events.flatMap { eventJSON in | ||
|
||
// Lenient unpacking: just skip an individual item if it's not usable | ||
guard let event = eventJSON.object, | ||
let name = event["name"].string, | ||
let time = event["time"].int, | ||
let utcOffset = event["utc_offset"].int, | ||
let rsvpCount = event["yes_rsvp_count"].int, | ||
let link = event["link"].string | ||
else { | ||
|
||
return nil | ||
} | ||
|
||
return MeetupEvent(name: name, time: time, utcOffset: utcOffset, | ||
rsvpCount: rsvpCount, link: link) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
{ | ||
"hostname" : "api.meetup.com", | ||
"events-queries" : { | ||
"all" : "PDX-iOS-CocoaHeads/events" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -11,7 +11,7 @@ let package = Package( | |
"Database", | ||
"Localization", | ||
"Public", | ||
"Resources", | ||
"Resources" | ||
] | ||
) | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
.rsvp-desc { | ||
font-size: smaller; | ||
} | ||
|
||
.meetup-link { | ||
font-size: smaller; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
<!DOCTYPE html> | ||
<html> | ||
<head> | ||
<meta charset="utf-8"> | ||
<meta http-equiv="X-UA-Compatible" content="IE=edge"> | ||
<meta name="description" content="{{ site.description }}"> | ||
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | ||
<title>PDX CocoaHeads Events</title> | ||
<link rel="stylesheet" href="css/main.css"> | ||
<link rel="stylesheet" href="css/upcoming-events.css"> | ||
</head> | ||
<body> | ||
<div class="container"> | ||
<nav> | ||
<ul> | ||
<li><a href="/">Home</a></li> | ||
<li><a href="#">Events</a></li> | ||
<li><a href="https://github.com/pdx-cocoaheads/policies/blob/master/citizen_code_of_conduct.md">Code of Conduct</a></li> | ||
</ul> | ||
</nav> | ||
<h2>Upcoming Events</h2> | ||
<hr/> | ||
<div class="event-list"> | ||
{{#events}} | ||
<p> | ||
<strong class="event-title">{{name}}</strong><br/> | ||
<em>Date:</em> {{formattedDate(time)}}<br/> | ||
<span class="rsvp-desc" style="float: left"> | ||
{{rsvpCount}} {{ pluralizedRSVP(rsvpCount) }} coming | ||
</span> | ||
<span class="meetup-link" style="float: right"> | ||
<a href="{{link}}">RSVP on Meetup.com</a> | ||
</span> | ||
<hr/> | ||
</p> | ||
{{/events}} | ||
{{^events}} | ||
We don't seem to have any events scheduled right now. | ||
{{/events}} | ||
</div> | ||
</div> | ||
</body> | ||
</html> |
Oops, something went wrong.