flams_router_backend/
index_components.rs1use flams_backend_types::archive_json::{ArchiveIndex, Institution};
2use flams_router_base::maybe_lazy;
3use flams_web_utils::{client_only, components::wait_and_then_fn};
4use ftml_component_utils::{
5 Block, Caption, Footer, Header, HeaderLeft, HeaderRight, Scrollbar, Text,
6};
7use ftml_dom::utils::css::inject_css;
8use ftml_uris::DocumentUri;
9use leptos::prelude::*;
10
11maybe_lazy!(Index = index());
12
13pub fn index() -> AnyView {
15 wait_and_then_fn(super::server_fns::index, |(is, idxs)| {
16 let mut libraries = Vec::new();
17 let mut books = Vec::new();
18 let mut papers = Vec::new();
19 let mut courses = Vec::new();
20 let mut self_studies = Vec::new();
21 for e in idxs {
22 match e {
23 e @ ArchiveIndex::Library { .. } => libraries.push(e),
24 e @ ArchiveIndex::Book { .. } => books.push(e),
25 e @ ArchiveIndex::Paper { .. } => papers.push(e),
26 e @ ArchiveIndex::Course { .. } => courses.push(e),
27 e @ ArchiveIndex::SelfStudy { .. } => self_studies.push(e),
28 }
29 }
30 let r = view! {
32 {do_books(books)}
33 {do_papers(papers)}
34 {do_self_studies(self_studies)}
35 {do_courses(courses,is)}
36 {do_libraries(libraries)}
37 }
38 .into_any();
39 inject_css(
40 "flams-index-card",
41 ".flams-index-card{max-width:400px;margin:10px;}",
42 );
43 r
44 })
45}
46fn wrap_list(ttl: &'static str, i: impl FnOnce() -> AnyView) -> AnyView {
62 use ftml_component_utils::Divider;
63 view! {
64 <h2 style="color:var(--colorBrandForeground1)">{ttl}</h2>
65 <div style="display:flex;flex-flow:wrap;">
66 {i()}
67 </div>
68 <Divider/>
69 }
70 .into_any()
71}
72
73fn link_doc<T: FnOnce() -> AnyView>(uri: &DocumentUri, i: T) -> AnyView {
74 view! {
75 <a target="_blank" href=format!("/?uri={}",urlencoding::encode(&uri.to_string())) style="color:var(--colorBrandForeground1)">
76 {i()}
77 </a>
78 }.into_any()
79}
80
81fn do_img(url: String) -> AnyView {
82 view!(<div style="width:100%"><div style="width:min-content;margin:auto;">
83 <img src=url style="max-width:350px;max-height:150px;"/>
84 </div></div>)
85 .into_any()
86}
87
88fn do_teaser(txt: String) -> AnyView {
89 use flams_web_utils::components::ClientOnly;
90 view!(<div style="margin:5px;"><Scrollbar style="max-height: 100px;"><Text>
91 <ClientOnly><span inner_html=txt style="font-size:smaller;"/></ClientOnly>
92 </Text></Scrollbar></div>)
93 .into_any()
94}
95
96fn do_books(books: Vec<ArchiveIndex>) -> AnyView {
97 if books.is_empty() {
98 return ().into_any();
99 }
100 client_only!({
101 wrap_list("Books", || {
102 books
103 .clone()
104 .into_iter()
105 .map(book)
106 .collect_view()
107 .into_any()
108 })
109 })
110 .into_any()
111}
112
113fn book(book: ArchiveIndex) -> AnyView {
114 let ArchiveIndex::Book {
115 title,
116 authors,
117 file,
118 teaser,
119 thumbnail,
120 } = book
121 else {
122 unreachable!()
123 };
124 view! {<Block class="flams-index-card">
125 <Header slot>
126 {link_doc(&file,|| view!(<Text bold=true><span inner_html=title.to_string()/></Text>).into_any())}
127 </Header>
128 <HeaderLeft slot><Caption>
129 {if authors.is_empty() {None} else {Some(IntoIterator::into_iter(authors).map(|a| view!{{a.to_string()}<br/>}).collect_view())}}
130 </Caption>
131 </HeaderLeft>
132 <div style="margin: 0 -12px;">
133 {thumbnail.map(|t| do_img(t.to_string()))}
134 {teaser.map(|t| do_teaser(t.to_string()))}
135 </div>
136 </Block>}.into_any()
137}
138
139fn do_papers(papers: Vec<ArchiveIndex>) -> AnyView {
140 if papers.is_empty() {
141 return ().into_any();
142 }
143 client_only!({
144 wrap_list("Papers", || {
145 papers
146 .clone()
147 .into_iter()
148 .map(paper)
149 .collect_view()
150 .into_any()
151 })
152 })
153 .into_any()
154}
155
156fn paper(paper: ArchiveIndex) -> AnyView {
157 let ArchiveIndex::Paper {
158 title,
159 authors,
160 file,
161 teaser,
162 thumbnail,
163 venue,
164 venue_url,
165 } = paper
166 else {
167 unreachable!()
168 };
169 view! {<Block class="flams-index-card">
170 <Header slot>
171 {link_doc(&file,|| view!(<Text bold=true><span inner_html=title.to_string()/></Text>).into_any())}
172 </Header>
173 <HeaderLeft slot><Caption>
174 {if authors.is_empty() {None} else {Some(IntoIterator::into_iter(authors).map(|a| view!{{a.to_string()}<br/>}).collect_view())}}
175 </Caption>
176 </HeaderLeft>
177 <HeaderRight slot>
178 {venue.map(|v| venue_url.map_or_else(|| leptos::either::Either::Right(view!(<b>{v.to_string()}</b>)),
179 |url| {
180 leptos::either::Either::Left(view!(
181 <a target="_blank" href=url.to_string() style="color:var(--colorBrandForeground1)">
182 <b>{v.to_string()}</b>
183 </a>
184 ))
185 }
186 ))}
187 </HeaderRight>
188 <div style="margin: 0 -12px;">
189 {thumbnail.map(|t| do_img(t.to_string()))}
190 {teaser.map(|t| do_teaser(t.to_string()))}
191 </div>
192 </Block>}.into_any()
193}
194
195fn do_self_studies(sss: Vec<ArchiveIndex>) -> AnyView {
196 if sss.is_empty() {
197 return ().into_any();
198 }
199 client_only!({
200 wrap_list("Self-Study Courses", || {
201 sss.clone()
202 .into_iter()
203 .map(self_study)
204 .collect_view()
205 .into_any()
206 })
207 })
208 .into_any()
209}
210
211fn self_study(ss: ArchiveIndex) -> AnyView {
212 let ArchiveIndex::SelfStudy {
213 title,
214 landing,
215 acronym,
216 notes,
217 slides,
218 thumbnail,
219 teaser,
220 ..
221 } = ss
222 else {
223 unreachable!()
224 };
225 view! {<Block class="flams-index-card">
226 <Header slot>
227 {link_doc(&landing,|| view!(
228 <Text bold=true><span inner_html=title.to_string()/>{acronym.map(|s| format!(" ({s})"))}</Text>
229 ).into_any())}
230 </Header>
231 <div style="margin: 0 -12px;">
232 {thumbnail.map(|t| do_img(t.to_string()))}
233 {teaser.map(|t| do_teaser(t.to_string()))}
234 </div>
235 <div style="margin-top:auto;"/>
236 <Footer slot>
237 <Caption>
238 {link_doc(¬es,|| "Notes".into_any())}
239 {slides.map(|s| view!(", "{link_doc(&s,|| "Slides".into_any())}))}
240 </Caption>
241 </Footer>
242 </Block>}.into_any()
243}
244
245fn do_courses(courses: Vec<ArchiveIndex>, insts: Vec<Institution>) -> AnyView {
246 if courses.is_empty() {
247 return ().into_any();
248 }
249 client_only!({
250 wrap_list("Courses", || {
251 courses
252 .clone()
253 .into_iter()
254 .map(|c| course(c, &insts))
255 .collect_view()
256 .into_any()
257 })
258 })
259 .into_any()
260}
261
262fn course(course: ArchiveIndex, insts: &[Institution]) -> AnyView {
263 let ArchiveIndex::Course {
264 title,
265 landing,
266 acronym,
267 authors: instructors,
268 institution,
269 notes,
270 slides,
271 thumbnail,
272 teaser,
273 ..
277 } = course
278 else {
279 unreachable!()
280 };
281 let inst = institution
282 .and_then(|inst| insts.iter().find(|i| i.acronym() == &*inst))
283 .cloned();
284 view! {<Block class="flams-index-card">
285 <Header slot>
286 {link_doc(&landing,|| view!(
287 <Text bold=true><span inner_html=title.to_string()/>{acronym.map(|s| format!(" ({s})"))}</Text>
288 ).into_any())}
289 </Header>
290 <HeaderLeft slot><Caption>
291 {if instructors.is_empty() {None} else {Some(IntoIterator::into_iter(instructors).map(|a| view!{{a.to_string()}<br/>}).collect_view())}}
292 </Caption>
293 </HeaderLeft>
294 <HeaderRight slot>{
295 {inst.map(|inst| view!(
296 <img style="max-width:50px;max-height:30px;" src=inst.logo().to_string() title=inst.title().to_string()/>
297 ))}
298 }</HeaderRight>
299 <div style="margin: 0 -12px;">
300 {thumbnail.map(|t| do_img(t.to_string()))}
301 {teaser.map(|t| do_teaser(t.to_string()))}
302 </div>
303 <div style="margin-top:auto;"/>
304 <Footer slot>
305 <Caption>
306 {link_doc(¬es,|| "Notes".into_any())}
307 {slides.map(|s| view!(", "{link_doc(&s,|| "Slides".into_any())}))}
308 </Caption>
309 </Footer>
310 </Block>}.into_any()
311}
312
313fn do_libraries(libs: Vec<ArchiveIndex>) -> AnyView {
314 if libs.is_empty() {
315 return ().into_any();
316 }
317 client_only!({
318 wrap_list("Libraries", || {
319 libs.clone()
320 .into_iter()
321 .map(library)
322 .collect_view()
323 .into_any()
324 })
325 })
326 .into_any()
327}
328
329fn library(lib: ArchiveIndex) -> AnyView {
330 let ArchiveIndex::Library {
331 archive,
332 title,
333 teaser,
334 thumbnail,
335 } = lib
336 else {
337 unreachable!()
338 };
339 view! {<Block class="flams-index-card">
340 <Header slot>
341 <Text bold=true><span inner_html=title.to_string()/></Text>
342 </Header>
346 <HeaderLeft slot><Caption>
347 {archive.to_string()}
348 </Caption></HeaderLeft>
349 <div style="margin: 0 -12px;">
350 {thumbnail.map(|t| do_img(t.to_string()))}
351 {teaser.map(|t| do_teaser(t.to_string()))}
352 </div>
353 <div style="margin-top:auto;"/>
354 </Block>}
355 .into_any()
356}