Skip to content

Commit c6178d8

Browse files
committed
feat: settings page
1 parent bbb45e7 commit c6178d8

File tree

13 files changed

+399
-43
lines changed

13 files changed

+399
-43
lines changed
Lines changed: 1 addition & 0 deletions
Loading
Lines changed: 1 addition & 0 deletions
Loading

crates/rostra-web-ui/assets/style.css

Lines changed: 144 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -412,17 +412,39 @@ img.u-userImage[loading="lazy"][src][width][height] {
412412
}
413413

414414

415-
.o-navBar__list {
415+
.o-topNav {
416416
display: flex;
417-
gap: 4pt;
417+
flex-wrap: wrap;
418+
gap: 0.5rem 1rem;
418419
}
419420

420-
.o-navBar__item {
421-
display: block;
421+
.o-topNav__item {
422+
display: flex;
423+
align-items: center;
424+
gap: 0.25rem;
422425
}
423426

424-
.o-navBar__header {
425-
font-weight: bold;
427+
.o-topNav__icon {
428+
display: inline-block;
429+
width: 1rem;
430+
height: 1rem;
431+
filter: invert(var(--invert));
432+
}
433+
434+
.o-topNav__icon.-back {
435+
background: url('/assets/icons/arrow-left.svg') center/contain no-repeat;
436+
}
437+
438+
.o-topNav__icon.-home {
439+
background: url('/assets/icons/house.svg') center/contain no-repeat;
440+
}
441+
442+
.o-topNav__icon.-support {
443+
background: url('/assets/icons/comment.svg') center/contain no-repeat;
444+
}
445+
446+
.o-topNav__icon.-settings {
447+
background: url('/assets/icons/gear.svg') center/contain no-repeat;
426448
}
427449

428450
.o-mainBar {
@@ -740,6 +762,11 @@ emoji-picker {
740762
.m-profileSummary__bio {
741763
flex-basis: 100%;
742764
width: 100%;
765+
}
766+
767+
.m-profileSummary__bioEdit {
768+
flex-basis: 100%;
769+
width: 100%;
743770
height: 10rem;
744771
border: 1px solid var(--color-button-border);
745772
border-radius: var(--border-radius-std);
@@ -1538,4 +1565,114 @@ input:checked+.slider:before {
15381565

15391566
.o-notification:hover {
15401567
opacity: 0.9;
1541-
}
1568+
}
1569+
1570+
/* Settings Page */
1571+
.o-settingsNav {
1572+
display: flex;
1573+
flex-direction: column;
1574+
gap: 0.25rem;
1575+
margin-top: 1rem;
1576+
padding: 0.5rem;
1577+
background: var(--color-timeline-bg);
1578+
border: 1px solid var(--color-timeline-item-border);
1579+
border-radius: var(--border-radius-std);
1580+
min-width: 200px;
1581+
}
1582+
1583+
.o-settingsNav__item {
1584+
display: block;
1585+
padding: 0.5rem 0.75rem;
1586+
border-radius: var(--border-radius-std);
1587+
text-decoration: none;
1588+
color: var(--color-text-default);
1589+
}
1590+
1591+
.o-settingsNav__item:hover {
1592+
background: var(--color-button-bg-hover);
1593+
}
1594+
1595+
.o-settingsNav__item.-active {
1596+
background: var(--color-button-bg);
1597+
font-weight: bold;
1598+
}
1599+
1600+
.o-settingsContent {
1601+
padding: 1rem;
1602+
background: var(--color-timeline-bg);
1603+
border: 1px solid var(--color-timeline-item-border);
1604+
border-radius: var(--border-radius-std);
1605+
}
1606+
1607+
.o-settingsContent__header {
1608+
margin: 0 0 1rem 0;
1609+
font-size: 1.5rem;
1610+
}
1611+
1612+
.o-settingsContent__section {
1613+
margin-bottom: 1.5rem;
1614+
}
1615+
1616+
.o-settingsContent__section:last-child {
1617+
margin-bottom: 0;
1618+
}
1619+
1620+
.o-settingsContent__sectionHeader {
1621+
margin: 0 0 0.75rem 0;
1622+
font-size: 1.1rem;
1623+
color: var(--color-text-muted);
1624+
}
1625+
1626+
.o-settingsContent__empty {
1627+
color: var(--color-text-muted);
1628+
font-style: italic;
1629+
}
1630+
1631+
.m-followeeList {
1632+
display: flex;
1633+
flex-direction: column;
1634+
gap: 0.5rem;
1635+
}
1636+
1637+
.m-followeeList__item {
1638+
display: flex;
1639+
align-items: center;
1640+
gap: 0.75rem;
1641+
padding: 0.5rem;
1642+
background: var(--color-post-bg);
1643+
border: 1px solid var(--color-button-border);
1644+
border-radius: var(--border-radius-std);
1645+
}
1646+
1647+
.m-followeeList__avatar {
1648+
flex-shrink: 0;
1649+
border-radius: var(--border-radius-std);
1650+
}
1651+
1652+
.m-followeeList__name {
1653+
flex: 1;
1654+
min-width: 0;
1655+
overflow: hidden;
1656+
text-overflow: ellipsis;
1657+
white-space: nowrap;
1658+
text-decoration: none;
1659+
color: var(--color-text-default);
1660+
}
1661+
1662+
.m-followeeList__name:hover {
1663+
text-decoration: underline;
1664+
}
1665+
1666+
.m-followeeList__actions {
1667+
flex-shrink: 0;
1668+
margin-left: 0.5rem;
1669+
}
1670+
1671+
.m-followeeList__unfollowButton {
1672+
font-size: 0.875rem;
1673+
padding: 0.25rem 0.5rem;
1674+
}
1675+
1676+
.m-followeeList__unfollowButtonIcon {
1677+
background: url('/assets/icons/xmark.svg') center/contain no-repeat;
1678+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,36 @@ impl UiState {
126126
}
127127
})
128128
}
129+
130+
/// Renders a standard two-column page layout with navbar and main content
131+
pub fn render_page_layout(&self, navbar: Markup, main_content: Markup) -> Markup {
132+
html! {
133+
(navbar)
134+
main ."o-mainBar" {
135+
(main_content)
136+
}
137+
}
138+
}
139+
140+
/// Renders the top navigation bar with Home, Support, and Settings links
141+
pub fn render_top_nav(&self) -> Markup {
142+
html! {
143+
div ."o-topNav" {
144+
a ."o-topNav__item" href="/ui" {
145+
span ."o-topNav__icon -home" {}
146+
"Home"
147+
}
148+
a ."o-topNav__item" href="https://github.com/dpc/rostra/discussions" {
149+
span ."o-topNav__icon -support" {}
150+
"Support"
151+
}
152+
a ."o-topNav__item" href="/ui/settings" {
153+
span ."o-topNav__icon -settings" {}
154+
"Settings"
155+
}
156+
}
157+
}
158+
}
129159
}
130160

131161
/// A static footer.

crates/rostra-web-ui/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
mod error;
2-
mod fragment;
2+
mod layout;
33
pub mod html_utils;
44
mod routes;
55
// TODO: move to own crate

crates/rostra-web-ui/src/routes.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ mod post;
88
mod profile;
99
mod profile_self;
1010
mod search;
11+
mod settings;
1112
mod timeline;
1213
mod unlock;
1314

@@ -195,6 +196,12 @@ pub fn route_handler(state: SharedState) -> Router<Arc<UiState>> {
195196
get(profile_self::get_self_account_edit).post(profile_self::post_self_account_edit),
196197
)
197198
.route("/ui/search/profiles", get(search::search_profiles))
199+
.route("/ui/settings", get(settings::get_settings))
200+
.route(
201+
"/ui/settings/followers",
202+
get(settings::get_settings_followers),
203+
)
204+
.route("/ui/settings/unfollow", post(settings::post_unfollow))
198205
// .route("/a/", put(account_new))
199206
// .route("/t/", put(token_new))
200207
// .route("/m/", put(metric_new).get(metric_find))

crates/rostra-web-ui/src/routes/profile.rs

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -190,12 +190,7 @@ impl UiState {
190190
) -> RequestResult<Markup> {
191191
Ok(html! {
192192
nav ."o-navBar" {
193-
div ."o-navBar__list" {
194-
span ."o-navBar__header" { "Rostra:" }
195-
a ."o-navBar__item" href="https://github.com/dpc/rostra/discussions" { "Support" }
196-
a ."o-navBar__item" href="https://github.com/dpc/rostra/wiki" { "Wiki" }
197-
a ."o-navBar__item" href="https://github.com/dpc/rostra" { "Github" }
198-
}
193+
(self.render_top_nav())
199194

200195
div ."o-navBar__userAccount" {
201196
(self.render_profile_summary(profile_id, session, session.ro_mode()).await?)

crates/rostra-web-ui/src/routes/profile_self.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -214,7 +214,7 @@ impl UiState {
214214
}
215215
}
216216

217-
textarea."m-profileSummary__bio"
217+
textarea."m-profileSummary__bioEdit"
218218
placeholder="Bio..."
219219
rows="8"
220220
dir="auto"

crates/rostra-web-ui/src/routes/profile_self/extractor.rs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,8 @@ where
2626
return Err((StatusCode::BAD_REQUEST, "Missing content type"));
2727
};
2828

29-
// Check if content type starts with "multipart/form-data" (it may include boundary parameter)
29+
// Check if content type starts with "multipart/form-data" (it may include
30+
// boundary parameter)
3031
if !content_type
3132
.to_str()
3233
.map(|s| s.starts_with("multipart/form-data"))
@@ -90,7 +91,10 @@ where
9091
};
9192

9293
if parts.avatar.replace((mime, v.to_vec())).is_some() {
93-
return Err((StatusCode::BAD_REQUEST, "Failed to parse multipart field"));
94+
return Err((
95+
StatusCode::BAD_REQUEST,
96+
"Failed to parse multipart field",
97+
));
9498
}
9599
}
96100
}

crates/rostra-web-ui/src/routes/search.rs

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
1+
use axum::Json;
12
use axum::extract::{Query, State};
23
use axum::response::IntoResponse;
3-
use axum::Json;
44
use rostra_core::id::RostraId;
55
use serde::{Deserialize, Serialize};
66

77
use super::unlock::session::UserSession;
8-
use crate::error::RequestResult;
98
use crate::SharedState;
9+
use crate::error::RequestResult;
1010

1111
#[derive(Deserialize)]
1212
pub struct SearchQuery {
@@ -33,11 +33,7 @@ pub async fn search_profiles(
3333
let (direct, extended) = db.get_followees_extended(session.id()).await;
3434

3535
// Collect all IDs to search (direct followees + extended)
36-
let all_ids: Vec<RostraId> = direct
37-
.keys()
38-
.copied()
39-
.chain(extended.into_iter())
40-
.collect();
36+
let all_ids: Vec<RostraId> = direct.keys().copied().chain(extended.into_iter()).collect();
4137

4238
// Filter by display name prefix
4339
let mut results = Vec::new();

0 commit comments

Comments
 (0)