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