serier och taggar
This commit is contained in:
		@@ -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 = () => {
 | 
			
		||||
								<a class="nav-link" href="/authors">
 | 
			
		||||
									Authors
 | 
			
		||||
								</a>
 | 
			
		||||
								<a class="nav-link" href="/series">
 | 
			
		||||
									Series
 | 
			
		||||
								</a>
 | 
			
		||||
							</div>
 | 
			
		||||
						</div>
 | 
			
		||||
					</div>
 | 
			
		||||
@@ -46,6 +50,7 @@ const App: Component = () => {
 | 
			
		||||
					<Route path="/books/author/:authorid" component={BookList} />
 | 
			
		||||
					<Route path="/books" component={BookList} />
 | 
			
		||||
					<Route path="/authors" component={AuthorList} />
 | 
			
		||||
					<Route path="/series" component={SeriesList} />
 | 
			
		||||
					<Route path="/" component={Home} />
 | 
			
		||||
				</Router>
 | 
			
		||||
			</main>
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										89
									
								
								Frontend/src/components/SeriesList.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										89
									
								
								Frontend/src/components/SeriesList.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -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<series[]>([]);
 | 
			
		||||
	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 (
 | 
			
		||||
		<div>
 | 
			
		||||
			<h1>Series!</h1>
 | 
			
		||||
			<input
 | 
			
		||||
				type="text"
 | 
			
		||||
				class="form-control mb-3"
 | 
			
		||||
				placeholder="Search..."
 | 
			
		||||
				onInput={(e) => update(e.currentTarget.value)}
 | 
			
		||||
			/>
 | 
			
		||||
			<table class="table p-3">
 | 
			
		||||
				<thead>
 | 
			
		||||
					<tr>
 | 
			
		||||
						<th>ID</th>
 | 
			
		||||
						<th>Name</th>
 | 
			
		||||
						<th>Author</th>
 | 
			
		||||
						<th>Book Count</th>
 | 
			
		||||
						<th>Published</th>
 | 
			
		||||
					</tr>
 | 
			
		||||
				</thead>
 | 
			
		||||
				<tbody>
 | 
			
		||||
					<For each={series()}>
 | 
			
		||||
						{(item) => (
 | 
			
		||||
							<tr>
 | 
			
		||||
								<td>{item.id}</td>
 | 
			
		||||
								<td>{item.name}</td>
 | 
			
		||||
								<td>
 | 
			
		||||
									<For each={distinctAuthors(item.books)}>
 | 
			
		||||
										{(author) => (
 | 
			
		||||
											<a href={`/books/author/${author.id}`}>{author.name}</a>
 | 
			
		||||
										)}
 | 
			
		||||
									</For>
 | 
			
		||||
								</td>
 | 
			
		||||
								<td>{item.books.length}</td>
 | 
			
		||||
								<td>
 | 
			
		||||
									{minDate(item.books) ?? "N/A"} -{" "}
 | 
			
		||||
									{maxDate(item.books) ?? "N/A"}
 | 
			
		||||
								</td>
 | 
			
		||||
							</tr>
 | 
			
		||||
						)}
 | 
			
		||||
					</For>
 | 
			
		||||
				</tbody>
 | 
			
		||||
			</table>
 | 
			
		||||
		</div>
 | 
			
		||||
	);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default SeriesList;
 | 
			
		||||
@@ -25,6 +25,17 @@ const BibblanService = {
 | 
			
		||||
		const response = await fetch(url);
 | 
			
		||||
		return response.json();
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	getSeries: async (
 | 
			
		||||
		query: string | undefined = undefined
 | 
			
		||||
	): Promise<author[]> => {
 | 
			
		||||
		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;
 | 
			
		||||
 
 | 
			
		||||
@@ -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<listBook>;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export type { book, author, series, listBook };
 | 
			
		||||
 
 | 
			
		||||
@@ -51,5 +51,59 @@ namespace Bibblan.Business.Services
 | 
			
		||||
            return conn.Query<AuthorVm>(query).Take(count).ToList();
 | 
			
		||||
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        internal List<SeriesVm> 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<long, SeriesVm>();
 | 
			
		||||
            conn.Query<SeriesVm, Books, Authors, SeriesVm>(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<Tag> 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<Tag>(query).ToList();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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<PostgresCalibreContext>(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<ColumnAttribute>()
 | 
			
		||||
                    .Any(attr => attr.Name == columnName))));
 | 
			
		||||
 | 
			
		||||
app.UseDefaultFiles();
 | 
			
		||||
app.MapStaticAssets();
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										32
									
								
								Server/ViewModels/SeriesVm.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								Server/ViewModels/SeriesVm.cs
									
									
									
									
									
										Normal file
									
								
							@@ -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<ListBook> Books { get; set; } = new();
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
		Reference in New Issue
	
	Block a user