jnfilter @ 5217357b4635fad76ca35e655517387f61ccbb2a

feat: Add view to select series
  1diff --git a/main.go b/main.go
  2index 22d1489b26af1ccd0d907ddec6974bc518acc0aa..7a8d2a67604cdc163da9c1c6549593d046605745 100644
  3--- a/main.go
  4+++ b/main.go
  5@@ -2,10 +2,12 @@ package main
  6 
  7 import (
  8 	"context"
  9+	"embed"
 10 	"errors"
 11 	"flag"
 12 	"fmt"
 13 	"io"
 14+	"log/slog"
 15 	"net/http"
 16 	"os"
 17 	"regexp"
 18@@ -20,15 +22,17 @@ 	"github.com/prometheus/client_golang/prometheus/promhttp"
 19 )
 20 
 21 const (
 22-	FeedUrl = "https://api.jovemnerd.com.br/feed-nerdcast/"
 23+	feedUrl = "https://api.jovemnerd.com.br/feed-nerdcast/"
 24 )
 25 
 26 type (
 27-	ErrorRequestHandler func(w http.ResponseWriter, r *http.Request) error
 28+	errorRequestHandler func(w http.ResponseWriter, r *http.Request) error
 29 )
 30 
 31 var (
 32-	SerieRegex = regexp.MustCompile(`(?P<serie>.+) (?P<number>[0-9abc]+) \- (?P<title>.+)`)
 33+	//go:embed static/*
 34+	assets     embed.FS
 35+	serieRegex = regexp.MustCompile(`(?P<serie>.+) (?P<number>[0-9abc]+) \- (?P<title>.+)`)
 36 )
 37 
 38 var (
 39@@ -46,7 +50,6 @@ 		"catar":        "Vai te Catar [0-9]+",
 40 		"cloud":        "Nerd na Cloud [0-9]+",
 41 		"contar":       "Vou (T|t)e Contar [0-9]+",
 42 		"parceiro":     "Papo de Parceiro [0-9]+",
 43-		"cash":         "NerdCash [0-9]+",
 44 	}
 45 
 46 	feedRequest = promauto.NewHistogramVec(prometheus.HistogramOpts{
 47@@ -105,7 +108,7 @@ 		code := strconv.Itoa(c)
 48 		feedRequest.WithLabelValues(code).Observe(since)
 49 	}()
 50 
 51-	res, err := http.Get(FeedUrl)
 52+	res, err := http.Get(feedUrl)
 53 	if err != nil {
 54 		return nil, err
 55 	}
 56@@ -157,9 +160,10 @@
 57 	return doc.WriteToBytes()
 58 }
 59 
 60-func handleError(next ErrorRequestHandler) http.HandlerFunc {
 61+func handleError(next errorRequestHandler) http.HandlerFunc {
 62 	return func(w http.ResponseWriter, r *http.Request) {
 63 		if err := next(w, r); err != nil {
 64+			slog.ErrorContext(r.Context(), "Error", "error", err.Error())
 65 			w.WriteHeader(http.StatusInternalServerError)
 66 		}
 67 	}
 68@@ -217,6 +221,20 @@
 69 	return nil
 70 }
 71 
 72+func view(w http.ResponseWriter, r *http.Request) error {
 73+	data, err := assets.ReadFile("static/index.html")
 74+	if err != nil {
 75+		return err
 76+	}
 77+
 78+	_, err = w.Write(data)
 79+	if err != nil {
 80+		return err
 81+	}
 82+
 83+	return nil
 84+}
 85+
 86 func podcast(w http.ResponseWriter, r *http.Request) error {
 87 	xml, err := fetchXML(r.Context())
 88 	if err != nil {
 89@@ -253,7 +271,7 @@ 	unique := make(map[string]any)
 90 	els := doc.FindElements("//channel/item")
 91 	for _, e := range els {
 92 		txt := e.FindElement("title").Text()
 93-		res := SerieRegex.FindStringSubmatch(txt)
 94+		res := serieRegex.FindStringSubmatch(txt)
 95 		if len(res) > 1 {
 96 			unique[res[1]] = nil
 97 		}
 98@@ -284,6 +302,7 @@
 99 	mux := http.NewServeMux()
100 	mux.Handle("/metrics", promhttp.Handler())
101 	mux.HandleFunc("/titles", wrap(handleError(titles)))
102+	mux.HandleFunc("/view", wrap(handleError(view)))
103 	mux.HandleFunc("/", wrap(observe(handleError(podcast))))
104 
105 	server := http.Server{
106diff --git a/static/index.html b/static/index.html
107new file mode 100644
108index 0000000000000000000000000000000000000000..25341fe7617bd568b168e24d97365c8930cdaed8
109--- /dev/null
110+++ b/static/index.html
111@@ -0,0 +1,212 @@
112+<!DOCTYPE html>
113+<html>
114+    <head lang="pt">
115+        <meta charset="utf-8">
116+        <meta name="viewport" content="width=device-width, initial-scale=1">
117+        <link rel="shortcut icon" href="data:image/x-icon;," type="image/x-icon">
118+        <style type="text/css" media="screen">
119+/* Resettings some html properties */
120+html, body, div, h1, header,section{
121+	margin: 0;
122+	padding: 0;
123+	border: 0;
124+    font-family: monospace;
125+}
126+
127+h1 {
128+    font-size: 1.25rem;
129+    color: #fff;
130+    text-transform: uppercase;
131+}
132+
133+ul {
134+    margin: 0;
135+}
136+
137+body {
138+    font-family: sans-serif;
139+    background-color: #f4f4f4;
140+}
141+
142+header {
143+    display: flex;
144+    justify-content: space-between;
145+    margin: auto;
146+    margin-bottom: 1em;
147+    background: #0062cc;
148+    padding: .75em;
149+    max-width: 960px;
150+}
151+
152+nav {
153+    top: .75em;
154+    right: .75em;
155+}
156+
157+.warning {
158+  font-size: small;
159+  color: red;
160+  visibility: hidden;
161+}
162+
163+.btn {
164+  display: inline-block;
165+  padding: .1rem .75rem;
166+  background: #e9ecef;
167+  border: #343a40 1px solid;
168+  font-size: .9rem;
169+  font-weight: 400;
170+  line-height: 1.5;
171+  cursor: pointer;
172+  color: #000;
173+  border-radius: 0;
174+  text-decoration: none;
175+  transition: 0.5s all;
176+}
177+
178+.btn:hover {
179+    background-color: #fff;
180+}
181+
182+nav li {
183+    display: inline;
184+    margin: 0 0 0 0;
185+}
186+
187+.section {
188+    justify-content: left;
189+    margin-bottom: 1em;
190+    display: flex;
191+}
192+
193+.section input {
194+    max-width: 10em;
195+}
196+
197+.form-ctl {
198+    width: 1.3em;
199+    height: 1.3em;
200+    background-color: white;
201+    border-radius: 0;
202+    vertical-align: middle;
203+    border: 1px solid #ddd;
204+    appearance: none;
205+    -webkit-appearance: none;
206+    outline: none;
207+}
208+
209+.form-ctl:checked {
210+    background-color: gray;
211+}
212+
213+a {
214+  display: flex;
215+  justify-content: center;
216+  align-content: center;
217+  flex-direction: column;
218+  padding-left: 5px;
219+}
220+
221+main {
222+    width: 40%;
223+    margin: 0 auto;
224+}
225+
226+#feedUrl {
227+    text-overflow: ellipsis;
228+    max-width: 100%;
229+    white-space: normal;
230+    word-break: break-all;
231+}
232+
233+@media (width <= 600px) {
234+    main {
235+        width: 90%;
236+    }
237+}
238+
239+        </style>
240+    </head>
241+    <body>
242+        <header>
243+            <h1>Filtro para o Nerdcast</h1>
244+            <nav>
245+                <ul>
246+                    <li>
247+                        <a class="btn" href="https://git.gabrielgio.me/jnfilter/">Código fonte ➤</a>
248+                    </li>
249+                </ul>
250+            </nav>
251+        </header>
252+        <main>
253+            <div class="section">
254+                Selecione os quadros:
255+            </div>
256+            <div class="section">
257+                <input class="form-ctl" type="checkbox" onchange="updateList(this)" id="nerdcast" /><a>NerdCast</a>
258+            </div>
259+            <div class="section">
260+                <input class="form-ctl" type="checkbox" onchange="updateList(this)" id="empreendedor" /><a>Empreendedor</a>
261+            </div>
262+            <div class="section">
263+                <input class="form-ctl" type="checkbox" onchange="updateList(this)" id="mamicas" /><a>Canecas de Mamicas</a>
264+            </div>
265+            <div class="section">
266+                <input class="form-ctl" type="checkbox" onchange="updateList(this)" id="english" /><a>Speak English</a>
267+            </div>
268+            <div class="section">
269+                <input class="form-ctl" type="checkbox" onchange="updateList(this)" id="nerdcash" /><a>NerdCash</a>
270+            </div>
271+            <div class="section">
272+                <input class="form-ctl" type="checkbox" onchange="updateList(this)" id="bunker" /><a>Lá do Bunker</a>
273+            </div>
274+            <div class="section">
275+                <input class="form-ctl" type="checkbox" onchange="updateList(this)" id="tech" /><a>NerdTech</a>
276+            </div>
277+            <div class="section">
278+                <input class="form-ctl" type="checkbox" onchange="updateList(this)" id="genera" /><a>Generacast</a>
279+            </div>
280+            <div class="section">
281+                <input class="form-ctl" type="checkbox" onchange="updateList(this)" id="rpg" /><a>NerdCast RPG</a>
282+            </div>
283+            <div class="section">
284+                <input class="form-ctl" type="checkbox" onchange="updateList(this)" id="catar" /><a>Vai te Catar</a>
285+            </div>
286+            <div class="section">
287+                <input class="form-ctl" type="checkbox" onchange="updateList(this)" id="cloud" /><a>Nerd na Cloud</a>
288+            </div>
289+            <div class="section">
290+                <input class="form-ctl" type="checkbox" onchange="updateList(this)" id="contar" /><a>Vou te Contar</a>
291+            </div>
292+            <div class="section">
293+                <input class="form-ctl" type="checkbox" onchange="updateList(this)" id="parceiro" /><a>Papo de Parceiro</a>
294+            </div>
295+            <div class="section">
296+                <a id="feedUrl" href="https://jnfilter.gabrielgio.me?q=mamicas" type="textbox">https://jnfilter.gabrielgio.me</a>
297+            </div>
298+        </main>
299+        <script>
300+var feedUrl = document.getElementById("feedUrl")
301+var fields = new Set()
302+
303+function updateFeedUrl() {
304+    if (fields.size == 0) {
305+        url = "https://jnfilter.gabrielgio.me"
306+    } else {
307+        url = "https://jnfilter.gabrielgio.me?q="+[...fields].join(',')
308+    }
309+    feedUrl.textContent = url
310+    feedUrl.href = url
311+}
312+
313+function updateList(elem) {
314+    if (elem.checked){
315+        fields.add(elem.id)
316+    } else {
317+        fields.delete(elem.id)
318+    }
319+    updateFeedUrl()
320+}
321+        </script>
322+    </body>
323+</html>