1use leptos::prelude::*;
6
7#[derive(Clone, Debug, Default, PartialEq)]
9pub struct ArticleData {
10 pub content: String,
12
13 pub title: String,
15
16 pub custom_css: Vec<String>,
18
19 pub custom_js: Vec<String>,
21}
22
23#[component]
27pub fn Article(
28 data: Signal<ArticleData>,
30) -> impl IntoView {
31 view! {
32 <article class="typstify-article">
33 <For
35 each=move || data.get().custom_css.clone()
36 key=|css| css.clone()
37 children=move |css| {
38 view! { <link rel="stylesheet" href=css /> }
39 }
40 />
41
42 <header class="typstify-article-header">
44 <h1 class="typstify-article-title">{move || data.get().title.clone()}</h1>
45 </header>
46
47 <div class="typstify-article-content" inner_html=move || data.get().content.clone()></div>
49
50 <For
52 each=move || data.get().custom_js.clone()
53 key=|js| js.clone()
54 children=move |js| {
55 view! { <script src=js defer=true></script> }
56 }
57 />
58
59 </article>
60 }
61}
62
63#[component]
65pub fn ArticleMeta(
66 #[prop(optional)]
68 date: Option<String>,
69 #[prop(optional)]
71 reading_time: Option<u32>,
72 #[prop(default = vec![])]
74 tags: Vec<String>,
75) -> impl IntoView {
76 let has_date = date.is_some();
77 let date_value = date.clone();
78 let has_reading_time = reading_time.is_some();
79 let reading_time_value = reading_time.unwrap_or(0);
80 let has_tags = !tags.is_empty();
81 let tags_list = StoredValue::new(tags);
82
83 view! {
84 <div class="typstify-article-meta">
85 <Show when=move || has_date>
86 <time class="typstify-article-date">{date_value.clone()}</time>
87 </Show>
88
89 <Show when=move || has_reading_time>
90 <span class="typstify-article-reading-time">{reading_time_value} " min read"</span>
91 </Show>
92
93 <Show when=move || has_tags>
94 <div class="typstify-article-tags">
95 <For
96 each=move || tags_list.get_value()
97 key=|tag| tag.clone()
98 children=move |tag| {
99 let href = format!("/tags/{}", tag.to_lowercase());
100 view! {
101 <a href=href class="typstify-tag">
102 {tag}
103 </a>
104 }
105 }
106 />
107
108 </div>
109 </Show>
110 </div>
111 }
112}
113
114#[component]
116pub fn Prose(
117 children: Children,
119) -> impl IntoView {
120 view! { <div class="typstify-prose">{children()}</div> }
121}
122
123#[cfg(test)]
124mod tests {
125 use super::*;
126
127 #[test]
128 fn test_article_data_default() {
129 let data = ArticleData::default();
130 assert!(data.content.is_empty());
131 assert!(data.title.is_empty());
132 assert!(data.custom_css.is_empty());
133 assert!(data.custom_js.is_empty());
134 }
135
136 #[test]
137 fn test_article_data_creation() {
138 let data = ArticleData {
139 content: "<p>Hello</p>".to_string(),
140 title: "Test Article".to_string(),
141 custom_css: vec!["style.css".to_string()],
142 custom_js: vec!["script.js".to_string()],
143 };
144
145 assert_eq!(data.title, "Test Article");
146 assert_eq!(data.custom_css.len(), 1);
147 assert_eq!(data.custom_js.len(), 1);
148 }
149}