flams_router_backend/
index_components.rs

1use 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(&notes,|| "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        //quizzes,
231        //homeworks,
232        //instances,
233        ..
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(&notes,|| "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        /*{link_doc(&landing,|| view!(
294          <Body1><b><span inner_html=title.to_string()/>{acronym.map(|s| format!(" ({s})"))}</b></Body1>
295        ))}*/
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    </Card>}
303}