Skip to content

Commit be0502e

Browse files
Landing page design v2 (#1)
1 parent 661b0fc commit be0502e

File tree

9 files changed

+490
-253
lines changed

9 files changed

+490
-253
lines changed

.github/workflows/deploy.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ on:
99
jobs:
1010
build:
1111
env:
12+
CAPTIVATE_USERNAME: ${{ secrets.CAPTIVATE_USERNAME }}
13+
CAPTIVATE_TOKEN: ${{ secrets.CAPTIVATE_TOKEN }}
1214
GOOGLE_CALENDAR_API_KEY: ${{ secrets.GOOGLE_CALENDAR_API_KEY }}
1315
runs-on: ubuntu-latest
1416
steps:

bin/flutter_spaces.dart

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
1+
import 'dart:io';
2+
13
import 'package:flutter_spaces/flutter_spaces.dart';
24
import 'package:static_shock/static_shock.dart';
35

46
Future<void> main(List<String> arguments) async {
7+
// Load Captivate credentials.
8+
final podcastShowId = "3743ba71-859c-4164-99ff-999b525ccf48";
9+
final (captivateUsername, captivateApiKey) = _loadCaptivateCredentials();
10+
511
// Configure the static website generator.
612
final staticShock = StaticShock()
713
..pick(DirectoryPicker.parse("images"))
@@ -10,8 +16,34 @@ Future<void> main(List<String> arguments) async {
1016
..plugin(const PrettyUrlsPlugin())
1117
..plugin(const SassPlugin())
1218
// Load data for the next scheduled Flutter space.
13-
..loadData(const FlutterSpacesCalendarLoader());
19+
..loadData(const FlutterSpacesCalendarLoader())
20+
// Load all episodes.
21+
..loadData(EpisodeLoader(
22+
captivateUsername: captivateUsername,
23+
captivateApiToken: captivateApiKey,
24+
showId: podcastShowId,
25+
));
1426

1527
// Generate the static website.
1628
await staticShock.generateSite();
1729
}
30+
31+
(String username, String apiToken) _loadCaptivateCredentials() {
32+
// REQUIRED: Environment variable called "captivate_username"
33+
final captivateUsername = Platform.environment["CAPTIVATE_USERNAME"] ?? Platform.environment["captivate_username"];
34+
35+
// REQUIRED: Environment variable called "captivate_token"
36+
final captivateApiToken = Platform.environment["CAPTIVATE_TOKEN"] ?? Platform.environment["captivate_token"];
37+
38+
if (captivateUsername == null) {
39+
print("Missing environment variable for Captivate username.");
40+
exit(-1);
41+
}
42+
43+
if (captivateApiToken == null) {
44+
print("Missing environment variable for Captivate API token.");
45+
exit(-1);
46+
}
47+
48+
return (captivateUsername, captivateApiToken);
49+
}

lib/flutter_spaces.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
export 'src/google_calendar.dart';
2+
export 'src/podcast_episodes.dart';

lib/src/podcast_episodes.dart

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import 'package:captivate/captivate.dart';
2+
import 'package:intl/intl.dart';
3+
import 'package:static_shock/static_shock.dart';
4+
5+
class EpisodeLoader implements DataLoader {
6+
EpisodeLoader({
7+
required this.captivateUsername,
8+
required this.captivateApiToken,
9+
required this.showId,
10+
});
11+
12+
final _client = CaptivateClient();
13+
final String captivateUsername;
14+
final String captivateApiToken;
15+
final String showId;
16+
17+
@override
18+
Future<Map<String, Object>> loadData(StaticShockPipelineContext context) async {
19+
context.log.info("Loading podcast episodes...");
20+
21+
context.log.detail("Authenticating with Captivate...");
22+
final userPayload = await _client.authenticate(
23+
username: captivateUsername,
24+
apiToken: captivateApiToken,
25+
);
26+
final authToken = userPayload!.user.token;
27+
context.log.detail("Done authenticating with Captivate");
28+
29+
context.log.detail("Requesting all episodes from Captivate...");
30+
final response = await _client.getEpisodes(authToken, showId);
31+
if (response == null) {
32+
context.log.warn("Failed to load episodes for show: $showId");
33+
return {
34+
"episodes": [],
35+
};
36+
}
37+
context.log.detail("Loaded ${response.episodes.length} episodes.");
38+
39+
context.log.info("Done loading episodes from Captivate.");
40+
41+
return {
42+
// Ordered from most recent to least recent.
43+
"loadedEpisodes": [
44+
for (final episode in response.episodes) //
45+
{
46+
"id": episode.id,
47+
"title": episode.title,
48+
"date": episode.formattedPublishedDate,
49+
}
50+
],
51+
};
52+
}
53+
}
54+
55+
extension on Episode {
56+
static final captivateDateFormat = DateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'");
57+
static final presentationDateFormat = DateFormat("MMMM dd, yyyy");
58+
59+
String get formattedPublishedDate {
60+
if (publishedDate == null) {
61+
return "Now Available";
62+
}
63+
64+
final dateTime = captivateDateFormat.parse(publishedDate!);
65+
return presentationDateFormat.format(dateTime);
66+
}
67+
}

pubspec.lock

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,15 @@ packages:
6262
url: "https://pub.dev"
6363
source: hosted
6464
version: "2.1.2"
65+
captivate:
66+
dependency: "direct main"
67+
description:
68+
path: "."
69+
ref: HEAD
70+
resolved-ref: "5b4f2fcbff928758048837bf489307617d748e1d"
71+
url: "https://github.com/Flutter-Bounty-Hunters/captivate"
72+
source: git
73+
version: "0.1.0"
6574
charcode:
6675
dependency: transitive
6776
description:
@@ -279,7 +288,7 @@ packages:
279288
source: hosted
280289
version: "4.3.0"
281290
intl:
282-
dependency: transitive
291+
dependency: "direct main"
283292
description:
284293
name: intl
285294
sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf
@@ -346,10 +355,10 @@ packages:
346355
dependency: transitive
347356
description:
348357
name: markdown
349-
sha256: ef2a1298144e3f985cc736b22e0ccdaf188b5b3970648f2d9dc13efd1d9df051
358+
sha256: "935e23e1ff3bc02d390bad4d4be001208ee92cc217cb5b5a6c19bc14aaa318c1"
350359
url: "https://pub.dev"
351360
source: hosted
352-
version: "7.2.2"
361+
version: "7.3.0"
353362
mason_logger:
354363
dependency: transitive
355364
description:
@@ -514,10 +523,10 @@ packages:
514523
dependency: transitive
515524
description:
516525
name: sass
517-
sha256: c6abff6269edf6dc6767fde2ead064cbf939b8982656bd6a2097eda5d9c8f42b
526+
sha256: "4577992b848d9bc5f4934dc30a73bb078d3f1f73c16d7b36bffdce220c1733c8"
518527
url: "https://pub.dev"
519528
source: hosted
520-
version: "1.83.1"
529+
version: "1.83.4"
521530
shelf:
522531
dependency: transitive
523532
description:

pubspec.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,12 @@ dependencies:
1111

1212
googleapis: ^13.2.0
1313
googleapis_auth: ^1.6.0
14+
intl: ^0.19.0
15+
16+
captivate:
17+
git:
18+
url: https://github.com/Flutter-Bounty-Hunters/captivate
19+
1420

1521
dev_dependencies:
1622
lints: ^2.0.0
264 KB
Loading

source/index.jinja

Lines changed: 63 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -36,79 +36,64 @@
3636

3737
<body>
3838
<main>
39-
<header>
40-
<div class="foreground">
41-
<img src="images/homepage/logo_circle.png" class="logo">
42-
43-
<div class="title">WEEKLY FLUTTER SPACE</div>
44-
45-
<!-- This spacer has size zero unless screen is narrow -->
46-
<!-- On narrow screen, this spacer takes up the same height as the phone image -->
47-
<div class="spacer-for-narrow">&nbsp;</div>
48-
49-
<div class="upsell">Join us every week for &NewLine; any and all Flutter topics!</div>
50-
51-
<div class="hosts">
52-
<p class="title">HOSTS</p>
53-
<div class="host-container">
54-
<div class="host matt">
55-
<img src="/images/homepage/photo_matt.png">
56-
<p>Matt Carroll</p>
57-
</div>
58-
<div class="host ray">
59-
<img src="/images/homepage/photo_ray.png">
60-
<p>Ray Li</p>
61-
</div>
62-
</div>
39+
<div id="mainPane">
40+
<div style="flex: 2;">&nbsp;</div>
41+
42+
<header>
43+
<img id="mascot" src="/images/homepage/dash-astronaut.png">
44+
<h1>Flutter Spaces</h1>
45+
<div class="divider">&nbsp;</div>
46+
<span class="subtitle">Weekly Flutter Conversations</span>
47+
</header>
48+
49+
<div style="flex: 2;">&nbsp;</div>
50+
51+
<div id="episodePlayer">
52+
<div id="playerContainer" style="margin: auto; max-width: 500px; height: 200px; overflow: hidden;">
53+
<iframe style="width: 100%; height: 200px;" frameborder="no" scrolling="no" allow="clipboard-write" seamless src="https://player.captivate.fm/episode/{{ loadedEpisodes[0].id }}"></iframe>
6354
</div>
6455
</div>
6556

66-
<div class="background">
67-
<div class="waves-and-phone">
68-
<div class="sound-waves-left">&nbsp;</div>
69-
<div class="phone">
70-
<img src="/images/homepage/phone-space-preview.png">
71-
72-
<div class="next-show-date">
73-
<p class="title">NEXT SPACE</p>
74-
<!-- This display date is filled by JS -->
75-
<p id="nextShowDate" class="date">&nbsp;</p>
76-
</div>
57+
<div style="flex: 2;">&nbsp;</div>
58+
59+
<div id="nextShowCalendar">
60+
<div style="display: flex; flex-direction: row; align-items: flex-start;">
61+
<div style="font-size: 30px; margin-right: 8px; margin-top:-4px;">📅</div>
62+
<div style="display: flex; flex-direction: column;">
63+
<h3>Next Space</h3>
64+
<p id="nextShowDate">&nbsp;</p>
7765
</div>
78-
<div class="sound-waves-right">&nbsp;</div>
7966
</div>
67+
<a target="_blank" href="https://calendar.google.com/calendar/event?action=TEMPLATE&amp;tmeid=NWxsNHN2dHZqZ2U0Zm1xZWZkM3ZrMzdsdGRfMjAyNTAxMjJUMjAwMDAwWiBlODJhNDE5M2IwODVhOWYwOTAyMjU5YzJmNjIwNDkxYjUyYTk1MmUxMWQxOTRmMTkzODM0ZTMxZmRhZDE1N2NiQGc&amp;tmsrc=e82a4193b085a9f0902259c2f620491b52a952e11d194f193834e31fdad157cb%40group.calendar.google.com&amp;scp=ALL">Add to Calendar</a>
8068
</div>
81-
</header>
8269

83-
<div id="x-divider" style="display: flex;">
84-
<hr class="divider" style="flex: 1; align-self: center;">
85-
<img src="/images/homepage/x-logo.png" class="logo">
86-
<hr class="divider" style="flex: 1; align-self: center;">
87-
</div>
70+
<div style="flex: 2;">&nbsp;</div>
8871

89-
<p class="follow-hosts">Follow <a href="https://x.com/suprdeclarative" target="_blank">Matt</a> and <a href="https://x.com/RayLiVerified" target="_blank">Ray</a> on X</p>
90-
91-
<div id="post-timeline">
92-
{% for episode in episodes %}
93-
94-
{% if episode.podcast_id %}
95-
<div style="margin: auto; max-width: 500px; height: 200px; margin-bottom: 20px; border-radius: 6px; overflow: hidden;"><iframe style="width: 100%; height: 200px;" frameborder="no" scrolling="no" allow="clipboard-write" seamless src="https://player.captivate.fm/episode/{{ episode.podcast_id }}"></iframe></div>
96-
{% endif %}
97-
98-
<br><br><br>
99-
{% endfor %}
72+
<div id="attribution">
73+
<p>Built with ❤️ by <a href="https://superdeclarative.com" target="_blank">Matt Carroll</a></p>
74+
</div>
75+
</div>
10076

101-
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>
77+
<div id="episodeDirectoryPane">
78+
<div id="episodeDirectoryWindow">
79+
{% for episode in loadedEpisodes %}
80+
<div class="episode-list-item" episodeId="{{ episode.id }}">
81+
<div class="content">
82+
<h4>{{ episode.title }}</h4>
83+
<p class="date">{{ episode.date }}</p>
84+
</div>
85+
</div>
86+
{% endfor %}
87+
</div>
10288
</div>
10389
</main>
104-
<footer>
105-
<hr class="divider">
106-
<h4>BROUGHT TO YOU BY</h4>
107-
<p><a href="https://codelessly.com/" target="_blank">Codelessy</a> <a href="https://flutterbountyhunters.com" target="_blank">Flutter Bounty Hunters</a> <a href="https://superdeclarative.com" target="_blank">SuperDeclarative!</a></p>
108-
</footer>
10990

11091
<script>
11192
onload = (function() {
93+
// Select the first episode because Captivate automatically puts
94+
// that episode in the show player.
95+
document.querySelector('.episode-list-item').classList.add('selected');
96+
11297
// Find the date of the next show.
11398
var nextShowDate = new Date({{ calendar.next.timestamp }});
11499
@@ -127,7 +112,25 @@
127112
128113
// Set the next show date display string.
129114
var nextShowDateDisplay = document.getElementById("nextShowDate");
130-
nextShowDateDisplay.innerHTML = formattedDate + "<BR>" + formattedTime;
115+
nextShowDateDisplay.innerHTML = formattedDate + "" + formattedTime;
116+
});
117+
118+
// Attach click handlers to all episodes in the directory.
119+
const episodeListItems = document.querySelectorAll('.episode-list-item');
120+
episodeListItems.forEach(episode => {
121+
episode.addEventListener('click', () => {
122+
var episodeId = episode.getAttribute("episodeId");
123+
console.log("Loading episode: " + episodeId);
124+
125+
var episodePlayer = document.getElementById("playerContainer");
126+
episodePlayer.innerHTML = `<iframe style="width: 100%; height: 200px;" frameborder="no" scrolling="no" allow="clipboard-write" seamless src="https://player.captivate.fm/episode/${episodeId}"></iframe>`;
127+
128+
// Remove the "selected" class from all items
129+
episodeListItems.forEach(el => el.classList.remove('selected'));
130+
131+
// Add the "selected" class to the clicked item
132+
episode.classList.add('selected');
133+
});
131134
});
132135
</script>
133136
</body>

0 commit comments

Comments
 (0)