Skip to main content

flams_router_backend/
index_components.rs

1use 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
13//#[component]
14pub 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        //leptos::logging::log!("Here: main");
31        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}
46/*
47fn client<V: IntoView + Send>(f: impl Fn() -> V + Send + 'static) -> impl IntoView {
48    let sig = RwSignal::new(false);
49    #[cfg(feature = "hydrate")]
50    let _ = Effect::new(move || sig.set(true));
51    move || {
52        if sig.get() {
53            f().into_any()
54        } else {
55            ().into_any()
56        }
57    }
58}
59 */
60
61fn 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(&notes,|| "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        //quizzes,
274        //homeworks,
275        //instances,
276        ..
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(&notes,|| "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        /*{link_doc(&landing,|| view!(
343          <BodyText><b><span inner_html=title.to_string()/>{acronym.map(|s| format!(" ({s})"))}</b></BodyText>
344        ))}*/
345      </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}