flams_router_backend/
index_components.rs

1use 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        //leptos::logging::log!("Here: main");
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        .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(&notes,|| "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        //quizzes,
272        //homeworks,
273        //instances,
274        ..
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(&notes,|| "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        /*{link_doc(&landing,|| view!(
344          <Body1><b><span inner_html=title.to_string()/>{acronym.map(|s| format!(" ({s})"))}</b></Body1>
345        ))}*/
346      </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}