51658595f61f648600967cb1f2ae40c083e9e5d8

Author
TheEdgeOfRage <git@theedgeofrage.com>
Committer
TheEdgeOfRage <git@theedgeofrage.com>
Date

Message

Add navbar

Diff

This diff is truncated to protect this page.

  1diff --git a/assets/ytrssil.png b/assets/ytrssil.png
  2new file mode 100644
  3index 0000000000000000000000000000000000000000..335bf88afd70537e1c5d29ce1fe58f0022fde862
  4Binary files /dev/null and b/assets/ytrssil.png differ
  5diff --git a/httpserver/ytrssil/channels.go b/httpserver/ytrssil/channels.go
  6index f4101d5bac44ec10f73b198fee1ed75a784210f7..9fedac0051644660d8b51f4388eee4c9034c9ebd 100644
  7--- a/httpserver/ytrssil/channels.go
  8+++ b/httpserver/ytrssil/channels.go
  9@@ -8,18 +8,10 @@ import (
 10 
 11 	"github.com/TheEdgeOfRage/ytrssil-api/db"
 12 	"github.com/TheEdgeOfRage/ytrssil-api/feedparser"
 13-	"github.com/TheEdgeOfRage/ytrssil-api/models"
 14 )
 15 
 16 func (srv *server) SubscribeToChannelJSON(c *gin.Context) {
 17-	var channel models.Channel
 18-	err := c.ShouldBindUri(&channel)
 19-	if err != nil {
 20-		c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": err.Error()})
 21-		return
 22-	}
 23-
 24-	err = srv.handler.SubscribeToChannel(c.Request.Context(), channel.ID)
 25+	err := srv.handler.SubscribeToChannel(c.Request.Context(), c.Param("channel_id"))
 26 	if err != nil {
 27 		if errors.Is(err, db.ErrAlreadySubscribed) {
 28 			c.AbortWithStatusJSON(http.StatusConflict, gin.H{"error": err.Error()})
 29@@ -38,14 +30,7 @@ func (srv *server) SubscribeToChannelJSON(c *gin.Context) {
 30 }
 31 
 32 func (srv *server) UnsubscribeFromChannelJSON(c *gin.Context) {
 33-	var channel models.Channel
 34-	err := c.ShouldBindUri(&channel)
 35-	if err != nil {
 36-		c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": err.Error()})
 37-		return
 38-	}
 39-
 40-	err = srv.handler.UnsubscribeFromChannel(c.Request.Context(), channel.ID)
 41+	err := srv.handler.UnsubscribeFromChannel(c.Request.Context(), c.Param("channel_id"))
 42 	if err != nil {
 43 		if errors.Is(err, db.ErrChannelNotFound) {
 44 			c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"error": err.Error()})
 45diff --git a/httpserver/ytrssil/channels_pages.go b/httpserver/ytrssil/channels_pages.go
 46new file mode 100644
 47index 0000000000000000000000000000000000000000..846e9f44918619ec648ad05275fc1eb4b0fcf69f
 48--- /dev/null
 49+++ b/httpserver/ytrssil/channels_pages.go
 50@@ -0,0 +1,30 @@
 51+package ytrssil
 52+
 53+import (
 54+	"errors"
 55+	"net/http"
 56+
 57+	"github.com/gin-gonic/gin"
 58+
 59+	"github.com/TheEdgeOfRage/ytrssil-api/db"
 60+	"github.com/TheEdgeOfRage/ytrssil-api/feedparser"
 61+)
 62+
 63+func (srv *server) SubscribeToChannelPage(c *gin.Context) {
 64+	err := srv.handler.SubscribeToChannel(c.Request.Context(), c.PostForm("channel_id"))
 65+	if err != nil {
 66+		if errors.Is(err, db.ErrAlreadySubscribed) {
 67+			c.String(http.StatusConflict, err.Error())
 68+			return
 69+		}
 70+		if errors.Is(err, feedparser.ErrInvalidChannelID) {
 71+			c.Data(http.StatusNotFound, "text/html", []byte(err.Error()))
 72+			return
 73+		}
 74+
 75+		c.String(http.StatusInternalServerError, err.Error())
 76+		return
 77+	}
 78+
 79+	c.String(http.StatusOK, "")
 80+}
 81diff --git a/httpserver/ytrssil/server.go b/httpserver/ytrssil/server.go
 82index 37afb434831bf74872ada122d5082d2a24bbf8f9..741f40512373002009a10d95d9978420c8f34587 100644
 83--- a/httpserver/ytrssil/server.go
 84+++ b/httpserver/ytrssil/server.go
 85@@ -65,6 +65,7 @@ func SetupGinRouter(
 86 	}
 87 	engine.GET("/healthz", srv.Healthz)
 88 	engine.POST("/fetch", srv.FetchVideosJSON)
 89+	engine.Static("/assets", "./assets")
 90 
 91 	engine.GET("/auth", srv.AuthPage)
 92 	engine.POST("/auth", srv.HandleAuth)
 93@@ -73,6 +74,7 @@ func SetupGinRouter(
 94 	pages.Use(auth.PageAuthMiddleware(cfg.AuthToken))
 95 	{
 96 		pages.GET("/", srv.NewVideosPage)
 97+		pages.POST("/subscribe", srv.SubscribeToChannelPage)
 98 		pages.PATCH("/videos/:video_id/watch", srv.MarkVideoAsWatchedPage)
 99 		pages.PATCH("/videos/:video_id/progress", srv.SetVideoProgressPage)
100 	}
101diff --git a/models/channel.go b/models/channel.go
102index 095ae8b725b11213ef5af87d10496633c3c70245..f5e91df34c2ce4e04366733a7c223cab3076606c 100644
103--- a/models/channel.go
104+++ b/models/channel.go
105@@ -2,7 +2,7 @@ package models
106 
107 type Channel struct {
108 	// YouTube ID of the channel
109-	ID string `json:"channel_id" uri:"channel_id" binding:"required"`
110+	ID string `json:"channel_id"`
111 	// Name of the channel
112 	Name string `json:"name"`
113 }
114diff --git a/pages/main.templ b/pages/main.templ
115index b23de3f4cf10d30565c6ccbd75c5b229255d63f3..26e6416b6f737ade10b8fe2fc9d353057c2ae58b 100644
116--- a/pages/main.templ
117+++ b/pages/main.templ
118@@ -15,9 +15,20 @@ templ BaseLayout(title string) {
119 				.pillarbox::before,
120 				.pillarbox::after { content: ''; position: absolute; top: 0; bottom: 0%; width: 36%; background: black; z-index: 1; }
121 			</style>
122+			<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/js/bootstrap.bundle.min.js" integrity="sha384-FKyoEForCGlyvwx9Hj09JcYn3nv7wiPVlz7YYwJrWVcXK/BmnVDxM+D2scQbITxI" crossorigin="anonymous"></script>
123 			<script src="https://cdn.jsdelivr.net/npm/htmx.org@2.0.8/dist/htmx.min.js"></script>
124+			<script>
125+				document.addEventListener('htmx:responseError', event => {
126+					const field = event.detail.elt.querySelector(`[name="channel_id"]`);
127+					field.setCustomValidity(event.detail.xhr.responseText);
128+					field.onchange = () => field.setCustomValidity('');
129+					field.reportValidity();
130+				});
131+			</script>
132 		</head>
133 		<body>
134+			@subscriptionModal()
135+			@navbar()
136 			<div class="container-fluid">
137 				{ children... }
138 			</div>
139diff --git a/pages/main_templ.go b/pages/main_templ.go
140index 280534d91bbb5bdd2f9aee6dd0b0f6f0bec857ba..08d84d4529f145f2f4fffa86d39bfd8c95e19509 100644
141--- a/pages/main_templ.go
142+++ b/pages/main_templ.go
143@@ -42,7 +42,19 @@ func BaseLayout(title string) templ.Component {
144 		if templ_7745c5c3_Err != nil {
145 			return templ_7745c5c3_Err
146 		}
147diff --git a/pages/navbar.templ b/pages/navbar.templ
148new file mode 100644
149index 0000000000000000000000000000000000000000..ea51178e20a4b15cc803d155e0081efbe1f25e56
150--- /dev/null
151+++ b/pages/navbar.templ
152@@ -0,0 +1,35 @@
153+package pages
154+
155+templ navbar() {
156+	<nav class="navbar navbar-expand-lg bg-body-tertiary">
157+		<div class="container-fluid">
158+			<a class="navbar-brand" href="/">
159+				<img src="/assets/ytrssil.png" alt="ytrssil" width="32" height="32"/>
160+			</a>
161+			<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent">
162+				<span class="navbar-toggler-icon"></span>
163+			</button>
164+			<div class="collapse navbar-collapse" id="navbarSupportedContent">
165+				<ul class="navbar-nav me-auto mb-2 mb-lg-0">
166+					<li class="nav-item me-md-2 mt-2 mt-md-0">
167+						<button type="button" data-bs-toggle="modal" data-bs-target="#subscription-modal" class="btn btn-success">
168+							Subscribe <i class="bi bi-bookmark-plus"></i>
169+						</button>
170+					</li>
171+					<li class="nav-item me-md-2 mt-2 mt-md-0">
172+						<button
173+							type="button"
174+							class="btn btn-primary"
175+							hx-post="/fetch"
176+							hx-swap="none"
177+						>Fetch <i class="bi bi-cloud-download"></i></button>
178+					</li>
179+				</ul>
180+				<form class="d-flex" role="search">
181+					<input class="form-control me-2" type="search" placeholder="Search"/>
182+					<button class="btn btn-outline-success" type="submit">Search</button>
183+				</form>
184+			</div>
185+		</div>
186+	</nav>
187+}
188diff --git a/pages/navbar_templ.go b/pages/navbar_templ.go
189new file mode 100644
190index 0000000000000000000000000000000000000000..f5a6e31c1b686999c0cf5b577f01df4984222530
191--- /dev/null
192+++ b/pages/navbar_templ.go
193@@ -0,0 +1,40 @@
194+// Code generated by templ - DO NOT EDIT.
195+
196+// templ: version: v0.3.960
197+package pages
198+
199+//lint:file-ignore SA4006 This context is only used if a nested component is present.
200+
201+import "github.com/a-h/templ"
202+import templruntime "github.com/a-h/templ/runtime"
203+
204+func navbar() templ.Component {
205+	return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
206+		templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
207+		if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
208+			return templ_7745c5c3_CtxErr
209+		}
210+		templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
211+		if !templ_7745c5c3_IsBuffer {
212+			defer func() {
213+				templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
214+				if templ_7745c5c3_Err == nil {
215+					templ_7745c5c3_Err = templ_7745c5c3_BufErr
216+				}
217+			}()
218+		}
219+		ctx = templ.InitializeContext(ctx)
220+		templ_7745c5c3_Var1 := templ.GetChildren(ctx)
221+		if templ_7745c5c3_Var1 == nil {
222+			templ_7745c5c3_Var1 = templ.NopComponent
223+		}
224+		ctx = templ.ClearChildren(ctx)
225diff --git a/pages/new_videos.templ b/pages/new_videos.templ
226index 1947ba9c165a9e77ffe3e749269687a518852ab6..2986d63f05193530a896d2d51a2fb2dda000f3a1 100644
227--- a/pages/new_videos.templ
228+++ b/pages/new_videos.templ
229@@ -32,6 +32,15 @@ templ thumbnail(video models.Video) {
230 				alt={ video.Title }
231 				style="width: 100%; aspect-ratio: 16/9; object-fit: contain;"
232 			/>
233+			<svg
234+				xmlns="http://www.w3.org/2000/svg"
235+				height="24"
236+				viewBox="0 0 24 24"
237+				width="24"
238+				focusable="false"
239+				style="pointer-events: none; width: 2rem; height: 2rem; z-index: 2; position: absolute; bottom: 0.5rem; right: 0.5rem;"
240+				fill="white"
241+			><path d="m13.974 2.052-8 4.7a4 4 0 00.385 7.097l.942.423-1.327.78a4 4 0 004.052 6.897l8-4.7a4.001 4.001 0 00-.384-7.096L16.7 9.73l1.326-.78a4 4 0 10-4.052-6.897ZM10 15V9l5 3-5 3Z"></path></svg>
242 		} else {
243 			<img
244 				src={ fmt.Sprintf("https://img.youtube.com/vi/%s/0.jpg", video.ID) }
245@@ -48,24 +57,7 @@ templ videoCard(video models.Video) {
246 	<div class="card">
247 		@thumbnail(video)
248 		<div class="card-body">
249-			if video.IsShort {
250-				<p class="card-title text-truncate mb-0">
251-					<svg
252-						xmlns="http://www.w3.org/2000/svg"
253-						height="24"
254-						viewBox="0 0 24 24"
255-						width="24"
256-						focusable="false"
257-						aria-hidden="true"
258-						style="pointer-events: none; display: inherit; width: 2rem; height: 2rem;"
259-						class="float-end"
260-						fill="white"
261-					><path d="m13.974 2.052-8 4.7a4 4 0 00.385 7.097l.942.423-1.327.78a4 4 0 004.052 6.897l8-4.7a4.001 4.001 0 00-.384-7.096L16.7 9.73l1.326-.78a4 4 0 10-4.052-6.897ZM10 15V9l5 3-5 3Z"></path></svg>
262-					{ video.Title }
263-				</p>
264-			} else {
265-				<p class="card-title text-truncate">{ video.Title }</p>
266-			}
267+			<p class="card-title text-truncate mb-0">{ video.Title }</p>
268 			<p>
269 				<a
270 					target="blank"
271diff --git a/pages/new_videos_templ.go b/pages/new_videos_templ.go
272index baf4eabd53deacdbbf77f7e239e74d1a1c8579f7..b7847bb26332426799c1cc044035cfad4a90b401 100644
273--- a/pages/new_videos_templ.go
274+++ b/pages/new_videos_templ.go
275@@ -164,7 +164,7 @@ func thumbnail(video models.Video) templ.Component {
276 			if templ_7745c5c3_Err != nil {
277 				return templ_7745c5c3_Err
278 			}
279-			templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "\" style=\"width: 100%; aspect-ratio: 16/9; object-fit: contain;\">")
280diff --git a/pages/subscription.templ b/pages/subscription.templ
281new file mode 100644
282index 0000000000000000000000000000000000000000..22cff9553caf6e46e025b72f65810ecde3a606de
283--- /dev/null
284+++ b/pages/subscription.templ
285@@ -0,0 +1,27 @@
286+package pages
287+
288+templ subscriptionModal() {
289+	<div class="modal fade" id="subscription-modal" tabindex="-1">
290+		<div class="modal-dialog">
291+			<div class="modal-content">
292+				<div class="modal-header">
293+					<h1 class="modal-title fs-5">Subscribe to channel</h1>
294+					<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
295+				</div>
296+				<div class="modal-body">
297+					<form
298+						hx-post="/subscribe"
299+						hx-target="#subscription-error"
300+						hx-swap="innerHTML"
301+						hx-on::after-request="if(event.detail.successful) bootstrap.Modal.getInstance(document.getElementById('subscription-modal')).hide()"
302+						class="d-flex"
303+					>
304+						<input type="text" name="channel_id" placeholder="Channel username or ID" class="form-control"/>
305+						<button type="submit" class="btn btn-primary ms-3"><i class="bi bi-plus"></i></button>
306+					</form>
307+					<div id="subscription-error" class="text-danger mt-2 ms-1"></div>
308+				</div>
309+			</div>
310+		</div>
311+	</div>
312+}
313diff --git a/pages/subscription_templ.go b/pages/subscription_templ.go
314new file mode 100644
315index 0000000000000000000000000000000000000000..8c805fd26fe4809a0d5ae5fddcc0951face4f8f7
316--- /dev/null
317+++ b/pages/subscription_templ.go
318@@ -0,0 +1,40 @@
319+// Code generated by templ - DO NOT EDIT.
320+
321+// templ: version: v0.3.960
322+package pages
323+
324+//lint:file-ignore SA4006 This context is only used if a nested component is present.
325+
326+import "github.com/a-h/templ"
327+import templruntime "github.com/a-h/templ/runtime"
328+
329+func subscriptionModal() templ.Component {
330+	return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
331+		templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
332+		if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
333+			return templ_7745c5c3_CtxErr
334+		}
335+		templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
336+		if !templ_7745c5c3_IsBuffer {
337+			defer func() {
338+				templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
339+				if templ_7745c5c3_Err == nil {
340+					templ_7745c5c3_Err = templ_7745c5c3_BufErr
341+				}
342+			}()
343+		}
344+		ctx = templ.InitializeContext(ctx)
345+		templ_7745c5c3_Var1 := templ.GetChildren(ctx)
346+		if templ_7745c5c3_Var1 == nil {
347+			templ_7745c5c3_Var1 = templ.NopComponent
348+		}
349+		ctx = templ.ClearChildren(ctx)