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