From f88c25f1175acc492794f0c0210939212afb9a14 Mon Sep 17 00:00:00 2001 From: florpan Date: Thu, 11 Sep 2025 01:26:06 +0200 Subject: [PATCH] serier och taggar --- Frontend/src/App.tsx | 5 ++ Frontend/src/components/SeriesList.tsx | 89 +++++++++++++++++++++ Frontend/src/services/bibblanservice.ts | 11 +++ Frontend/src/types/types.ts | 17 +++- Server/Business/Services/DatabaseService.cs | 54 +++++++++++++ Server/Controllers/BibblanController.cs | 23 ++++++ Server/Program.cs | 12 +++ Server/ViewModels/SeriesVm.cs | 32 ++++++++ 8 files changed, 242 insertions(+), 1 deletion(-) create mode 100644 Frontend/src/components/SeriesList.tsx create mode 100644 Server/ViewModels/SeriesVm.cs diff --git a/Frontend/src/App.tsx b/Frontend/src/App.tsx index e165f62..79462ef 100644 --- a/Frontend/src/App.tsx +++ b/Frontend/src/App.tsx @@ -3,6 +3,7 @@ import type { Component } from "solid-js"; import Home from "./components/Home"; import BookList from "./components/BookList"; import AuthorList from "./components/AuthorList"; +import SeriesList from "./components/SeriesList"; const App: Component = () => { return ( @@ -35,6 +36,9 @@ const App: Component = () => { Authors + + Series + @@ -46,6 +50,7 @@ const App: Component = () => { + diff --git a/Frontend/src/components/SeriesList.tsx b/Frontend/src/components/SeriesList.tsx new file mode 100644 index 0000000..3f29ecb --- /dev/null +++ b/Frontend/src/components/SeriesList.tsx @@ -0,0 +1,89 @@ +import { createSignal, onMount, For, type Component } from "solid-js"; +import AuthorCard from "./AuthorCard"; +import BibblanService from "../services/bibblanservice"; +import { series, listBook } from "../types/types"; + +const SeriesList: Component = () => { + const [series, setSeries] = createSignal([]); + const [query, setQuery] = createSignal(""); + + const update = (query: string) => { + setQuery(query); + BibblanService.getSeries(query).then(setSeries); + }; + + onMount(() => { + update(query()); + }); + + const distinctAuthors = ( + books: listBook[] + ): { id: number; name: string }[] => { + return [ + ...new Set( + books.map((b) => JSON.stringify({ id: b.authorId, name: b.authorName })) + ), + ].map((x) => JSON.parse(x)) as { id: number; name: string }[]; + }; + + const minDate = (books: listBook[]): string => { + if (books.length === 0) return ""; + const darr = books.map((b) => new Date(b.pubDate)); + const d = darr.reduce((min, b) => (b < min ? b : min), darr[0]); + return d?.toISOString().substr(0, 10) ?? ""; + }; + + const maxDate = (books: listBook[]): string => { + if (books.length === 0) return ""; + const darr = books.map((b) => new Date(b.pubDate)); + const d = darr.reduce((max, b) => (b > max ? b : max), darr[0]); + return d?.toISOString().substr(0, 10) ?? ""; + }; + + return ( +
+

Series!

+ update(e.currentTarget.value)} + /> + + + + + + + + + + + + + {(item) => ( + + + + + + + + )} + + +
IDNameAuthorBook CountPublished
{item.id}{item.name} + + {(author) => ( + {author.name} + )} + + {item.books.length} + {minDate(item.books) ?? "N/A"} -{" "} + {maxDate(item.books) ?? "N/A"} +
+
+ ); +}; + +export default SeriesList; diff --git a/Frontend/src/services/bibblanservice.ts b/Frontend/src/services/bibblanservice.ts index 1e41986..2e7eca3 100644 --- a/Frontend/src/services/bibblanservice.ts +++ b/Frontend/src/services/bibblanservice.ts @@ -25,6 +25,17 @@ const BibblanService = { const response = await fetch(url); return response.json(); }, + + getSeries: async ( + query: string | undefined = undefined + ): Promise => { + let url = "/api/bibblan/series"; + if (query != undefined && query.length > 0) { + url += `?query=${encodeURIComponent(query)}`; + } + const response = await fetch(url); + return response.json(); + }, }; export default BibblanService; diff --git a/Frontend/src/types/types.ts b/Frontend/src/types/types.ts index a6bf830..a7bf30f 100644 --- a/Frontend/src/types/types.ts +++ b/Frontend/src/types/types.ts @@ -18,4 +18,19 @@ interface author { bookCount: number; } -export type { book, author }; +interface listBook { + id: number; + title: string; + authorId: number; + authorName: string; + pubDate: Date; + seriesIndex: number; +} + +interface series { + id: number; + name: string; + books: Array; +} + +export type { book, author, series, listBook }; diff --git a/Server/Business/Services/DatabaseService.cs b/Server/Business/Services/DatabaseService.cs index 998be35..58b981c 100644 --- a/Server/Business/Services/DatabaseService.cs +++ b/Server/Business/Services/DatabaseService.cs @@ -51,5 +51,59 @@ namespace Bibblan.Business.Services return conn.Query(query).Take(count).ToList(); } + + internal List GetSeries(int count, BookFilter? filter) + { + var query = @"select s.id as seriesid, s.name as seriesname, b.*, a.* + --b.id as bookid, b.title, b.pubdate, b.series_index, + --a.id as authorid, a.name as authorname + from series s + inner join books_series_link bsl on s.id = bsl.series + inner join books b on bsl.book = b.id + inner join books_authors_link bal on b.id = bal.book + inner join authors a on bal.author = a.id"; + if (!String.IsNullOrWhiteSpace(filter?.Query)) + { + filter.Query = filter.Query?.ToLowerInvariant().Trim() ?? ""; + query += $" where lower(s.name) like '%{filter.Query}%'"; + } + var conn = new NpgsqlConnection(settings.BibblanConnection); + var lookup = new Dictionary(); + conn.Query(query, (s,b,a) => + { + SeriesVm svm; + if (!lookup.TryGetValue(s.Id, out svm)) + { + lookup.Add(s.Id, svm = s); + } + svm.Books.Add(new ListBook + { + AuthorId = a.Id, + AuthorName = a.Name, + Id = b.Id, + PubDate = b.Pubdate, + SeriesIndex = b.SeriesIndex, + Title = b.Title + + }); + return svm; + }, splitOn: "id").Take(count).ToList(); + return lookup.Values.AsList(); + } + + internal List GetTags(BookFilter? filter) + { + var query = @"select t.id, t.name, count(btl.*) as bookcount from tags t + inner join books_tags_link btl on t.id = btl.tag + group by t.id, t.name + order by bookcount desc"; + if (!String.IsNullOrWhiteSpace(filter?.Query)) + { + filter.Query = filter.Query.ToLowerInvariant(); + query += $" having lower(name) like '%{filter.Query}%'"; + } + var conn = new NpgsqlConnection(settings.BibblanConnection); + return conn.Query(query).ToList(); + } } } diff --git a/Server/Controllers/BibblanController.cs b/Server/Controllers/BibblanController.cs index 0bef8eb..694836d 100644 --- a/Server/Controllers/BibblanController.cs +++ b/Server/Controllers/BibblanController.cs @@ -66,5 +66,28 @@ namespace Bibblan.Controllers } + [HttpGet("series")] + public IActionResult GetSeries(string query = null) + { + BookFilter filter = query != null ? new BookFilter + { + Query = query + } : null; + + var series = _db.GetSeries(100, filter).ToList(); + return Ok(series); + } + + [HttpGet("tags")] + public IActionResult GetTags(string query = null) + { + BookFilter filter = query != null ? new BookFilter + { + Query = query + } : null; + + var tags = _db.GetTags(filter).ToList(); + return Ok(tags); + } } } diff --git a/Server/Program.cs b/Server/Program.cs index 1497e76..b5db4ad 100644 --- a/Server/Program.cs +++ b/Server/Program.cs @@ -1,7 +1,10 @@ using Bibblan.Business.Services; using Bibblan.Models; +using Bibblan.ViewModels; +using Dapper; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Options; +using System.ComponentModel.DataAnnotations.Schema; var builder = WebApplication.CreateBuilder(args); builder.Configuration.AddJsonFile($"appsettings.user.json", true, true); @@ -28,6 +31,15 @@ builder.Services.AddDbContext(options => options.UseNpgs var app = builder.Build(); Dapper.DefaultTypeMap.MatchNamesWithUnderscores = true; +Dapper.SqlMapper.SetTypeMap( + typeof(SeriesVm), + new CustomPropertyTypeMap( + typeof(SeriesVm), + (type, columnName) => + type.GetProperties().FirstOrDefault(prop => + prop.GetCustomAttributes(false) + .OfType() + .Any(attr => attr.Name == columnName)))); app.UseDefaultFiles(); app.MapStaticAssets(); diff --git a/Server/ViewModels/SeriesVm.cs b/Server/ViewModels/SeriesVm.cs new file mode 100644 index 0000000..412ccd5 --- /dev/null +++ b/Server/ViewModels/SeriesVm.cs @@ -0,0 +1,32 @@ +using System.ComponentModel.DataAnnotations.Schema; + +namespace Bibblan.ViewModels +{ + public class Tag + { + public long Id { get; set; } + public string Name { get; set; } + public int BookCount { get; set; } + } + + public class ListBook + { + public long Id { get; set; } + public string Title { get; set; } + public DateTime PubDate { get; set; } + public double SeriesIndex { get; set; } + public long AuthorId { get; set; } + public string AuthorName { get; set; } + + } + + public class SeriesVm + { + [Column("seriesid")] + public long Id { get; set; } + [Column("seriesname")] + public string Name { get; set; } + public List Books { get; set; } = new(); + + } +}