~ololduck/Frogue
feat(ui): add landing page for anonymous visitors
Browse the tree at this commit
5 files changed, +138, −20
TODO.md+3−3
@@ -31,9 +31,9 @@ if a user directly logs in with something like `ssh username@SSH_SERVER_ADDRESS`
### `/` (index)
−- [ ] have a landing page that explains what this Frogue instance is about.
− - [ ] Have a link to login
− - [ ] If registrations are open, have link to register
+- [x] have a landing page that explains what this Frogue instance is about.
+ - [x] Have a link to login
+ - [x] If registrations are open, have link to register
- [ ] if we are logged in, show the user's Feed? like what a logged in Mastodon/snac/others user would see, but with ForgeFed additions (external activity on the user's repo/tickets/patches)?
- [x] show AP feed
- [ ] show instance/forgefed feed
frogue/src/views/index.rs+81−10
@@ -4,30 +4,86 @@ use tracing::instrument;
use super::Page;
use super::components::Pagination;
use crate::activitypub::feed::FeedEntry;
+use crate::registration::RegistrationMode;
pub const FEED_PAGE_SIZE: usize = 20;
+/// The landing page for anonymous visitors: explains what Frogue is, shows
+/// instance statistics, and indicates whether registration is open.
+pub(super) struct Landing {
+ pub user_count: u64,
+ pub repository_count: u64,
+ pub registration_mode: RegistrationMode,
+}
+
+impl Render for Landing {
+ fn render(&self) -> Markup {
+ html! {
+ section class="landing-intro" {
+ h2 { "What is Frogue?" }
+ p {
+ "Frogue is a " em { "software forge" } ". It's a place to host and collaborate on software projects,"
+ "track issues, and review patch proposals. It speaks " a href="https://activitypub.rocks/" { "ActivityPub" }
+ " and " a href="https://forgefed.org/" { "ForgeFed" } ", so it can federate with other forges and ActivityPub clients."
+ }
+ }
+
+ section class="landing-stats" {
+ h2 { "This instance" }
+ ul {
+ li { strong { (self.user_count) } " users" }
+ li { strong { (self.repository_count) } " public repositories" }
+ }
+ }
+
+ section class="landing-registration" {
+ h2 { "Join" }
+ @match self.registration_mode {
+ RegistrationMode::Open => {
+ p {
+ "Registrations are open. "
+ a href="/register" { "Create an account" }
+ " or "
+ a href="/login" { "log in" }
+ "."
+ }
+ }
+ RegistrationMode::InviteOnly => {
+ p {
+ "Registrations are invite-only. Ask an existing user for an invite link, "
+ "then "
+ a href="/register" { "register" }
+ "."
+ }
+ }
+ }
+ }
+ }
+ }
+}
+
/// The body of the index page: when a user is logged in and federation is
−/// enabled, show their home feed. Otherwise show an empty page.
+/// enabled, show their home feed. Otherwise show the landing page.
pub(super) struct Index {
pub entries: Vec<FeedEntry>,
pub public_url: String,
pub viewer_username: Option<String>,
pub page: u64,
+ pub landing: Landing,
}
impl Render for Index {
fn render(&self) -> Markup {
− if self.entries.is_empty() {
− // Empty feed - show a message or empty page
− if self.viewer_username.is_some() {
− html! {
− p { "Your feed is empty. Follow some remote users or post a note!" }
− }
− } else {
− html! {}
+ if self.viewer_username.is_none() {
+ // Anonymous visitor: show the landing page
+ self.landing.render()
+ } else if self.entries.is_empty() {
+ // Empty feed - show a message
+ html! {
+ p { "Your feed is empty. Follow some remote users or post a note!" }
}
} else {
+ // Logged in with feed entries
html! {
div class="feed" {
@for entry in &self.entries {
@@ -114,16 +170,26 @@ fn render_feed_entry(
/// The index page, as seen by `viewer` - nobody, when it is `None`.
/// When logged in and federation is enabled, shows the home feed.
+/// When not logged in, shows the landing page with instance stats.
#[instrument(skip(entries))]
pub fn index(
viewer: Option<&str>,
entries: Vec<FeedEntry>,
public_url: &str,
page: u64,
+ user_count: u64,
+ repository_count: u64,
+ registration_mode: RegistrationMode,
) -> HttpResponse {
let viewer_username = viewer.map(str::to_string);
+ let title = if viewer.is_some() {
+ "Home - Frogue".to_string()
+ } else {
+ "Frogue".to_string()
+ };
+
Page {
− title: "Home - Frogue".to_string(),
+ title,
viewer: viewer_username.clone(),
extra_head: None,
body: Index {
@@ -131,6 +197,11 @@ pub fn index(
public_url: public_url.to_string(),
viewer_username,
page,
+ landing: Landing {
+ user_count,
+ repository_count,
+ registration_mode,
+ },
},
}
.into_response()
frogue/src/views/mod.rs+1−1
@@ -68,7 +68,7 @@ impl<B: Render> Page<B> {
}
main { (self.body) }
footer {
− p { "Powered by " a href="#no-public-url-yet"{em{"Frogue"}} " version " code{(env!("CARGO_PKG_VERSION"))}}
+ p { "Powered by " a href=(env!("CARGO_PKG_REPOSITORY")) {em{"Frogue"}} " version " code{(env!("CARGO_PKG_VERSION"))}}
}
}
}
frogue/src/webui/mod.rs+51−4
@@ -1,7 +1,7 @@
use std::sync::Arc;
use actix_web::{HttpResponse, get, web};
−use sea_orm::{ColumnTrait, EntityTrait, QueryFilter};
+use sea_orm::{ColumnTrait, EntityTrait, PaginatorTrait, QueryFilter};
use tokio::sync::RwLock;
use crate::AppState;
@@ -225,9 +225,38 @@ pub async fn index(
// Parse page number, default to 0, invalid -> 0
let page = query.page.unwrap_or(0);
+ // Get registration mode (always available for landing page)
+ let registration_mode = crate::registration::registration_mode();
+
// Only show feed if logged in and federation is enabled
if viewer_name.is_none() || !crate::activitypub::enabled(&state) {
− return views::index(viewer_name, vec![], &public_url, page);
+ // For anonymous visitors, query instance stats
+ let (user_count, repository_count) = if viewer_name.is_none() {
+ let user_count = Users::find()
+ .count(&db)
+ .await
+ .unwrap_or(0);
+ let repository_count = Repositories::find()
+ .filter(crate::entities::repositories::Column::Visibility.eq(
+ crate::entities::repositories::Visibility::Public,
+ ))
+ .count(&db)
+ .await
+ .unwrap_or(0);
+ (user_count, repository_count)
+ } else {
+ (0, 0)
+ };
+
+ return views::index(
+ viewer_name,
+ vec![],
+ &public_url,
+ page,
+ user_count,
+ repository_count,
+ registration_mode,
+ );
}
// Get the user ID from the viewer
@@ -238,7 +267,17 @@ pub async fn index(
.await
{
Ok(Some(u)) => u,
− _ => return views::index(viewer_name, vec![], &public_url, page),
+ _ => {
+ return views::index(
+ viewer_name,
+ vec![],
+ &public_url,
+ page,
+ 0,
+ 0,
+ registration_mode,
+ );
+ }
};
// Fetch the feed
@@ -258,5 +297,13 @@ pub async fn index(
}
};
− views::index(viewer_name, entries, &public_url, page)
+ views::index(
+ viewer_name,
+ entries,
+ &public_url,
+ page,
+ 0,
+ 0,
+ registration_mode,
+ )
}
frogue/tests/webui.rs+2−2
@@ -48,14 +48,14 @@ async fn index_renders_the_home_page() -> miette::Result<()> {
response.body
);
assert!(
− response.body.contains("<title>Home - Frogue</title>"),
+ response.body.contains("<title>Frogue</title>"),
"the index should carry its title, got: {}",
response.body
);
assert!(
response
.body
− .contains("<h1><a href=\"/\">🐸 Frogue</a></h1>"),
+ .contains("<h1><a href=\"/\">🐸 Frogue<span style=\"color: #ccc\">(Fed)</span></a></h1>"),
"the index should carry the site header, got: {}",
response.body
);