1import gleam/int
2import gleam/option.{type Option}
3import lustre/attribute as attr
4import lustre/element
5import lustre/element/html
6import tom
7import website/style
8
9pub type Post(a) {
10 Post(
11 id: String,
12 title: String,
13 author: String,
14 date_posted: tom.Date,
15 summary: String,
16 content: element.Element(a),
17 )
18}
19
20pub type Link {
21 Link(title: String, url: String, img: String)
22}
23
24pub type Project(a) {
25 Project(
26 id: String,
27 title: String,
28 author: String,
29 img: String,
30 links: List(Link),
31 archived: Bool,
32 summary: String,
33 content: element.Element(a),
34 )
35}
36
37pub type Anime {
38 Anime(title: String, link: Option(String), thoughts: Option(String))
39}
40
41pub fn wrapper(
42 page: String,
43 elements: List(element.Element(a)),
44) -> element.Element(a) {
45 html.html([attr.attribute("lang", "en"), style.background_color()], [
46 html.head([], [
47 html.meta([attr.attribute("charset", "UTF-8")]),
48 html.title([], page <> " | Naomi Roberts"),
49 html.link([attr.rel("preconnect"), attr.href("https://fonts.bunny.net")]),
50 html.link([
51 attr.rel("stylesheet"),
52 attr.href("https://fonts.bunny.net/css?family=almarai:400"),
53 ]),
54 ]),
55 style.body_query(),
56 html.body([attr.class("body-margin"), style.body_styles()], [
57 navbar(),
58 html.div([attr.style("margin", "1em")], elements),
59 ]),
60 html.footer([style.footer()], [
61 html.hr([attr.style("color", "lightgrey")]),
62 html.p([], [
63 html.text("The source code for this website is available "),
64 link([], "here", "https://codeberg.org/naomi/website", True),
65 html.text(" or "),
66 link([], "here", "https://tangled.sh/@lesbian.skin/website", True),
67 html.text(" under the "),
68 link(
69 [],
70 "BSD 3 with Attribution",
71 "https://spdx.org/licenses/BSD-3-Clause-Attribution",
72 True,
73 ),
74 html.text(" license."),
75 ]),
76 html.p([], [
77 html.text("If you wish to get in contact, please send an email to "),
78 link([], "mia@naomieow.xyz", "mailto:mia@naomieow.xyz", True),
79 html.text(", or contact me on "),
80 link([], "Discord", "https://chat.lesbian.skin", True),
81 html.text("."),
82 ]),
83 ]),
84 ])
85}
86
87pub fn navbar() -> element.Element(a) {
88 html.div([style.navbar()], [
89 navitem(name: "home", href: "/", external: False),
90 navitem(name: "projects", href: "/projects", external: False),
91 navitem(name: "posts", href: "/posts", external: False),
92 navitem(
93 name: "anime",
94 href: "https://anilist.co/user/naomieow/",
95 external: True,
96 ),
97 html.div([style.navbar_right()], [icon_link(Bluesky), icon_link(Tangled)]),
98 ])
99}
100
101fn navitem(
102 name text: String,
103 href href: String,
104 external external: Bool,
105) -> element.Element(a) {
106 link([style.navitem()], text:, href:, external:)
107}
108
109pub type Icon {
110 Bluesky
111 Tangled
112}
113
114pub fn icon_link(icon icon: Icon) -> element.Element(a) {
115 let #(href, img) = case icon {
116 Bluesky -> #("https://bsky.app/profile/lesbian.skin", "/assets/bsky.svg")
117 Tangled -> #("https://tangled.sh/@lesbian.skin", "/assets/git.svg")
118 }
119
120 html.a([attr.href(href), style.link_icon()], [
121 html.img([attr.src(img), style.icon_size()]),
122 ])
123}
124
125pub fn link(
126 attributes attributes: List(attr.Attribute(a)),
127 text text: String,
128 href href: String,
129 external external: Bool,
130) -> element.Element(a) {
131 let attrs = [attr.href(href), style.link(), ..attributes]
132 html.a(
133 case external {
134 False -> attrs
135 True -> [
136 attr.target("_blank"),
137 attr.rel("noopener noreferrer"),
138 attr.style("display", "inline-block"),
139 ..attrs
140 ]
141 },
142 [
143 element.text(text),
144 // Seems to cause some DOM issues?
145 // in some places it appears `<a href>text<p style>↗</p></a>
146 // but in other places it's `<p><a href>text<a></p><p style><a href>↗</a></p><p><p/>`
147 // case external {
148 // True ->
149 // html.p(
150 // [
151 // attr.styles([
152 // #("display", "inline-block"),
153 // #("font-size", "0.75em"),
154 // #("margin", "0"),
155 // #("vertical-align", "top"),
156 // ]),
157 // ],
158 // [element.text("↗")],
159 // )
160 // False -> element.none()
161 // },
162 ],
163 )
164}
165
166pub fn raw_link(
167 attributes attributes: List(attr.Attribute(a)),
168 content content: List(element.Element(a)),
169 href href: String,
170 external external: Bool,
171) -> element.Element(a) {
172 let attrs = [attr.href(href), style.link(), ..attributes]
173 html.a(
174 case external {
175 False -> attrs
176 True -> [
177 attr.target("_blank"),
178 attr.rel("noopener noreferrer"),
179 attr.style("display", "inline-block"),
180 ..attrs
181 ]
182 },
183 content,
184 )
185}
186
187pub fn warning(title: String, contents: String) -> element.Element(a) {
188 html.div([], [
189 html.h3([], [element.text(title)]),
190 html.p([], [element.text(contents)]),
191 ])
192}
193
194pub fn date_to_string(date: tom.Date) -> String {
195 date.day |> int.to_string()
196 <> "/"
197 <> date.month |> int.to_string()
198 <> "/"
199 <> date.year |> int.to_string()
200}