Skip to content

Landing page design v2 #1

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jan 18, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/deploy.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ on:
jobs:
build:
env:
CAPTIVATE_USERNAME: ${{ secrets.CAPTIVATE_USERNAME }}
CAPTIVATE_TOKEN: ${{ secrets.CAPTIVATE_TOKEN }}
GOOGLE_CALENDAR_API_KEY: ${{ secrets.GOOGLE_CALENDAR_API_KEY }}
runs-on: ubuntu-latest
steps:
Expand Down
34 changes: 33 additions & 1 deletion bin/flutter_spaces.dart
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
import 'dart:io';

import 'package:flutter_spaces/flutter_spaces.dart';
import 'package:static_shock/static_shock.dart';

Future<void> main(List<String> arguments) async {
// Load Captivate credentials.
final podcastShowId = "3743ba71-859c-4164-99ff-999b525ccf48";
final (captivateUsername, captivateApiKey) = _loadCaptivateCredentials();

// Configure the static website generator.
final staticShock = StaticShock()
..pick(DirectoryPicker.parse("images"))
Expand All @@ -10,8 +16,34 @@ Future<void> main(List<String> arguments) async {
..plugin(const PrettyUrlsPlugin())
..plugin(const SassPlugin())
// Load data for the next scheduled Flutter space.
..loadData(const FlutterSpacesCalendarLoader());
..loadData(const FlutterSpacesCalendarLoader())
// Load all episodes.
..loadData(EpisodeLoader(
captivateUsername: captivateUsername,
captivateApiToken: captivateApiKey,
showId: podcastShowId,
));

// Generate the static website.
await staticShock.generateSite();
}

(String username, String apiToken) _loadCaptivateCredentials() {
// REQUIRED: Environment variable called "captivate_username"
final captivateUsername = Platform.environment["CAPTIVATE_USERNAME"] ?? Platform.environment["captivate_username"];

// REQUIRED: Environment variable called "captivate_token"
final captivateApiToken = Platform.environment["CAPTIVATE_TOKEN"] ?? Platform.environment["captivate_token"];

if (captivateUsername == null) {
print("Missing environment variable for Captivate username.");
exit(-1);
}

if (captivateApiToken == null) {
print("Missing environment variable for Captivate API token.");
exit(-1);
}

return (captivateUsername, captivateApiToken);
}
1 change: 1 addition & 0 deletions lib/flutter_spaces.dart
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export 'src/google_calendar.dart';
export 'src/podcast_episodes.dart';
67 changes: 67 additions & 0 deletions lib/src/podcast_episodes.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import 'package:captivate/captivate.dart';
import 'package:intl/intl.dart';
import 'package:static_shock/static_shock.dart';

class EpisodeLoader implements DataLoader {
EpisodeLoader({
required this.captivateUsername,
required this.captivateApiToken,
required this.showId,
});

final _client = CaptivateClient();
final String captivateUsername;
final String captivateApiToken;
final String showId;

@override
Future<Map<String, Object>> loadData(StaticShockPipelineContext context) async {
context.log.info("Loading podcast episodes...");

context.log.detail("Authenticating with Captivate...");
final userPayload = await _client.authenticate(
username: captivateUsername,
apiToken: captivateApiToken,
);
final authToken = userPayload!.user.token;
context.log.detail("Done authenticating with Captivate");

context.log.detail("Requesting all episodes from Captivate...");
final response = await _client.getEpisodes(authToken, showId);
if (response == null) {
context.log.warn("Failed to load episodes for show: $showId");
return {
"episodes": [],
};
}
context.log.detail("Loaded ${response.episodes.length} episodes.");

context.log.info("Done loading episodes from Captivate.");

return {
// Ordered from most recent to least recent.
"loadedEpisodes": [
for (final episode in response.episodes) //
{
"id": episode.id,
"title": episode.title,
"date": episode.formattedPublishedDate,
}
],
};
}
}

extension on Episode {
static final captivateDateFormat = DateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'");
static final presentationDateFormat = DateFormat("MMMM dd, yyyy");

String get formattedPublishedDate {
if (publishedDate == null) {
return "Now Available";
}

final dateTime = captivateDateFormat.parse(publishedDate!);
return presentationDateFormat.format(dateTime);
}
}
19 changes: 14 additions & 5 deletions pubspec.lock
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,15 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.1.2"
captivate:
dependency: "direct main"
description:
path: "."
ref: HEAD
resolved-ref: "5b4f2fcbff928758048837bf489307617d748e1d"
url: "https://github.com/Flutter-Bounty-Hunters/captivate"
source: git
version: "0.1.0"
charcode:
dependency: transitive
description:
Expand Down Expand Up @@ -279,7 +288,7 @@ packages:
source: hosted
version: "4.3.0"
intl:
dependency: transitive
dependency: "direct main"
description:
name: intl
sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf
Expand Down Expand Up @@ -346,10 +355,10 @@ packages:
dependency: transitive
description:
name: markdown
sha256: ef2a1298144e3f985cc736b22e0ccdaf188b5b3970648f2d9dc13efd1d9df051
sha256: "935e23e1ff3bc02d390bad4d4be001208ee92cc217cb5b5a6c19bc14aaa318c1"
url: "https://pub.dev"
source: hosted
version: "7.2.2"
version: "7.3.0"
mason_logger:
dependency: transitive
description:
Expand Down Expand Up @@ -514,10 +523,10 @@ packages:
dependency: transitive
description:
name: sass
sha256: c6abff6269edf6dc6767fde2ead064cbf939b8982656bd6a2097eda5d9c8f42b
sha256: "4577992b848d9bc5f4934dc30a73bb078d3f1f73c16d7b36bffdce220c1733c8"
url: "https://pub.dev"
source: hosted
version: "1.83.1"
version: "1.83.4"
shelf:
dependency: transitive
description:
Expand Down
6 changes: 6 additions & 0 deletions pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@ dependencies:

googleapis: ^13.2.0
googleapis_auth: ^1.6.0
intl: ^0.19.0

captivate:
git:
url: https://github.com/Flutter-Bounty-Hunters/captivate


dev_dependencies:
lints: ^2.0.0
Expand Down
Binary file added source/images/homepage/dash-astronaut.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
123 changes: 63 additions & 60 deletions source/index.jinja
Original file line number Diff line number Diff line change
Expand Up @@ -36,79 +36,64 @@

<body>
<main>
<header>
<div class="foreground">
<img src="images/homepage/logo_circle.png" class="logo">

<div class="title">WEEKLY FLUTTER SPACE</div>

<!-- This spacer has size zero unless screen is narrow -->
<!-- On narrow screen, this spacer takes up the same height as the phone image -->
<div class="spacer-for-narrow">&nbsp;</div>

<div class="upsell">Join us every week for &NewLine; any and all Flutter topics!</div>

<div class="hosts">
<p class="title">HOSTS</p>
<div class="host-container">
<div class="host matt">
<img src="/images/homepage/photo_matt.png">
<p>Matt Carroll</p>
</div>
<div class="host ray">
<img src="/images/homepage/photo_ray.png">
<p>Ray Li</p>
</div>
</div>
<div id="mainPane">
<div style="flex: 2;">&nbsp;</div>

<header>
<img id="mascot" src="/images/homepage/dash-astronaut.png">
<h1>Flutter Spaces</h1>
<div class="divider">&nbsp;</div>
<span class="subtitle">Weekly Flutter Conversations</span>
</header>

<div style="flex: 2;">&nbsp;</div>

<div id="episodePlayer">
<div id="playerContainer" style="margin: auto; max-width: 500px; height: 200px; overflow: hidden;">
<iframe style="width: 100%; height: 200px;" frameborder="no" scrolling="no" allow="clipboard-write" seamless src="https://player.captivate.fm/episode/{{ loadedEpisodes[0].id }}"></iframe>
</div>
</div>

<div class="background">
<div class="waves-and-phone">
<div class="sound-waves-left">&nbsp;</div>
<div class="phone">
<img src="/images/homepage/phone-space-preview.png">

<div class="next-show-date">
<p class="title">NEXT SPACE</p>
<!-- This display date is filled by JS -->
<p id="nextShowDate" class="date">&nbsp;</p>
</div>
<div style="flex: 2;">&nbsp;</div>

<div id="nextShowCalendar">
<div style="display: flex; flex-direction: row; align-items: flex-start;">
<div style="font-size: 30px; margin-right: 8px; margin-top:-4px;">📅</div>
<div style="display: flex; flex-direction: column;">
<h3>Next Space</h3>
<p id="nextShowDate">&nbsp;</p>
</div>
<div class="sound-waves-right">&nbsp;</div>
</div>
<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>
</div>
</header>

<div id="x-divider" style="display: flex;">
<hr class="divider" style="flex: 1; align-self: center;">
<img src="/images/homepage/x-logo.png" class="logo">
<hr class="divider" style="flex: 1; align-self: center;">
</div>
<div style="flex: 2;">&nbsp;</div>

<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>

<div id="post-timeline">
{% for episode in episodes %}

{% if episode.podcast_id %}
<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>
{% endif %}

<br><br><br>
{% endfor %}
<div id="attribution">
<p>Built with ❤️ by <a href="https://superdeclarative.com" target="_blank">Matt Carroll</a></p>
</div>
</div>

<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>
<div id="episodeDirectoryPane">
<div id="episodeDirectoryWindow">
{% for episode in loadedEpisodes %}
<div class="episode-list-item" episodeId="{{ episode.id }}">
<div class="content">
<h4>{{ episode.title }}</h4>
<p class="date">{{ episode.date }}</p>
</div>
</div>
{% endfor %}
</div>
</div>
</main>
<footer>
<hr class="divider">
<h4>BROUGHT TO YOU BY</h4>
<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>
</footer>

<script>
onload = (function() {
// Select the first episode because Captivate automatically puts
// that episode in the show player.
document.querySelector('.episode-list-item').classList.add('selected');

// Find the date of the next show.
var nextShowDate = new Date({{ calendar.next.timestamp }});

Expand All @@ -127,7 +112,25 @@

// Set the next show date display string.
var nextShowDateDisplay = document.getElementById("nextShowDate");
nextShowDateDisplay.innerHTML = formattedDate + "<BR>" + formattedTime;
nextShowDateDisplay.innerHTML = formattedDate + " • " + formattedTime;
});

// Attach click handlers to all episodes in the directory.
const episodeListItems = document.querySelectorAll('.episode-list-item');
episodeListItems.forEach(episode => {
episode.addEventListener('click', () => {
var episodeId = episode.getAttribute("episodeId");
console.log("Loading episode: " + episodeId);

var episodePlayer = document.getElementById("playerContainer");
episodePlayer.innerHTML = `<iframe style="width: 100%; height: 200px;" frameborder="no" scrolling="no" allow="clipboard-write" seamless src="https://player.captivate.fm/episode/${episodeId}"></iframe>`;

// Remove the "selected" class from all items
episodeListItems.forEach(el => el.classList.remove('selected'));

// Add the "selected" class to the clicked item
episode.classList.add('selected');
});
});
</script>
</body>
Expand Down
Loading