Compare commits

...

5 Commits

Author SHA1 Message Date
gamer147
48ee43c4f6 [FA-24] Reading lists
All checks were successful
CI / build-backend (pull_request) Successful in 1m32s
CI / build-frontend (pull_request) Successful in 42s
2026-01-19 22:06:34 -05:00
98ae4ea4f2 Merge pull request 'feature/FA-27_Bookmarks' (#59) from feature/FA-27_Bookmarks into master
All checks were successful
CI / build-backend (push) Successful in 1m16s
CI / build-frontend (push) Successful in 40s
Build Gateway / build-subgraphs (map[name:novel-service project:FictionArchive.Service.NovelService subgraph:Novel]) (push) Successful in 47s
Build Gateway / build-subgraphs (map[name:scheduler-service project:FictionArchive.Service.SchedulerService subgraph:Scheduler]) (push) Successful in 42s
Build Gateway / build-subgraphs (map[name:translation-service project:FictionArchive.Service.TranslationService subgraph:Translation]) (push) Successful in 45s
Build Gateway / build-subgraphs (map[name:user-service project:FictionArchive.Service.UserService subgraph:User]) (push) Successful in 43s
Build Gateway / build-subgraphs (map[name:usernoveldata-service project:FictionArchive.Service.UserNovelDataService subgraph:UserNovelData]) (push) Successful in 43s
Release / build-and-push (map[dockerfile:FictionArchive.Service.AuthenticationService/Dockerfile name:authentication-service]) (push) Successful in 2m19s
Release / build-and-push (map[dockerfile:FictionArchive.Service.FileService/Dockerfile name:file-service]) (push) Successful in 2m3s
Release / build-and-push (map[dockerfile:FictionArchive.Service.NovelService/Dockerfile name:novel-service]) (push) Successful in 1m41s
Release / build-and-push (map[dockerfile:FictionArchive.Service.SchedulerService/Dockerfile name:scheduler-service]) (push) Successful in 1m37s
Release / build-and-push (map[dockerfile:FictionArchive.Service.TranslationService/Dockerfile name:translation-service]) (push) Successful in 1m48s
Release / build-and-push (map[dockerfile:FictionArchive.Service.UserNovelDataService/Dockerfile name:usernoveldata-service]) (push) Successful in 1m34s
Release / build-and-push (map[dockerfile:FictionArchive.Service.UserService/Dockerfile name:user-service]) (push) Successful in 1m33s
Release / build-frontend (push) Successful in 1m39s
Build Gateway / build-gateway (push) Successful in 3m11s
Reviewed-on: #59
2026-01-19 22:28:03 +00:00
gamer147
15e1a84f55 [FA-27] Update CICD
All checks were successful
CI / build-backend (pull_request) Successful in 1m6s
CI / build-frontend (pull_request) Successful in 41s
2026-01-19 17:03:44 -05:00
gamer147
70d4ba201a [FA-27] Fix unit test based on changes
All checks were successful
CI / build-backend (pull_request) Successful in 1m10s
CI / build-frontend (pull_request) Successful in 43s
2026-01-19 16:47:55 -05:00
gamer147
b69bcd6bf4 [FA-27] Fix user adding not using correct id
Some checks failed
CI / build-backend (pull_request) Failing after 1m2s
CI / build-frontend (pull_request) Successful in 41s
2026-01-19 16:14:49 -05:00
39 changed files with 2665 additions and 10 deletions

View File

@@ -28,6 +28,9 @@ jobs:
- name: user-service - name: user-service
project: FictionArchive.Service.UserService project: FictionArchive.Service.UserService
subgraph: User subgraph: User
- name: usernoveldata-service
project: FictionArchive.Service.UserNovelDataService
subgraph: UserNovelData
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
@@ -110,6 +113,12 @@ jobs:
name: user-service-subgraph name: user-service-subgraph
path: subgraphs/user path: subgraphs/user
- name: Download UserNovelData Service subgraph
uses: christopherhx/gitea-download-artifact@v4
with:
name: usernoveldata-service-subgraph
path: subgraphs/usernoveldata
- name: Configure subgraph URLs for Docker - name: Configure subgraph URLs for Docker
run: | run: |
for fsp in subgraphs/*/*.fsp; do for fsp in subgraphs/*/*.fsp; do

View File

@@ -27,6 +27,8 @@ jobs:
dockerfile: FictionArchive.Service.SchedulerService/Dockerfile dockerfile: FictionArchive.Service.SchedulerService/Dockerfile
- name: authentication-service - name: authentication-service
dockerfile: FictionArchive.Service.AuthenticationService/Dockerfile dockerfile: FictionArchive.Service.AuthenticationService/Dockerfile
- name: usernoveldata-service
dockerfile: FictionArchive.Service.UserNovelDataService/Dockerfile
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4

View File

@@ -106,4 +106,393 @@ public class Mutation
return new BookmarkPayload { Success = true }; return new BookmarkPayload { Success = true };
} }
[Authorize]
[Error<InvalidOperationException>]
public async Task<ReadingListPayload> CreateReadingList(
UserNovelDataServiceDbContext dbContext,
ClaimsPrincipal claimsPrincipal,
CreateReadingListInput input)
{
var oAuthProviderId = claimsPrincipal.FindFirst(ClaimTypes.NameIdentifier)?.Value;
if (string.IsNullOrEmpty(oAuthProviderId))
{
throw new InvalidOperationException("Unable to determine current user identity");
}
if (string.IsNullOrWhiteSpace(input.Name))
{
throw new InvalidOperationException("Reading list name is required");
}
var user = await dbContext.Users
.FirstOrDefaultAsync(u => u.OAuthProviderId == oAuthProviderId);
if (user == null)
{
user = new User { OAuthProviderId = oAuthProviderId };
dbContext.Users.Add(user);
await dbContext.SaveChangesAsync();
}
var readingList = new ReadingList
{
UserId = user.Id,
Name = input.Name.Trim(),
Description = input.Description?.Trim()
};
dbContext.ReadingLists.Add(readingList);
await dbContext.SaveChangesAsync();
return new ReadingListPayload
{
Success = true,
ReadingList = new ReadingListDto
{
Id = readingList.Id,
Name = readingList.Name,
Description = readingList.Description,
Items = [],
ItemCount = 0,
CreatedTime = readingList.CreatedTime
}
};
}
[Authorize]
[Error<InvalidOperationException>]
public async Task<ReadingListPayload> UpdateReadingList(
UserNovelDataServiceDbContext dbContext,
ClaimsPrincipal claimsPrincipal,
UpdateReadingListInput input)
{
var oAuthProviderId = claimsPrincipal.FindFirst(ClaimTypes.NameIdentifier)?.Value;
if (string.IsNullOrEmpty(oAuthProviderId))
{
throw new InvalidOperationException("Unable to determine current user identity");
}
if (string.IsNullOrWhiteSpace(input.Name))
{
throw new InvalidOperationException("Reading list name is required");
}
var user = await dbContext.Users
.AsNoTracking()
.FirstOrDefaultAsync(u => u.OAuthProviderId == oAuthProviderId);
if (user == null)
{
return new ReadingListPayload { Success = false };
}
var readingList = await dbContext.ReadingLists
.Include(r => r.Items)
.FirstOrDefaultAsync(r => r.Id == input.Id && r.UserId == user.Id);
if (readingList == null)
{
return new ReadingListPayload { Success = false };
}
readingList.Name = input.Name.Trim();
readingList.Description = input.Description?.Trim();
await dbContext.SaveChangesAsync();
return new ReadingListPayload
{
Success = true,
ReadingList = new ReadingListDto
{
Id = readingList.Id,
Name = readingList.Name,
Description = readingList.Description,
Items = readingList.Items.OrderBy(i => i.Order).Select(i => new ReadingListItemDto
{
NovelId = i.NovelId,
Order = i.Order,
AddedTime = i.CreatedTime
}),
ItemCount = readingList.Items.Count,
CreatedTime = readingList.CreatedTime
}
};
}
[Authorize]
[Error<InvalidOperationException>]
public async Task<DeleteReadingListPayload> DeleteReadingList(
UserNovelDataServiceDbContext dbContext,
ClaimsPrincipal claimsPrincipal,
int id)
{
var oAuthProviderId = claimsPrincipal.FindFirst(ClaimTypes.NameIdentifier)?.Value;
if (string.IsNullOrEmpty(oAuthProviderId))
{
throw new InvalidOperationException("Unable to determine current user identity");
}
var user = await dbContext.Users
.AsNoTracking()
.FirstOrDefaultAsync(u => u.OAuthProviderId == oAuthProviderId);
if (user == null)
{
return new DeleteReadingListPayload { Success = false };
}
var readingList = await dbContext.ReadingLists
.FirstOrDefaultAsync(r => r.Id == id && r.UserId == user.Id);
if (readingList == null)
{
return new DeleteReadingListPayload { Success = false };
}
dbContext.ReadingLists.Remove(readingList);
await dbContext.SaveChangesAsync();
return new DeleteReadingListPayload { Success = true };
}
[Authorize]
[Error<InvalidOperationException>]
public async Task<ReadingListPayload> AddToReadingList(
UserNovelDataServiceDbContext dbContext,
ClaimsPrincipal claimsPrincipal,
AddToReadingListInput input)
{
var oAuthProviderId = claimsPrincipal.FindFirst(ClaimTypes.NameIdentifier)?.Value;
if (string.IsNullOrEmpty(oAuthProviderId))
{
throw new InvalidOperationException("Unable to determine current user identity");
}
var user = await dbContext.Users
.AsNoTracking()
.FirstOrDefaultAsync(u => u.OAuthProviderId == oAuthProviderId);
if (user == null)
{
return new ReadingListPayload { Success = false };
}
var readingList = await dbContext.ReadingLists
.Include(r => r.Items)
.FirstOrDefaultAsync(r => r.Id == input.ReadingListId && r.UserId == user.Id);
if (readingList == null)
{
return new ReadingListPayload { Success = false };
}
// Idempotent: if already in list, return success
var existingItem = readingList.Items.FirstOrDefault(i => i.NovelId == input.NovelId);
if (existingItem != null)
{
return new ReadingListPayload
{
Success = true,
ReadingList = new ReadingListDto
{
Id = readingList.Id,
Name = readingList.Name,
Description = readingList.Description,
Items = readingList.Items.OrderBy(i => i.Order).Select(i => new ReadingListItemDto
{
NovelId = i.NovelId,
Order = i.Order,
AddedTime = i.CreatedTime
}),
ItemCount = readingList.Items.Count,
CreatedTime = readingList.CreatedTime
}
};
}
// Add at the end (highest order + 1)
var maxOrder = readingList.Items.Any() ? readingList.Items.Max(i => i.Order) : -1;
var newItem = new ReadingListItem
{
ReadingListId = readingList.Id,
NovelId = input.NovelId,
Order = maxOrder + 1
};
dbContext.ReadingListItems.Add(newItem);
await dbContext.SaveChangesAsync();
// Reload to get updated items
readingList = await dbContext.ReadingLists
.AsNoTracking()
.Include(r => r.Items.OrderBy(i => i.Order))
.FirstAsync(r => r.Id == input.ReadingListId);
return new ReadingListPayload
{
Success = true,
ReadingList = new ReadingListDto
{
Id = readingList.Id,
Name = readingList.Name,
Description = readingList.Description,
Items = readingList.Items.Select(i => new ReadingListItemDto
{
NovelId = i.NovelId,
Order = i.Order,
AddedTime = i.CreatedTime
}),
ItemCount = readingList.Items.Count,
CreatedTime = readingList.CreatedTime
}
};
}
[Authorize]
[Error<InvalidOperationException>]
public async Task<ReadingListPayload> RemoveFromReadingList(
UserNovelDataServiceDbContext dbContext,
ClaimsPrincipal claimsPrincipal,
int listId,
uint novelId)
{
var oAuthProviderId = claimsPrincipal.FindFirst(ClaimTypes.NameIdentifier)?.Value;
if (string.IsNullOrEmpty(oAuthProviderId))
{
throw new InvalidOperationException("Unable to determine current user identity");
}
var user = await dbContext.Users
.AsNoTracking()
.FirstOrDefaultAsync(u => u.OAuthProviderId == oAuthProviderId);
if (user == null)
{
return new ReadingListPayload { Success = false };
}
var readingList = await dbContext.ReadingLists
.Include(r => r.Items)
.FirstOrDefaultAsync(r => r.Id == listId && r.UserId == user.Id);
if (readingList == null)
{
return new ReadingListPayload { Success = false };
}
var item = readingList.Items.FirstOrDefault(i => i.NovelId == novelId);
if (item != null)
{
dbContext.ReadingListItems.Remove(item);
await dbContext.SaveChangesAsync();
}
// Reload to get updated items
readingList = await dbContext.ReadingLists
.AsNoTracking()
.Include(r => r.Items.OrderBy(i => i.Order))
.FirstAsync(r => r.Id == listId);
return new ReadingListPayload
{
Success = true,
ReadingList = new ReadingListDto
{
Id = readingList.Id,
Name = readingList.Name,
Description = readingList.Description,
Items = readingList.Items.Select(i => new ReadingListItemDto
{
NovelId = i.NovelId,
Order = i.Order,
AddedTime = i.CreatedTime
}),
ItemCount = readingList.Items.Count,
CreatedTime = readingList.CreatedTime
}
};
}
[Authorize]
[Error<InvalidOperationException>]
public async Task<ReadingListPayload> ReorderReadingListItem(
UserNovelDataServiceDbContext dbContext,
ClaimsPrincipal claimsPrincipal,
ReorderReadingListItemInput input)
{
var oAuthProviderId = claimsPrincipal.FindFirst(ClaimTypes.NameIdentifier)?.Value;
if (string.IsNullOrEmpty(oAuthProviderId))
{
throw new InvalidOperationException("Unable to determine current user identity");
}
var user = await dbContext.Users
.AsNoTracking()
.FirstOrDefaultAsync(u => u.OAuthProviderId == oAuthProviderId);
if (user == null)
{
return new ReadingListPayload { Success = false };
}
var readingList = await dbContext.ReadingLists
.Include(r => r.Items)
.FirstOrDefaultAsync(r => r.Id == input.ReadingListId && r.UserId == user.Id);
if (readingList == null)
{
return new ReadingListPayload { Success = false };
}
var item = readingList.Items.FirstOrDefault(i => i.NovelId == input.NovelId);
if (item == null)
{
throw new InvalidOperationException("Novel not found in reading list");
}
var oldOrder = item.Order;
var newOrder = input.NewOrder;
// Shift other items
if (newOrder < oldOrder)
{
// Moving up: shift items between newOrder and oldOrder down
foreach (var i in readingList.Items.Where(x => x.Order >= newOrder && x.Order < oldOrder))
{
i.Order++;
}
}
else if (newOrder > oldOrder)
{
// Moving down: shift items between oldOrder and newOrder up
foreach (var i in readingList.Items.Where(x => x.Order > oldOrder && x.Order <= newOrder))
{
i.Order--;
}
}
item.Order = newOrder;
await dbContext.SaveChangesAsync();
return new ReadingListPayload
{
Success = true,
ReadingList = new ReadingListDto
{
Id = readingList.Id,
Name = readingList.Name,
Description = readingList.Description,
Items = readingList.Items.OrderBy(i => i.Order).Select(i => new ReadingListItemDto
{
NovelId = i.NovelId,
Order = i.Order,
AddedTime = i.CreatedTime
}),
ItemCount = readingList.Items.Count,
CreatedTime = readingList.CreatedTime
}
};
}
} }

View File

@@ -1,4 +1,5 @@
using System.Security.Claims; using System.Security.Claims;
using FictionArchive.Service.UserNovelDataService.Models.Database;
using FictionArchive.Service.UserNovelDataService.Models.DTOs; using FictionArchive.Service.UserNovelDataService.Models.DTOs;
using FictionArchive.Service.UserNovelDataService.Services; using FictionArchive.Service.UserNovelDataService.Services;
using HotChocolate.Authorization; using HotChocolate.Authorization;
@@ -42,4 +43,94 @@ public class Query
CreatedTime = b.CreatedTime CreatedTime = b.CreatedTime
}); });
} }
[Authorize]
public async Task<IEnumerable<ReadingListDto>> GetReadingLists(
UserNovelDataServiceDbContext dbContext,
ClaimsPrincipal claimsPrincipal)
{
var oAuthProviderId = claimsPrincipal.FindFirst(ClaimTypes.NameIdentifier)?.Value;
if (string.IsNullOrEmpty(oAuthProviderId))
{
return [];
}
var user = await dbContext.Users
.AsNoTracking()
.FirstOrDefaultAsync(u => u.OAuthProviderId == oAuthProviderId);
if (user == null)
{
return [];
}
var lists = await dbContext.ReadingLists
.AsNoTracking()
.Include(r => r.Items)
.Where(r => r.UserId == user.Id)
.OrderByDescending(r => r.LastUpdatedTime)
.ToListAsync();
return lists.Select(r => new ReadingListDto
{
Id = r.Id,
Name = r.Name,
Description = r.Description,
ItemCount = r.Items.Count,
Items = r.Items.Select(i => new ReadingListItemDto
{
NovelId = i.NovelId,
Order = i.Order,
AddedTime = i.CreatedTime
}),
CreatedTime = r.CreatedTime
});
}
[Authorize]
public async Task<ReadingListDto?> GetReadingList(
UserNovelDataServiceDbContext dbContext,
ClaimsPrincipal claimsPrincipal,
int id)
{
var oAuthProviderId = claimsPrincipal.FindFirst(ClaimTypes.NameIdentifier)?.Value;
if (string.IsNullOrEmpty(oAuthProviderId))
{
return null;
}
var user = await dbContext.Users
.AsNoTracking()
.FirstOrDefaultAsync(u => u.OAuthProviderId == oAuthProviderId);
if (user == null)
{
return null;
}
var readingList = await dbContext.ReadingLists
.AsNoTracking()
.Include(r => r.Items.OrderBy(i => i.Order))
.FirstOrDefaultAsync(r => r.Id == id && r.UserId == user.Id);
if (readingList == null)
{
return null;
}
return new ReadingListDto
{
Id = readingList.Id,
Name = readingList.Name,
Description = readingList.Description,
ItemCount = readingList.Items.Count,
Items = readingList.Items.Select(i => new ReadingListItemDto
{
NovelId = i.NovelId,
Order = i.Order,
AddedTime = i.CreatedTime
}),
CreatedTime = readingList.CreatedTime
};
}
} }

View File

@@ -0,0 +1,289 @@
// <auto-generated />
using System;
using FictionArchive.Service.UserNovelDataService.Services;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using NodaTime;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace FictionArchive.Service.UserNovelDataService.Migrations
{
[DbContext(typeof(UserNovelDataServiceDbContext))]
[Migration("20260120014840_AddReadingLists")]
partial class AddReadingLists
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "9.0.11")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("FictionArchive.Service.UserNovelDataService.Models.Database.Bookmark", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<long>("ChapterId")
.HasColumnType("bigint");
b.Property<Instant>("CreatedTime")
.HasColumnType("timestamp with time zone");
b.Property<string>("Description")
.HasColumnType("text");
b.Property<Instant>("LastUpdatedTime")
.HasColumnType("timestamp with time zone");
b.Property<long>("NovelId")
.HasColumnType("bigint");
b.Property<Guid>("UserId")
.HasColumnType("uuid");
b.HasKey("Id");
b.HasIndex("UserId", "ChapterId")
.IsUnique();
b.HasIndex("UserId", "NovelId");
b.ToTable("Bookmarks");
});
modelBuilder.Entity("FictionArchive.Service.UserNovelDataService.Models.Database.Chapter", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.Property<Instant>("CreatedTime")
.HasColumnType("timestamp with time zone");
b.Property<Instant>("LastUpdatedTime")
.HasColumnType("timestamp with time zone");
b.Property<long>("VolumeId")
.HasColumnType("bigint");
b.HasKey("Id");
b.HasIndex("VolumeId");
b.ToTable("Chapters");
});
modelBuilder.Entity("FictionArchive.Service.UserNovelDataService.Models.Database.Novel", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.Property<Instant>("CreatedTime")
.HasColumnType("timestamp with time zone");
b.Property<Instant>("LastUpdatedTime")
.HasColumnType("timestamp with time zone");
b.HasKey("Id");
b.ToTable("Novels");
});
modelBuilder.Entity("FictionArchive.Service.UserNovelDataService.Models.Database.ReadingList", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<Instant>("CreatedTime")
.HasColumnType("timestamp with time zone");
b.Property<string>("Description")
.HasColumnType("text");
b.Property<Instant>("LastUpdatedTime")
.HasColumnType("timestamp with time zone");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text");
b.Property<Guid>("UserId")
.HasColumnType("uuid");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("ReadingLists");
});
modelBuilder.Entity("FictionArchive.Service.UserNovelDataService.Models.Database.ReadingListItem", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<Instant>("CreatedTime")
.HasColumnType("timestamp with time zone");
b.Property<Instant>("LastUpdatedTime")
.HasColumnType("timestamp with time zone");
b.Property<long>("NovelId")
.HasColumnType("bigint");
b.Property<int>("Order")
.HasColumnType("integer");
b.Property<int>("ReadingListId")
.HasColumnType("integer");
b.HasKey("Id");
b.HasIndex("ReadingListId", "NovelId")
.IsUnique();
b.HasIndex("ReadingListId", "Order");
b.ToTable("ReadingListItems");
});
modelBuilder.Entity("FictionArchive.Service.UserNovelDataService.Models.Database.User", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<Instant>("CreatedTime")
.HasColumnType("timestamp with time zone");
b.Property<Instant>("LastUpdatedTime")
.HasColumnType("timestamp with time zone");
b.Property<string>("OAuthProviderId")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.ToTable("Users");
});
modelBuilder.Entity("FictionArchive.Service.UserNovelDataService.Models.Database.Volume", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.Property<Instant>("CreatedTime")
.HasColumnType("timestamp with time zone");
b.Property<Instant>("LastUpdatedTime")
.HasColumnType("timestamp with time zone");
b.Property<long>("NovelId")
.HasColumnType("bigint");
b.HasKey("Id");
b.HasIndex("NovelId");
b.ToTable("Volumes");
});
modelBuilder.Entity("FictionArchive.Service.UserNovelDataService.Models.Database.Bookmark", b =>
{
b.HasOne("FictionArchive.Service.UserNovelDataService.Models.Database.User", "User")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("User");
});
modelBuilder.Entity("FictionArchive.Service.UserNovelDataService.Models.Database.Chapter", b =>
{
b.HasOne("FictionArchive.Service.UserNovelDataService.Models.Database.Volume", "Volume")
.WithMany("Chapters")
.HasForeignKey("VolumeId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Volume");
});
modelBuilder.Entity("FictionArchive.Service.UserNovelDataService.Models.Database.ReadingList", b =>
{
b.HasOne("FictionArchive.Service.UserNovelDataService.Models.Database.User", "User")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("User");
});
modelBuilder.Entity("FictionArchive.Service.UserNovelDataService.Models.Database.ReadingListItem", b =>
{
b.HasOne("FictionArchive.Service.UserNovelDataService.Models.Database.ReadingList", "ReadingList")
.WithMany("Items")
.HasForeignKey("ReadingListId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("ReadingList");
});
modelBuilder.Entity("FictionArchive.Service.UserNovelDataService.Models.Database.Volume", b =>
{
b.HasOne("FictionArchive.Service.UserNovelDataService.Models.Database.Novel", "Novel")
.WithMany("Volumes")
.HasForeignKey("NovelId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Novel");
});
modelBuilder.Entity("FictionArchive.Service.UserNovelDataService.Models.Database.Novel", b =>
{
b.Navigation("Volumes");
});
modelBuilder.Entity("FictionArchive.Service.UserNovelDataService.Models.Database.ReadingList", b =>
{
b.Navigation("Items");
});
modelBuilder.Entity("FictionArchive.Service.UserNovelDataService.Models.Database.Volume", b =>
{
b.Navigation("Chapters");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,89 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
using NodaTime;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace FictionArchive.Service.UserNovelDataService.Migrations
{
/// <inheritdoc />
public partial class AddReadingLists : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "ReadingLists",
columns: table => new
{
Id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
UserId = table.Column<Guid>(type: "uuid", nullable: false),
Name = table.Column<string>(type: "text", nullable: false),
Description = table.Column<string>(type: "text", nullable: true),
CreatedTime = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
LastUpdatedTime = table.Column<Instant>(type: "timestamp with time zone", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_ReadingLists", x => x.Id);
table.ForeignKey(
name: "FK_ReadingLists_Users_UserId",
column: x => x.UserId,
principalTable: "Users",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "ReadingListItems",
columns: table => new
{
Id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
ReadingListId = table.Column<int>(type: "integer", nullable: false),
NovelId = table.Column<long>(type: "bigint", nullable: false),
Order = table.Column<int>(type: "integer", nullable: false),
CreatedTime = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
LastUpdatedTime = table.Column<Instant>(type: "timestamp with time zone", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_ReadingListItems", x => x.Id);
table.ForeignKey(
name: "FK_ReadingListItems_ReadingLists_ReadingListId",
column: x => x.ReadingListId,
principalTable: "ReadingLists",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_ReadingListItems_ReadingListId_NovelId",
table: "ReadingListItems",
columns: new[] { "ReadingListId", "NovelId" },
unique: true);
migrationBuilder.CreateIndex(
name: "IX_ReadingListItems_ReadingListId_Order",
table: "ReadingListItems",
columns: new[] { "ReadingListId", "Order" });
migrationBuilder.CreateIndex(
name: "IX_ReadingLists_UserId",
table: "ReadingLists",
column: "UserId");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "ReadingListItems");
migrationBuilder.DropTable(
name: "ReadingLists");
}
}
}

View File

@@ -102,6 +102,70 @@ namespace FictionArchive.Service.UserNovelDataService.Migrations
b.ToTable("Novels"); b.ToTable("Novels");
}); });
modelBuilder.Entity("FictionArchive.Service.UserNovelDataService.Models.Database.ReadingList", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<Instant>("CreatedTime")
.HasColumnType("timestamp with time zone");
b.Property<string>("Description")
.HasColumnType("text");
b.Property<Instant>("LastUpdatedTime")
.HasColumnType("timestamp with time zone");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text");
b.Property<Guid>("UserId")
.HasColumnType("uuid");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("ReadingLists");
});
modelBuilder.Entity("FictionArchive.Service.UserNovelDataService.Models.Database.ReadingListItem", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<Instant>("CreatedTime")
.HasColumnType("timestamp with time zone");
b.Property<Instant>("LastUpdatedTime")
.HasColumnType("timestamp with time zone");
b.Property<long>("NovelId")
.HasColumnType("bigint");
b.Property<int>("Order")
.HasColumnType("integer");
b.Property<int>("ReadingListId")
.HasColumnType("integer");
b.HasKey("Id");
b.HasIndex("ReadingListId", "NovelId")
.IsUnique();
b.HasIndex("ReadingListId", "Order");
b.ToTable("ReadingListItems");
});
modelBuilder.Entity("FictionArchive.Service.UserNovelDataService.Models.Database.User", b => modelBuilder.Entity("FictionArchive.Service.UserNovelDataService.Models.Database.User", b =>
{ {
b.Property<Guid>("Id") b.Property<Guid>("Id")
@@ -169,6 +233,28 @@ namespace FictionArchive.Service.UserNovelDataService.Migrations
b.Navigation("Volume"); b.Navigation("Volume");
}); });
modelBuilder.Entity("FictionArchive.Service.UserNovelDataService.Models.Database.ReadingList", b =>
{
b.HasOne("FictionArchive.Service.UserNovelDataService.Models.Database.User", "User")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("User");
});
modelBuilder.Entity("FictionArchive.Service.UserNovelDataService.Models.Database.ReadingListItem", b =>
{
b.HasOne("FictionArchive.Service.UserNovelDataService.Models.Database.ReadingList", "ReadingList")
.WithMany("Items")
.HasForeignKey("ReadingListId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("ReadingList");
});
modelBuilder.Entity("FictionArchive.Service.UserNovelDataService.Models.Database.Volume", b => modelBuilder.Entity("FictionArchive.Service.UserNovelDataService.Models.Database.Volume", b =>
{ {
b.HasOne("FictionArchive.Service.UserNovelDataService.Models.Database.Novel", "Novel") b.HasOne("FictionArchive.Service.UserNovelDataService.Models.Database.Novel", "Novel")
@@ -185,6 +271,11 @@ namespace FictionArchive.Service.UserNovelDataService.Migrations
b.Navigation("Volumes"); b.Navigation("Volumes");
}); });
modelBuilder.Entity("FictionArchive.Service.UserNovelDataService.Models.Database.ReadingList", b =>
{
b.Navigation("Items");
});
modelBuilder.Entity("FictionArchive.Service.UserNovelDataService.Models.Database.Volume", b => modelBuilder.Entity("FictionArchive.Service.UserNovelDataService.Models.Database.Volume", b =>
{ {
b.Navigation("Chapters"); b.Navigation("Chapters");

View File

@@ -0,0 +1,3 @@
namespace FictionArchive.Service.UserNovelDataService.Models.DTOs;
public record AddToReadingListInput(int ReadingListId, uint NovelId);

View File

@@ -0,0 +1,3 @@
namespace FictionArchive.Service.UserNovelDataService.Models.DTOs;
public record CreateReadingListInput(string Name, string? Description);

View File

@@ -0,0 +1,6 @@
namespace FictionArchive.Service.UserNovelDataService.Models.DTOs;
public class DeleteReadingListPayload
{
public bool Success { get; init; }
}

View File

@@ -0,0 +1,13 @@
using NodaTime;
namespace FictionArchive.Service.UserNovelDataService.Models.DTOs;
public class ReadingListDto
{
public int Id { get; init; }
public required string Name { get; init; }
public string? Description { get; init; }
public IEnumerable<ReadingListItemDto> Items { get; init; } = [];
public int ItemCount { get; init; }
public Instant CreatedTime { get; init; }
}

View File

@@ -0,0 +1,10 @@
using NodaTime;
namespace FictionArchive.Service.UserNovelDataService.Models.DTOs;
public class ReadingListItemDto
{
public uint NovelId { get; init; }
public int Order { get; init; }
public Instant AddedTime { get; init; }
}

View File

@@ -0,0 +1,7 @@
namespace FictionArchive.Service.UserNovelDataService.Models.DTOs;
public class ReadingListPayload
{
public ReadingListDto? ReadingList { get; init; }
public bool Success { get; init; }
}

View File

@@ -0,0 +1,3 @@
namespace FictionArchive.Service.UserNovelDataService.Models.DTOs;
public record ReorderReadingListItemInput(int ReadingListId, uint NovelId, int NewOrder);

View File

@@ -0,0 +1,3 @@
namespace FictionArchive.Service.UserNovelDataService.Models.DTOs;
public record UpdateReadingListInput(int Id, string Name, string? Description);

View File

@@ -0,0 +1,14 @@
using FictionArchive.Service.Shared.Models;
namespace FictionArchive.Service.UserNovelDataService.Models.Database;
public class ReadingList : BaseEntity<int>
{
public Guid UserId { get; set; }
public virtual User User { get; set; } = null!;
public required string Name { get; set; }
public string? Description { get; set; }
public virtual ICollection<ReadingListItem> Items { get; set; } = new List<ReadingListItem>();
}

View File

@@ -0,0 +1,12 @@
using FictionArchive.Service.Shared.Models;
namespace FictionArchive.Service.UserNovelDataService.Models.Database;
public class ReadingListItem : BaseEntity<int>
{
public int ReadingListId { get; set; }
public virtual ReadingList ReadingList { get; set; } = null!;
public uint NovelId { get; set; }
public int Order { get; set; }
}

View File

@@ -11,6 +11,8 @@ public class UserNovelDataServiceDbContext : FictionArchiveDbContext
public DbSet<Novel> Novels { get; set; } public DbSet<Novel> Novels { get; set; }
public DbSet<Volume> Volumes { get; set; } public DbSet<Volume> Volumes { get; set; }
public DbSet<Chapter> Chapters { get; set; } public DbSet<Chapter> Chapters { get; set; }
public DbSet<ReadingList> ReadingLists { get; set; }
public DbSet<ReadingListItem> ReadingListItems { get; set; }
public UserNovelDataServiceDbContext(DbContextOptions options, ILogger<UserNovelDataServiceDbContext> logger) : base(options, logger) public UserNovelDataServiceDbContext(DbContextOptions options, ILogger<UserNovelDataServiceDbContext> logger) : base(options, logger)
{ {
@@ -34,5 +36,32 @@ public class UserNovelDataServiceDbContext : FictionArchiveDbContext
.HasForeignKey(b => b.UserId) .HasForeignKey(b => b.UserId)
.OnDelete(DeleteBehavior.Cascade); .OnDelete(DeleteBehavior.Cascade);
}); });
modelBuilder.Entity<ReadingList>(entity =>
{
// Index for fetching user's lists
entity.HasIndex(r => r.UserId);
// User relationship with cascade delete
entity.HasOne(r => r.User)
.WithMany()
.HasForeignKey(r => r.UserId)
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity<ReadingListItem>(entity =>
{
// Unique constraint: one entry per novel per list
entity.HasIndex(i => new { i.ReadingListId, i.NovelId }).IsUnique();
// Index for efficient ordered retrieval
entity.HasIndex(i => new { i.ReadingListId, i.Order });
// ReadingList relationship with cascade delete
entity.HasOne(i => i.ReadingList)
.WithMany(r => r.Items)
.HasForeignKey(i => i.ReadingListId)
.OnDelete(DeleteBehavior.Cascade);
});
} }
} }

View File

@@ -213,10 +213,10 @@ public class UserManagementServiceTests
dbContext.Users.Add(inviter); dbContext.Users.Add(inviter);
await dbContext.SaveChangesAsync(); await dbContext.SaveChangesAsync();
var authentikUid = "authentik-uid-789"; var authentikPk = 456;
var authClient = Substitute.For<IAuthenticationServiceClient>(); var authClient = Substitute.For<IAuthenticationServiceClient>();
authClient.CreateUserAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>()) authClient.CreateUserAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>())
.Returns(new AuthentikUserResponse { Pk = 456, Uid = authentikUid }); .Returns(new AuthentikUserResponse { Pk = authentikPk, Uid = "authentik-uid-789" });
authClient.SendRecoveryEmailAsync(Arg.Any<int>()).Returns(true); authClient.SendRecoveryEmailAsync(Arg.Any<int>()).Returns(true);
var service = CreateService(dbContext, authClient); var service = CreateService(dbContext, authClient);
@@ -228,7 +228,7 @@ public class UserManagementServiceTests
result.Should().NotBeNull(); result.Should().NotBeNull();
result!.Username.Should().Be("newusername"); result!.Username.Should().Be("newusername");
result.Email.Should().Be("newuser@test.com"); result.Email.Should().Be("newuser@test.com");
result.OAuthProviderId.Should().Be(authentikUid); result.OAuthProviderId.Should().Be(authentikPk.ToString());
result.InviterId.Should().Be(inviter.Id); result.InviterId.Should().Be(inviter.Id);
result.AvailableInvites.Should().Be(0); result.AvailableInvites.Should().Be(0);
result.Disabled.Should().BeFalse(); result.Disabled.Should().BeFalse();

View File

@@ -86,7 +86,7 @@ public class UserManagementService
{ {
Username = username, Username = username,
Email = email, Email = email,
OAuthProviderId = authentikUser.Uid, OAuthProviderId = authentikUser.Pk.ToString(),
Disabled = false, Disabled = false,
AvailableInvites = 0, AvailableInvites = 0,
InviterId = inviter.Id InviterId = inviter.Id

View File

@@ -5,7 +5,8 @@ services:
postgres: postgres:
image: postgres:16-alpine image: postgres:16-alpine
networks: networks:
- fictionarchive fictionarchive:
ipv4_address: 172.20.0.10
environment: environment:
POSTGRES_USER: ${POSTGRES_USER:-postgres} POSTGRES_USER: ${POSTGRES_USER:-postgres}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-postgres} POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-postgres}
@@ -23,10 +24,12 @@ services:
rabbitmq: rabbitmq:
image: rabbitmq:3-management-alpine image: rabbitmq:3-management-alpine
networks: networks:
- fictionarchive fictionarchive:
ipv4_address: 172.20.0.11
environment: environment:
RABBITMQ_DEFAULT_USER: ${RABBITMQ_USER:-guest} RABBITMQ_DEFAULT_USER: ${RABBITMQ_USER:-guest}
RABBITMQ_DEFAULT_PASS: ${RABBITMQ_PASSWORD:-guest} RABBITMQ_DEFAULT_PASS: ${RABBITMQ_PASSWORD:-guest}
RABBITMQ_SERVER_ADDITIONAL_ERL_ARGS: -rabbit max_message_size 536870912
volumes: volumes:
- /srv/docker_volumes/fictionarchive/rabbitmq:/var/lib/rabbitmq - /srv/docker_volumes/fictionarchive/rabbitmq:/var/lib/rabbitmq
healthcheck: healthcheck:
@@ -36,10 +39,14 @@ services:
retries: 5 retries: 5
restart: unless-stopped restart: unless-stopped
# ===========================================
# VPN Container
# ===========================================
vpn: vpn:
image: dperson/openvpn-client # or gluetun, wireguard, etc. image: dperson/openvpn-client
networks: networks:
fictionarchive: fictionarchive:
ipv4_address: 172.20.0.20
aliases: aliases:
- novel-service - novel-service
cap_add: cap_add:
@@ -48,6 +55,19 @@ services:
- /dev/net/tun - /dev/net/tun
volumes: volumes:
- /srv/docker_volumes/korean_vpn:/vpn - /srv/docker_volumes/korean_vpn:/vpn
dns:
- 192.168.3.1
environment:
- DNS=1.1.1.1,8.8.8.8
extra_hosts:
- "postgres:172.20.0.10"
- "rabbitmq:172.20.0.11"
healthcheck:
test: ["CMD", "ping", "-c", "1", "-W", "5", "1.1.1.1"]
interval: 30s
timeout: 10s
retries: 3
start_period: 30s
restart: unless-stopped restart: unless-stopped
# =========================================== # ===========================================
@@ -67,7 +87,7 @@ services:
rabbitmq: rabbitmq:
condition: service_healthy condition: service_healthy
vpn: vpn:
condition: service_started condition: service_healthy
network_mode: "service:vpn" network_mode: "service:vpn"
restart: unless-stopped restart: unless-stopped
@@ -102,6 +122,20 @@ services:
condition: service_healthy condition: service_healthy
restart: unless-stopped restart: unless-stopped
usernoveldata-service:
image: git.orfl.xyz/conco/fictionarchive-usernoveldata-service:latest
networks:
- fictionarchive
environment:
ConnectionStrings__DefaultConnection: Host=postgres;Database=FictionArchive_UserNovelDataService;Username=${POSTGRES_USER:-postgres};Password=${POSTGRES_PASSWORD:-postgres}
RabbitMQ__ConnectionString: amqp://${RABBITMQ_USER:-guest}:${RABBITMQ_PASSWORD:-guest}@rabbitmq
depends_on:
postgres:
condition: service_healthy
rabbitmq:
condition: service_healthy
restart: unless-stopped
file-service: file-service:
image: git.orfl.xyz/conco/fictionarchive-file-service:latest image: git.orfl.xyz/conco/fictionarchive-file-service:latest
networks: networks:
@@ -144,6 +178,7 @@ services:
- scheduler-service - scheduler-service
- file-service - file-service
- user-service - user-service
- usernoveldata-service
restart: unless-stopped restart: unless-stopped
# =========================================== # ===========================================
@@ -165,3 +200,7 @@ networks:
web: web:
external: yes external: yes
fictionarchive: fictionarchive:
ipam:
driver: default
config:
- subnet: 172.20.0.0/24

View File

@@ -0,0 +1,368 @@
<script lang="ts">
import { SvelteSet } from 'svelte/reactivity';
import { Button } from '$lib/components/ui/button';
import { Popover, PopoverTrigger, PopoverContent } from '$lib/components/ui/popover';
import { Input } from '$lib/components/ui/input';
import { client } from '$lib/graphql/client';
import {
GetReadingListsWithItemsDocument,
AddToReadingListDocument,
RemoveFromReadingListDocument,
CreateReadingListDocument,
type GetReadingListsWithItemsQuery
} from '$lib/graphql/__generated__/graphql';
import { isAuthenticated } from '$lib/auth/authStore';
import ListPlus from '@lucide/svelte/icons/list-plus';
import Plus from '@lucide/svelte/icons/plus';
import Check from '@lucide/svelte/icons/check';
import Loader2 from '@lucide/svelte/icons/loader-2';
interface Props {
novelId: number;
size?: 'default' | 'sm' | 'icon';
}
let { novelId, size = 'default' }: Props = $props();
type ReadingList = GetReadingListsWithItemsQuery['readingLists'][0];
// State
let popoverOpen = $state(false);
let readingLists: ReadingList[] = $state([]);
let fetching = $state(false);
let error: string | null = $state(null);
// Track which lists the novel is in (by list ID)
let novelInLists = new SvelteSet<number>();
// Track loading state for individual list toggles
let loadingListIds = new SvelteSet<number>();
// Quick-create state
let showQuickCreate = $state(false);
let newListName = $state('');
let creatingList = $state(false);
let createError: string | null = $state(null);
// Fetch reading lists when popover opens
$effect(() => {
if (popoverOpen && $isAuthenticated) {
fetchReadingLists();
}
});
// Reset quick-create form when popover closes
$effect(() => {
if (!popoverOpen) {
showQuickCreate = false;
newListName = '';
createError = null;
}
});
async function fetchReadingLists() {
fetching = true;
error = null;
try {
const result = await client.query(GetReadingListsWithItemsDocument, {}).toPromise();
if (result.error) {
error = result.error.message;
return;
}
if (result.data) {
readingLists = result.data.readingLists;
// Build the set of list IDs that contain this novel
const inLists = new SvelteSet<number>();
for (const list of readingLists) {
if (list.items.some((item) => item.novelId === novelId)) {
inLists.add(list.id);
}
}
novelInLists = inLists;
}
} catch (e) {
error = e instanceof Error ? e.message : 'Failed to load reading lists';
} finally {
fetching = false;
}
}
async function toggleNovelInList(listId: number) {
const isInList = novelInLists.has(listId);
loadingListIds = new SvelteSet([...loadingListIds, listId]);
try {
if (isInList) {
// Remove from list
const result = await client
.mutation(RemoveFromReadingListDocument, {
input: { listId, novelId }
})
.toPromise();
if (result.error) {
error = result.error.message;
return;
}
if (result.data?.removeFromReadingList?.errors?.length) {
error = result.data.removeFromReadingList.errors[0]?.message ?? 'Failed to remove from list';
return;
}
if (result.data?.removeFromReadingList?.readingListPayload?.success) {
// Update local state
novelInLists = new SvelteSet([...novelInLists].filter((id) => id !== listId));
// Update item count in list
readingLists = readingLists.map((list) =>
list.id === listId
? { ...list, itemCount: list.itemCount - 1, items: list.items.filter((i) => i.novelId !== novelId) }
: list
);
}
} else {
// Add to list
const result = await client
.mutation(AddToReadingListDocument, {
input: { readingListId: listId, novelId }
})
.toPromise();
if (result.error) {
error = result.error.message;
return;
}
if (result.data?.addToReadingList?.errors?.length) {
error = result.data.addToReadingList.errors[0]?.message ?? 'Failed to add to list';
return;
}
if (result.data?.addToReadingList?.readingListPayload?.success) {
// Update local state
novelInLists = new SvelteSet([...novelInLists, listId]);
// Update item count in list
readingLists = readingLists.map((list) =>
list.id === listId
? {
...list,
itemCount: list.itemCount + 1,
items: [...list.items, { novelId, order: list.itemCount, addedTime: new Date().toISOString() }]
}
: list
);
}
}
} catch (e) {
error = e instanceof Error ? e.message : 'An error occurred';
} finally {
loadingListIds = new SvelteSet([...loadingListIds].filter((id) => id !== listId));
}
}
async function createListAndAdd() {
if (!newListName.trim()) {
createError = 'Name is required';
return;
}
creatingList = true;
createError = null;
try {
const result = await client
.mutation(CreateReadingListDocument, {
input: {
name: newListName.trim(),
description: null
}
})
.toPromise();
if (result.error) {
createError = result.error.message;
return;
}
if (result.data?.createReadingList?.errors?.length) {
createError = result.data.createReadingList.errors[0]?.message ?? 'Failed to create list';
return;
}
const newList = result.data?.createReadingList?.readingListPayload?.readingList;
if (newList) {
// Now add the novel to the new list
const addResult = await client
.mutation(AddToReadingListDocument, {
input: { readingListId: newList.id, novelId }
})
.toPromise();
if (addResult.error) {
createError = addResult.error.message;
return;
}
if (addResult.data?.addToReadingList?.errors?.length) {
createError = addResult.data.addToReadingList.errors[0]?.message ?? 'Failed to add to list';
return;
}
// Add the new list to our local state
const fullNewList: ReadingList = {
...newList,
itemCount: 1,
items: [{ novelId, order: 0, addedTime: new Date().toISOString() }]
};
readingLists = [...readingLists, fullNewList];
novelInLists = new SvelteSet([...novelInLists, newList.id]);
// Reset quick-create form
showQuickCreate = false;
newListName = '';
}
} catch (e) {
createError = e instanceof Error ? e.message : 'An error occurred';
} finally {
creatingList = false;
}
}
function handleClick(e: MouseEvent) {
e.preventDefault();
e.stopPropagation();
}
function handleKeyDown(e: KeyboardEvent) {
if (e.key === 'Enter') {
e.preventDefault();
createListAndAdd();
}
}
// Compute if novel is in any list for button state
let isInAnyList = $derived(novelInLists.size > 0);
</script>
{#if $isAuthenticated}
<!-- svelte-ignore a11y_click_events_have_key_events -->
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div onclick={handleClick}>
<Popover bind:open={popoverOpen}>
<PopoverTrigger asChild>
{#snippet child({ props })}
<Button
variant={isInAnyList ? 'default' : 'outline'}
{size}
class={size === 'icon' ? 'h-8 w-8' : 'gap-2'}
{...props}
>
<ListPlus class="h-4 w-4" />
{#if size !== 'icon'}
<span>{isInAnyList ? 'In Lists' : 'Add to List'}</span>
{/if}
</Button>
{/snippet}
</PopoverTrigger>
<PopoverContent class="w-80">
<div class="space-y-4">
<div class="space-y-2">
<h4 class="font-medium leading-none">Add to Reading List</h4>
<p class="text-sm text-muted-foreground">
Select lists to add or remove this novel.
</p>
</div>
{#if fetching}
<div class="flex items-center justify-center py-4">
<Loader2 class="h-5 w-5 animate-spin text-muted-foreground" />
</div>
{:else if error}
<p class="text-sm text-destructive">{error}</p>
{:else if readingLists.length === 0 && !showQuickCreate}
<div class="text-center py-4">
<p class="text-sm text-muted-foreground mb-3">No reading lists yet</p>
<Button size="sm" variant="outline" onclick={() => (showQuickCreate = true)}>
<Plus class="h-4 w-4 mr-1" />
Create your first list
</Button>
</div>
{:else}
<!-- Reading lists -->
<div class="space-y-1 max-h-[200px] overflow-y-auto">
{#each readingLists as list (list.id)}
{@const isInList = novelInLists.has(list.id)}
{@const isLoading = loadingListIds.has(list.id)}
<button
type="button"
class="flex w-full items-center gap-3 rounded-md px-2 py-2 text-left hover:bg-accent transition-colors disabled:opacity-50"
onclick={() => toggleNovelInList(list.id)}
disabled={isLoading}
>
<div
class="flex h-4 w-4 shrink-0 items-center justify-center rounded border {isInList
? 'bg-primary border-primary'
: 'border-input'}"
>
{#if isLoading}
<Loader2 class="h-3 w-3 animate-spin text-primary-foreground" />
{:else if isInList}
<Check class="h-3 w-3 text-primary-foreground" />
{/if}
</div>
<div class="flex-1 min-w-0">
<div class="text-sm font-medium truncate">{list.name}</div>
<div class="text-xs text-muted-foreground">
{list.itemCount} {list.itemCount === 1 ? 'novel' : 'novels'}
</div>
</div>
</button>
{/each}
</div>
<!-- Quick-create section -->
{#if showQuickCreate}
<div class="border-t pt-3 space-y-2">
<div class="flex gap-2">
<Input
type="text"
placeholder="New list name"
bind:value={newListName}
disabled={creatingList}
onkeydown={handleKeyDown}
class="flex-1"
/>
<Button size="sm" onclick={createListAndAdd} disabled={creatingList || !newListName.trim()}>
{#if creatingList}
<Loader2 class="h-4 w-4 animate-spin" />
{:else}
Add
{/if}
</Button>
</div>
{#if createError}
<p class="text-xs text-destructive">{createError}</p>
{/if}
</div>
{:else}
<div class="border-t pt-3">
<Button
size="sm"
variant="ghost"
class="w-full justify-start"
onclick={() => (showQuickCreate = true)}
>
<Plus class="h-4 w-4 mr-2" />
Create new list
</Button>
</div>
{/if}
{/if}
</div>
</PopoverContent>
</Popover>
</div>
{/if}

View File

@@ -19,11 +19,10 @@
description="Explore and read archived novels." description="Explore and read archived novels."
/> />
<NavigationCard <NavigationCard
href="/lists" href="/reading-lists"
icon={List} icon={List}
title="Reading Lists" title="Reading Lists"
description="Organize stories into custom collections." description="Organize stories into custom collections."
disabled
/> />
<NavigationCard <NavigationCard
href="/recommendations" href="/recommendations"

View File

@@ -2,6 +2,7 @@
import * as NavigationMenu from '$lib/components/ui/navigation-menu'; import * as NavigationMenu from '$lib/components/ui/navigation-menu';
import AuthenticationDisplay from './AuthenticationDisplay.svelte'; import AuthenticationDisplay from './AuthenticationDisplay.svelte';
import SearchBar from './SearchBar.svelte'; import SearchBar from './SearchBar.svelte';
import { isAuthenticated } from '$lib/auth/authStore';
let pathname = $state(typeof window !== 'undefined' ? window.location.pathname : '/'); let pathname = $state(typeof window !== 'undefined' ? window.location.pathname : '/');
@@ -24,6 +25,11 @@
<NavigationMenu.Item> <NavigationMenu.Item>
<NavigationMenu.Link href="/novels" active={isActive('/novels')}>Novels</NavigationMenu.Link> <NavigationMenu.Link href="/novels" active={isActive('/novels')}>Novels</NavigationMenu.Link>
</NavigationMenu.Item> </NavigationMenu.Item>
{#if $isAuthenticated}
<NavigationMenu.Item>
<NavigationMenu.Link href="/reading-lists" active={isActive('/reading-lists')}>Reading Lists</NavigationMenu.Link>
</NavigationMenu.Item>
{/if}
</NavigationMenu.List> </NavigationMenu.List>
</NavigationMenu.Root> </NavigationMenu.Root>
<div class="flex-1"></div> <div class="flex-1"></div>

View File

@@ -55,6 +55,7 @@
import { formatRelativeTime, formatAbsoluteTime } from '$lib/utils/time'; import { formatRelativeTime, formatAbsoluteTime } from '$lib/utils/time';
import { sanitizeHtml } from '$lib/utils/sanitize'; import { sanitizeHtml } from '$lib/utils/sanitize';
import ChapterBookmarkButton from './ChapterBookmarkButton.svelte'; import ChapterBookmarkButton from './ChapterBookmarkButton.svelte';
import AddToReadingListButton from './AddToReadingListButton.svelte';
// Direct imports for faster builds // Direct imports for faster builds
import ArrowLeft from '@lucide/svelte/icons/arrow-left'; import ArrowLeft from '@lucide/svelte/icons/arrow-left';
import ExternalLink from '@lucide/svelte/icons/external-link'; import ExternalLink from '@lucide/svelte/icons/external-link';
@@ -491,6 +492,7 @@
<Trash2 class="h-3 w-3" /> <Trash2 class="h-3 w-3" />
Delete Delete
</Button> </Button>
<AddToReadingListButton novelId={novel.id} />
{/if} {/if}
{#if refreshSuccess} {#if refreshSuccess}
<Badge variant="outline" class="bg-green-500/10 text-green-600 border-green-500/30"> <Badge variant="outline" class="bg-green-500/10 text-green-600 border-green-500/30">

View File

@@ -0,0 +1,393 @@
<script lang="ts">
import { SvelteMap } from 'svelte/reactivity';
import { onMount } from 'svelte';
import { client } from '$lib/graphql/client';
import {
GetReadingListDocument,
NovelsDocument,
RemoveFromReadingListDocument,
ReorderReadingListItemDocument,
type GetReadingListQuery,
type NovelsQuery
} from '$lib/graphql/__generated__/graphql';
import { isAuthenticated, login } from '$lib/auth/authStore';
import { Button } from '$lib/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '$lib/components/ui/card';
import ArrowLeft from '@lucide/svelte/icons/arrow-left';
import ArrowUp from '@lucide/svelte/icons/arrow-up';
import ArrowDown from '@lucide/svelte/icons/arrow-down';
import Trash2 from '@lucide/svelte/icons/trash-2';
import BookOpen from '@lucide/svelte/icons/book-open';
import LogIn from '@lucide/svelte/icons/log-in';
interface Props {
listId: string;
}
let { listId }: Props = $props();
type ReadingList = NonNullable<GetReadingListQuery['readingList']>;
type ReadingListItem = ReadingList['items'][number];
type NovelNode = NonNullable<NonNullable<NovelsQuery['novels']>['edges']>[number]['node'];
// State
let readingList: ReadingList | null = $state(null);
let novels = new SvelteMap<number, NovelNode>();
let fetching = $state(true);
let error: string | null = $state(null);
// Operation state
let reordering = $state(false);
let removing: number | null = $state(null);
let operationError: string | null = $state(null);
// Derived: sorted items by order
const sortedItems = $derived(
readingList?.items ? [...readingList.items].sort((a, b) => a.order - b.order) : []
);
async function fetchReadingList() {
fetching = true;
error = null;
try {
const id = parseInt(listId, 10);
if (isNaN(id)) {
error = 'Invalid reading list ID';
fetching = false;
return;
}
const result = await client.query(GetReadingListDocument, { id }).toPromise();
if (result.error) {
error = result.error.message;
return;
}
if (!result.data?.readingList) {
error = 'Reading list not found';
return;
}
readingList = result.data.readingList;
// Fetch novel details for all items
if (readingList.items.length > 0) {
await fetchNovels(readingList.items.map((item) => item.novelId));
}
} catch (e) {
error = e instanceof Error ? e.message : 'Unknown error';
} finally {
fetching = false;
}
}
async function fetchNovels(novelIds: number[]) {
if (novelIds.length === 0) return;
try {
const result = await client
.query(NovelsDocument, {
first: novelIds.length,
where: { id: { in: novelIds } }
})
.toPromise();
if (result.data?.novels?.edges) {
for (const edge of result.data.novels.edges) {
novels.set(edge.node.id, edge.node);
}
}
} catch {
// Non-critical: novels just won't show extra details
}
}
async function moveItem(item: ReadingListItem, direction: 'up' | 'down') {
if (!readingList || reordering) return;
const currentIndex = sortedItems.findIndex((i) => i.novelId === item.novelId);
const targetIndex = direction === 'up' ? currentIndex - 1 : currentIndex + 1;
if (targetIndex < 0 || targetIndex >= sortedItems.length) return;
const targetItem = sortedItems[targetIndex];
const newOrder = targetItem.order;
reordering = true;
operationError = null;
try {
const result = await client
.mutation(ReorderReadingListItemDocument, {
input: {
readingListId: readingList.id,
novelId: item.novelId,
newOrder
}
})
.toPromise();
if (result.error) {
operationError = result.error.message;
return;
}
if (result.data?.reorderReadingListItem?.errors?.length) {
operationError = result.data.reorderReadingListItem.errors[0]?.message ?? 'Failed to reorder';
return;
}
// Refresh the list to get updated order
await fetchReadingList();
} catch (e) {
operationError = e instanceof Error ? e.message : 'Failed to reorder';
} finally {
reordering = false;
}
}
async function removeItem(novelId: number) {
if (!readingList || removing !== null) return;
removing = novelId;
operationError = null;
try {
const result = await client
.mutation(RemoveFromReadingListDocument, {
input: {
listId: readingList.id,
novelId
}
})
.toPromise();
if (result.error) {
operationError = result.error.message;
return;
}
if (result.data?.removeFromReadingList?.errors?.length) {
operationError = result.data.removeFromReadingList.errors[0]?.message ?? 'Failed to remove';
return;
}
// Update local state
if (readingList) {
readingList = {
...readingList,
items: readingList.items.filter((item) => item.novelId !== novelId),
itemCount: readingList.itemCount - 1
};
}
} catch (e) {
operationError = e instanceof Error ? e.message : 'Failed to remove';
} finally {
removing = null;
}
}
onMount(() => {
if ($isAuthenticated) {
fetchReadingList();
} else {
fetching = false;
}
});
// Re-fetch when auth changes
$effect(() => {
if ($isAuthenticated) {
fetchReadingList();
} else {
readingList = null;
novels.clear();
fetching = false;
}
});
</script>
<div class="space-y-6">
<!-- Back Navigation -->
<Button variant="ghost" href="/reading-lists" class="gap-2 -ml-2">
<ArrowLeft class="h-4 w-4" />
Back to Reading Lists
</Button>
{#if !$isAuthenticated}
<!-- Auth gate - sign in prompt -->
<Card>
<CardContent class="py-12">
<div class="text-center space-y-4">
<BookOpen class="mx-auto h-12 w-12 text-muted-foreground" />
<div>
<h3 class="text-lg font-medium">Sign in to view Reading Lists</h3>
<p class="text-muted-foreground">
Sign in to view and manage your reading lists.
</p>
</div>
<Button onclick={login}>
<LogIn class="mr-2 h-4 w-4" />
Sign In
</Button>
</div>
</CardContent>
</Card>
{:else if fetching}
<!-- Loading state -->
<Card>
<CardContent>
<div class="flex items-center justify-center py-12">
<div
class="border-primary h-10 w-10 animate-spin rounded-full border-2 border-t-transparent"
aria-label="Loading reading list"
></div>
</div>
</CardContent>
</Card>
{:else if error}
<!-- Error state -->
<Card class="border-destructive/40 bg-destructive/5">
<CardContent class="py-8">
<div class="text-center">
<p class="text-destructive text-lg font-medium">
{error === 'Reading list not found' ? 'Reading List Not Found' : 'Error Loading Reading List'}
</p>
<p class="text-muted-foreground mt-2 text-sm">{error}</p>
<Button variant="outline" onclick={fetchReadingList} class="mt-4">
Try Again
</Button>
</div>
</CardContent>
</Card>
{:else if readingList}
<!-- Header -->
<Card>
<CardHeader>
<CardTitle class="text-2xl">{readingList.name}</CardTitle>
{#if readingList.description}
<CardDescription class="text-base">{readingList.description}</CardDescription>
{/if}
<p class="text-sm text-muted-foreground">
{readingList.itemCount} {readingList.itemCount === 1 ? 'novel' : 'novels'}
</p>
</CardHeader>
</Card>
<!-- Operation error -->
{#if operationError}
<Card class="border-destructive/40 bg-destructive/5">
<CardContent class="py-4">
<p class="text-destructive text-sm">{operationError}</p>
</CardContent>
</Card>
{/if}
<!-- Novels list -->
{#if sortedItems.length === 0}
<!-- Empty state -->
<Card>
<CardContent class="py-12">
<div class="text-center space-y-4">
<BookOpen class="mx-auto h-12 w-12 text-muted-foreground" />
<div>
<h3 class="text-lg font-medium">No novels in this list</h3>
<p class="text-muted-foreground">
Add novels to this reading list from a novel's detail page.
</p>
</div>
<Button href="/novels" variant="outline">
Browse Novels
</Button>
</div>
</CardContent>
</Card>
{:else}
<Card>
<CardContent class="py-4">
<div class="space-y-2">
{#each sortedItems as item, index (item.novelId)}
{@const novel = novels.get(item.novelId)}
<div
class="flex items-center gap-4 p-3 rounded-lg hover:bg-muted/50 transition-colors {removing === item.novelId || reordering ? 'opacity-50' : ''}"
>
<!-- Cover image -->
<a href={`/novels/${item.novelId}`} class="shrink-0">
{#if novel?.coverImage?.newPath}
<div class="w-16 h-20 overflow-hidden rounded-md bg-muted/50">
<img
src={novel.coverImage.newPath}
alt={novel?.name ?? 'Novel cover'}
class="h-full w-full object-cover"
/>
</div>
{:else}
<div class="w-16 h-20 rounded-md bg-muted/50 flex items-center justify-center">
<BookOpen class="h-6 w-6 text-muted-foreground/50" />
</div>
{/if}
</a>
<!-- Novel info -->
<div class="flex-1 min-w-0">
<a
href={`/novels/${item.novelId}`}
class="font-medium hover:text-primary transition-colors line-clamp-1"
>
{novel?.name ?? `Novel #${item.novelId}`}
</a>
{#if novel?.description}
<p class="text-sm text-muted-foreground line-clamp-2 mt-1">
{novel.description}
</p>
{/if}
</div>
<!-- Action buttons -->
<div class="flex items-center gap-1 shrink-0">
<!-- Move up -->
<Button
variant="ghost"
size="icon"
class="h-8 w-8"
disabled={index === 0 || reordering || removing !== null}
onclick={() => moveItem(item, 'up')}
>
<ArrowUp class="h-4 w-4" />
<span class="sr-only">Move up</span>
</Button>
<!-- Move down -->
<Button
variant="ghost"
size="icon"
class="h-8 w-8"
disabled={index === sortedItems.length - 1 || reordering || removing !== null}
onclick={() => moveItem(item, 'down')}
>
<ArrowDown class="h-4 w-4" />
<span class="sr-only">Move down</span>
</Button>
<!-- Remove -->
<Button
variant="ghost"
size="icon"
class="h-8 w-8 text-destructive hover:text-destructive"
disabled={reordering || removing !== null}
onclick={() => removeItem(item.novelId)}
>
<Trash2 class="h-4 w-4" />
<span class="sr-only">Remove from list</span>
</Button>
</div>
</div>
{/each}
</div>
</CardContent>
</Card>
{/if}
{/if}
</div>

View File

@@ -0,0 +1,432 @@
<script lang="ts">
import { onMount } from 'svelte';
import { Dialog as DialogPrimitive } from 'bits-ui';
import { client } from '$lib/graphql/client';
import {
GetReadingListsDocument,
CreateReadingListDocument,
UpdateReadingListDocument,
DeleteReadingListDocument,
type GetReadingListsQuery
} from '$lib/graphql/__generated__/graphql';
import { isAuthenticated, login } from '$lib/auth/authStore';
import { Button } from '$lib/components/ui/button';
import { Input } from '$lib/components/ui/input';
import { Textarea } from '$lib/components/ui/textarea';
import { Card, CardContent, CardHeader, CardTitle, CardDescription, CardFooter } from '$lib/components/ui/card';
import Plus from '@lucide/svelte/icons/plus';
import Pencil from '@lucide/svelte/icons/pencil';
import Trash2 from '@lucide/svelte/icons/trash-2';
import BookOpen from '@lucide/svelte/icons/book-open';
import X from '@lucide/svelte/icons/x';
import LogIn from '@lucide/svelte/icons/log-in';
type ReadingList = GetReadingListsQuery['readingLists'][0];
// State
let readingLists: ReadingList[] = $state([]);
let fetching = $state(true);
let error: string | null = $state(null);
// Dialog state
let dialogOpen = $state(false);
let dialogMode: 'create' | 'edit' = $state('create');
let editingList: ReadingList | null = $state(null);
// Form state
let formName = $state('');
let formDescription = $state('');
let formSubmitting = $state(false);
let formError: string | null = $state(null);
// Delete confirmation state
let deleteDialogOpen = $state(false);
let deletingList: ReadingList | null = $state(null);
let deleteSubmitting = $state(false);
let deleteError: string | null = $state(null);
// Reset form when dialog opens/closes
$effect(() => {
if (dialogOpen) {
if (dialogMode === 'edit' && editingList) {
formName = editingList.name;
formDescription = editingList.description ?? '';
} else {
formName = '';
formDescription = '';
}
formError = null;
}
});
async function fetchReadingLists(skipCache = false) {
fetching = true;
error = null;
try {
const result = await client.query(GetReadingListsDocument, {}, { requestPolicy: skipCache ? 'network-only' : 'cache-first' }).toPromise();
if (result.error) {
error = result.error.message;
return;
}
if (result.data) {
readingLists = result.data.readingLists;
}
} catch (e) {
error = e instanceof Error ? e.message : 'Unknown error';
} finally {
fetching = false;
}
}
function openCreateDialog() {
dialogMode = 'create';
editingList = null;
dialogOpen = true;
}
function openEditDialog(list: ReadingList) {
dialogMode = 'edit';
editingList = list;
dialogOpen = true;
}
function openDeleteDialog(list: ReadingList) {
deletingList = list;
deleteError = null;
deleteDialogOpen = true;
}
async function handleSubmit(e: Event) {
e.preventDefault();
if (!formName.trim()) {
formError = 'Name is required';
return;
}
formSubmitting = true;
formError = null;
try {
if (dialogMode === 'create') {
const result = await client
.mutation(CreateReadingListDocument, {
input: {
name: formName.trim(),
description: formDescription.trim() || null
}
})
.toPromise();
if (result.error) {
formError = result.error.message;
return;
}
if (result.data?.createReadingList?.errors?.length) {
formError = result.data.createReadingList.errors[0]?.message ?? 'Failed to create reading list';
return;
}
if (result.data?.createReadingList?.readingListPayload?.readingList) {
dialogOpen = false;
await fetchReadingLists(true);
}
} else if (dialogMode === 'edit' && editingList) {
const result = await client
.mutation(UpdateReadingListDocument, {
input: {
id: editingList.id,
name: formName.trim(),
description: formDescription.trim() || null
}
})
.toPromise();
if (result.error) {
formError = result.error.message;
return;
}
if (result.data?.updateReadingList?.errors?.length) {
formError = result.data.updateReadingList.errors[0]?.message ?? 'Failed to update reading list';
return;
}
if (result.data?.updateReadingList?.readingListPayload?.readingList) {
dialogOpen = false;
await fetchReadingLists(true);
}
}
} catch (e) {
formError = e instanceof Error ? e.message : 'An error occurred';
} finally {
formSubmitting = false;
}
}
async function handleDelete() {
if (!deletingList) return;
deleteSubmitting = true;
deleteError = null;
try {
const result = await client
.mutation(DeleteReadingListDocument, {
input: { id: deletingList.id }
})
.toPromise();
if (result.error) {
deleteError = result.error.message;
return;
}
if (result.data?.deleteReadingList?.errors?.length) {
deleteError = result.data.deleteReadingList.errors[0]?.message ?? 'Failed to delete reading list';
return;
}
if (result.data?.deleteReadingList?.success) {
deleteDialogOpen = false;
deletingList = null;
await fetchReadingLists(true);
}
} catch (e) {
deleteError = e instanceof Error ? e.message : 'An error occurred';
} finally {
deleteSubmitting = false;
}
}
onMount(() => {
if ($isAuthenticated) {
fetchReadingLists();
} else {
fetching = false;
}
});
// Re-fetch when auth changes
$effect(() => {
if ($isAuthenticated) {
fetchReadingLists();
} else {
readingLists = [];
fetching = false;
}
});
</script>
<div class="space-y-6">
<div class="flex items-center justify-between">
<div>
<h1 class="text-3xl font-bold">Reading Lists</h1>
<p class="text-muted-foreground">Organize your novels into collections</p>
</div>
{#if $isAuthenticated}
<Button onclick={openCreateDialog}>
<Plus class="mr-2 h-4 w-4" />
New List
</Button>
{/if}
</div>
{#if !$isAuthenticated}
<!-- Auth gate - sign in prompt -->
<Card>
<CardContent class="py-12">
<div class="text-center space-y-4">
<BookOpen class="mx-auto h-12 w-12 text-muted-foreground" />
<div>
<h3 class="text-lg font-medium">Sign in to use Reading Lists</h3>
<p class="text-muted-foreground">
Create and manage your personal reading lists to organize novels.
</p>
</div>
<Button onclick={login}>
<LogIn class="mr-2 h-4 w-4" />
Sign In
</Button>
</div>
</CardContent>
</Card>
{:else if fetching}
<!-- Loading state -->
<div class="flex items-center justify-center py-12">
<div class="text-muted-foreground">Loading your reading lists...</div>
</div>
{:else if error}
<!-- Error state -->
<Card>
<CardContent class="py-6">
<p class="text-destructive">{error}</p>
<Button class="mt-4" onclick={fetchReadingLists}>Try Again</Button>
</CardContent>
</Card>
{:else if readingLists.length === 0}
<!-- Empty state -->
<Card>
<CardContent class="py-12">
<div class="text-center space-y-4">
<BookOpen class="mx-auto h-12 w-12 text-muted-foreground" />
<div>
<h3 class="text-lg font-medium">No reading lists yet</h3>
<p class="text-muted-foreground">
Create your first reading list to start organizing your novels.
</p>
</div>
<Button onclick={openCreateDialog}>
<Plus class="mr-2 h-4 w-4" />
Create Your First List
</Button>
</div>
</CardContent>
</Card>
{:else}
<!-- Reading lists grid -->
<div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
{#each readingLists as list (list.id)}
<a href={`/reading-lists/${list.id}`} class="block group">
<Card class="h-full transition-colors hover:border-primary/50">
<CardHeader class="pb-2">
<div class="flex items-start justify-between">
<CardTitle class="line-clamp-1">{list.name}</CardTitle>
<div class="flex gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
<Button
variant="ghost"
size="icon"
class="h-8 w-8"
onclick={(e: MouseEvent) => {
e.preventDefault();
e.stopPropagation();
openEditDialog(list);
}}
>
<Pencil class="h-4 w-4" />
<span class="sr-only">Edit</span>
</Button>
<Button
variant="ghost"
size="icon"
class="h-8 w-8 text-destructive hover:text-destructive"
onclick={(e: MouseEvent) => {
e.preventDefault();
e.stopPropagation();
openDeleteDialog(list);
}}
>
<Trash2 class="h-4 w-4" />
<span class="sr-only">Delete</span>
</Button>
</div>
</div>
{#if list.description}
<CardDescription class="line-clamp-2">{list.description}</CardDescription>
{/if}
</CardHeader>
<CardFooter class="pt-2">
<span class="text-sm text-muted-foreground">
{list.itemCount} {list.itemCount === 1 ? 'novel' : 'novels'}
</span>
</CardFooter>
</Card>
</a>
{/each}
</div>
{/if}
</div>
<!-- Create/Edit Dialog -->
<DialogPrimitive.Root bind:open={dialogOpen}>
<DialogPrimitive.Portal>
<DialogPrimitive.Overlay class="fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0" />
<DialogPrimitive.Content class="fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg">
<div class="flex flex-col space-y-1.5 text-center sm:text-left">
<DialogPrimitive.Title class="text-lg font-semibold leading-none tracking-tight">
{dialogMode === 'create' ? 'Create Reading List' : 'Edit Reading List'}
</DialogPrimitive.Title>
<DialogPrimitive.Description class="text-sm text-muted-foreground">
{dialogMode === 'create' ? 'Create a new reading list to organize your novels.' : 'Update your reading list details.'}
</DialogPrimitive.Description>
</div>
<form onsubmit={handleSubmit} class="space-y-4">
<div class="space-y-2">
<label for="list-name" class="text-sm font-medium">Name</label>
<Input
id="list-name"
type="text"
placeholder="My Reading List"
bind:value={formName}
disabled={formSubmitting}
/>
</div>
<div class="space-y-2">
<label for="list-description" class="text-sm font-medium">Description (optional)</label>
<Textarea
id="list-description"
placeholder="A collection of..."
bind:value={formDescription}
disabled={formSubmitting}
class="min-h-[80px] resize-none"
/>
</div>
{#if formError}
<p class="text-sm text-destructive">{formError}</p>
{/if}
<div class="flex justify-end gap-2">
<Button variant="outline" type="button" disabled={formSubmitting} onclick={() => dialogOpen = false}>
Cancel
</Button>
<Button type="submit" disabled={formSubmitting || !formName.trim()}>
{#if formSubmitting}
{dialogMode === 'create' ? 'Creating...' : 'Saving...'}
{:else}
{dialogMode === 'create' ? 'Create' : 'Save'}
{/if}
</Button>
</div>
</form>
<DialogPrimitive.Close class="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
<X class="h-4 w-4" />
<span class="sr-only">Close</span>
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</DialogPrimitive.Portal>
</DialogPrimitive.Root>
<!-- Delete Confirmation Dialog -->
<DialogPrimitive.Root bind:open={deleteDialogOpen}>
<DialogPrimitive.Portal>
<DialogPrimitive.Overlay class="fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0" />
<DialogPrimitive.Content class="fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg">
<div class="flex flex-col space-y-1.5 text-center sm:text-left">
<DialogPrimitive.Title class="text-lg font-semibold leading-none tracking-tight">
Delete Reading List
</DialogPrimitive.Title>
<DialogPrimitive.Description class="text-sm text-muted-foreground">
Are you sure you want to delete "{deletingList?.name}"? This action cannot be undone.
</DialogPrimitive.Description>
</div>
{#if deleteError}
<p class="text-sm text-destructive">{deleteError}</p>
{/if}
<div class="flex justify-end gap-2">
<Button variant="outline" type="button" disabled={deleteSubmitting} onclick={() => deleteDialogOpen = false}>
Cancel
</Button>
<Button variant="destructive" onclick={handleDelete} disabled={deleteSubmitting}>
{deleteSubmitting ? 'Deleting...' : 'Delete'}
</Button>
</div>
<DialogPrimitive.Close class="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
<X class="h-4 w-4" />
<span class="sr-only">Close</span>
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</DialogPrimitive.Portal>
</DialogPrimitive.Root>

View File

@@ -18,6 +18,18 @@ export type Scalars = {
UnsignedInt: { input: any; output: any; } UnsignedInt: { input: any; output: any; }
}; };
export type AddToReadingListError = InvalidOperationError;
export type AddToReadingListInput = {
novelId: Scalars['UnsignedInt']['input'];
readingListId: Scalars['Int']['input'];
};
export type AddToReadingListPayload = {
errors: Maybe<Array<AddToReadingListError>>;
readingListPayload: Maybe<ReadingListPayload>;
};
/** Defines when a policy shall be executed. */ /** Defines when a policy shall be executed. */
export const ApplyPolicy = { export const ApplyPolicy = {
/** After the resolver was executed. */ /** After the resolver was executed. */
@@ -96,6 +108,18 @@ export type ChapterReaderDto = {
volumeOrder: Scalars['Int']['output']; volumeOrder: Scalars['Int']['output'];
}; };
export type CreateReadingListError = InvalidOperationError;
export type CreateReadingListInput = {
description?: InputMaybe<Scalars['String']['input']>;
name: Scalars['String']['input'];
};
export type CreateReadingListPayload = {
errors: Maybe<Array<CreateReadingListError>>;
readingListPayload: Maybe<ReadingListPayload>;
};
export type DeleteJobError = KeyNotFoundError; export type DeleteJobError = KeyNotFoundError;
export type DeleteJobInput = { export type DeleteJobInput = {
@@ -118,6 +142,17 @@ export type DeleteNovelPayload = {
errors: Maybe<Array<DeleteNovelError>>; errors: Maybe<Array<DeleteNovelError>>;
}; };
export type DeleteReadingListError = InvalidOperationError;
export type DeleteReadingListInput = {
id: Scalars['Int']['input'];
};
export type DeleteReadingListPayload = {
errors: Maybe<Array<DeleteReadingListError>>;
success: Maybe<Scalars['Boolean']['output']>;
};
export type DuplicateNameError = Error & { export type DuplicateNameError = Error & {
message: Scalars['String']['output']; message: Scalars['String']['output'];
}; };
@@ -274,19 +309,35 @@ export type ListFilterInputTypeOfVolumeDtoFilterInput = {
}; };
export type Mutation = { export type Mutation = {
addToReadingList: AddToReadingListPayload;
createReadingList: CreateReadingListPayload;
deleteJob: DeleteJobPayload; deleteJob: DeleteJobPayload;
deleteNovel: DeleteNovelPayload; deleteNovel: DeleteNovelPayload;
deleteReadingList: DeleteReadingListPayload;
fetchChapterContents: FetchChapterContentsPayload; fetchChapterContents: FetchChapterContentsPayload;
importNovel: ImportNovelPayload; importNovel: ImportNovelPayload;
inviteUser: InviteUserPayload; inviteUser: InviteUserPayload;
removeBookmark: RemoveBookmarkPayload; removeBookmark: RemoveBookmarkPayload;
removeFromReadingList: RemoveFromReadingListPayload;
reorderReadingListItem: ReorderReadingListItemPayload;
runJob: RunJobPayload; runJob: RunJobPayload;
scheduleEventJob: ScheduleEventJobPayload; scheduleEventJob: ScheduleEventJobPayload;
translateText: TranslateTextPayload; translateText: TranslateTextPayload;
updateReadingList: UpdateReadingListPayload;
upsertBookmark: UpsertBookmarkPayload; upsertBookmark: UpsertBookmarkPayload;
}; };
export type MutationAddToReadingListArgs = {
input: AddToReadingListInput;
};
export type MutationCreateReadingListArgs = {
input: CreateReadingListInput;
};
export type MutationDeleteJobArgs = { export type MutationDeleteJobArgs = {
input: DeleteJobInput; input: DeleteJobInput;
}; };
@@ -297,6 +348,11 @@ export type MutationDeleteNovelArgs = {
}; };
export type MutationDeleteReadingListArgs = {
input: DeleteReadingListInput;
};
export type MutationFetchChapterContentsArgs = { export type MutationFetchChapterContentsArgs = {
input: FetchChapterContentsInput; input: FetchChapterContentsInput;
}; };
@@ -317,6 +373,16 @@ export type MutationRemoveBookmarkArgs = {
}; };
export type MutationRemoveFromReadingListArgs = {
input: RemoveFromReadingListInput;
};
export type MutationReorderReadingListItemArgs = {
input: ReorderReadingListItemInput;
};
export type MutationRunJobArgs = { export type MutationRunJobArgs = {
input: RunJobInput; input: RunJobInput;
}; };
@@ -332,6 +398,11 @@ export type MutationTranslateTextArgs = {
}; };
export type MutationUpdateReadingListArgs = {
input: UpdateReadingListInput;
};
export type MutationUpsertBookmarkArgs = { export type MutationUpsertBookmarkArgs = {
input: UpsertBookmarkInput; input: UpsertBookmarkInput;
}; };
@@ -501,6 +572,8 @@ export type Query = {
currentUser: Maybe<UserDto>; currentUser: Maybe<UserDto>;
jobs: Array<SchedulerJob>; jobs: Array<SchedulerJob>;
novels: Maybe<NovelsConnection>; novels: Maybe<NovelsConnection>;
readingList: Maybe<ReadingListDto>;
readingLists: Array<ReadingListDto>;
translationEngines: Array<TranslationEngineDescriptor>; translationEngines: Array<TranslationEngineDescriptor>;
translationRequests: Maybe<TranslationRequestsConnection>; translationRequests: Maybe<TranslationRequestsConnection>;
}; };
@@ -530,6 +603,11 @@ export type QueryNovelsArgs = {
}; };
export type QueryReadingListArgs = {
id: Scalars['Int']['input'];
};
export type QueryTranslationEnginesArgs = { export type QueryTranslationEnginesArgs = {
order?: InputMaybe<Array<TranslationEngineDescriptorSortInput>>; order?: InputMaybe<Array<TranslationEngineDescriptorSortInput>>;
where?: InputMaybe<TranslationEngineDescriptorFilterInput>; where?: InputMaybe<TranslationEngineDescriptorFilterInput>;
@@ -545,6 +623,26 @@ export type QueryTranslationRequestsArgs = {
where?: InputMaybe<TranslationRequestDtoFilterInput>; where?: InputMaybe<TranslationRequestDtoFilterInput>;
}; };
export type ReadingListDto = {
createdTime: Scalars['Instant']['output'];
description: Maybe<Scalars['String']['output']>;
id: Scalars['Int']['output'];
itemCount: Scalars['Int']['output'];
items: Array<ReadingListItemDto>;
name: Scalars['String']['output'];
};
export type ReadingListItemDto = {
addedTime: Scalars['Instant']['output'];
novelId: Scalars['UnsignedInt']['output'];
order: Scalars['Int']['output'];
};
export type ReadingListPayload = {
readingList: Maybe<ReadingListDto>;
success: Scalars['Boolean']['output'];
};
export type RemoveBookmarkError = InvalidOperationError; export type RemoveBookmarkError = InvalidOperationError;
export type RemoveBookmarkInput = { export type RemoveBookmarkInput = {
@@ -556,6 +654,31 @@ export type RemoveBookmarkPayload = {
errors: Maybe<Array<RemoveBookmarkError>>; errors: Maybe<Array<RemoveBookmarkError>>;
}; };
export type RemoveFromReadingListError = InvalidOperationError;
export type RemoveFromReadingListInput = {
listId: Scalars['Int']['input'];
novelId: Scalars['UnsignedInt']['input'];
};
export type RemoveFromReadingListPayload = {
errors: Maybe<Array<RemoveFromReadingListError>>;
readingListPayload: Maybe<ReadingListPayload>;
};
export type ReorderReadingListItemError = InvalidOperationError;
export type ReorderReadingListItemInput = {
newOrder: Scalars['Int']['input'];
novelId: Scalars['UnsignedInt']['input'];
readingListId: Scalars['Int']['input'];
};
export type ReorderReadingListItemPayload = {
errors: Maybe<Array<ReorderReadingListItemError>>;
readingListPayload: Maybe<ReadingListPayload>;
};
export type RunJobError = JobPersistenceError; export type RunJobError = JobPersistenceError;
export type RunJobInput = { export type RunJobInput = {
@@ -781,6 +904,19 @@ export type UnsignedIntOperationFilterInputType = {
nlte?: InputMaybe<Scalars['UnsignedInt']['input']>; nlte?: InputMaybe<Scalars['UnsignedInt']['input']>;
}; };
export type UpdateReadingListError = InvalidOperationError;
export type UpdateReadingListInput = {
description?: InputMaybe<Scalars['String']['input']>;
id: Scalars['Int']['input'];
name: Scalars['String']['input'];
};
export type UpdateReadingListPayload = {
errors: Maybe<Array<UpdateReadingListError>>;
readingListPayload: Maybe<ReadingListPayload>;
};
export type UpsertBookmarkError = InvalidOperationError; export type UpsertBookmarkError = InvalidOperationError;
export type UpsertBookmarkInput = { export type UpsertBookmarkInput = {
@@ -841,6 +977,20 @@ export type VolumeDtoFilterInput = {
order?: InputMaybe<IntOperationFilterInput>; order?: InputMaybe<IntOperationFilterInput>;
}; };
export type AddToReadingListMutationVariables = Exact<{
input: AddToReadingListInput;
}>;
export type AddToReadingListMutation = { addToReadingList: { readingListPayload: { success: boolean, readingList: { id: number, name: string, itemCount: number } | null } | null, errors: Array<{ message: string }> | null } };
export type CreateReadingListMutationVariables = Exact<{
input: CreateReadingListInput;
}>;
export type CreateReadingListMutation = { createReadingList: { readingListPayload: { success: boolean, readingList: { id: number, name: string, description: string | null, itemCount: number, createdTime: any } | null } | null, errors: Array<{ message: string }> | null } };
export type DeleteNovelMutationVariables = Exact<{ export type DeleteNovelMutationVariables = Exact<{
input: DeleteNovelInput; input: DeleteNovelInput;
}>; }>;
@@ -848,6 +998,13 @@ export type DeleteNovelMutationVariables = Exact<{
export type DeleteNovelMutation = { deleteNovel: { boolean: boolean | null, errors: Array<{ message: string }> | null } }; export type DeleteNovelMutation = { deleteNovel: { boolean: boolean | null, errors: Array<{ message: string }> | null } };
export type DeleteReadingListMutationVariables = Exact<{
input: DeleteReadingListInput;
}>;
export type DeleteReadingListMutation = { deleteReadingList: { success: boolean | null, errors: Array<{ message: string }> | null } };
export type ImportNovelMutationVariables = Exact<{ export type ImportNovelMutationVariables = Exact<{
input: ImportNovelInput; input: ImportNovelInput;
}>; }>;
@@ -869,6 +1026,27 @@ export type RemoveBookmarkMutationVariables = Exact<{
export type RemoveBookmarkMutation = { removeBookmark: { bookmarkPayload: { success: boolean } | null, errors: Array<{ message: string }> | null } }; export type RemoveBookmarkMutation = { removeBookmark: { bookmarkPayload: { success: boolean } | null, errors: Array<{ message: string }> | null } };
export type RemoveFromReadingListMutationVariables = Exact<{
input: RemoveFromReadingListInput;
}>;
export type RemoveFromReadingListMutation = { removeFromReadingList: { readingListPayload: { success: boolean, readingList: { id: number, name: string, itemCount: number } | null } | null, errors: Array<{ message: string }> | null } };
export type ReorderReadingListItemMutationVariables = Exact<{
input: ReorderReadingListItemInput;
}>;
export type ReorderReadingListItemMutation = { reorderReadingListItem: { readingListPayload: { success: boolean } | null, errors: Array<{ message: string }> | null } };
export type UpdateReadingListMutationVariables = Exact<{
input: UpdateReadingListInput;
}>;
export type UpdateReadingListMutation = { updateReadingList: { readingListPayload: { success: boolean, readingList: { id: number, name: string, description: string | null, itemCount: number, createdTime: any } | null } | null, errors: Array<{ message: string }> | null } };
export type UpsertBookmarkMutationVariables = Exact<{ export type UpsertBookmarkMutationVariables = Exact<{
input: UpsertBookmarkInput; input: UpsertBookmarkInput;
}>; }>;
@@ -909,19 +1087,45 @@ export type NovelsQueryVariables = Exact<{
export type NovelsQuery = { novels: { edges: Array<{ cursor: string, node: { id: any, url: string, name: string, description: string, rawStatus: NovelStatus, lastUpdatedTime: any, coverImage: { newPath: string | null } | null, volumes: Array<{ id: any, order: number, name: string, chapters: Array<{ order: any, name: string }> }>, tags: Array<{ key: string, displayName: string, tagType: TagType }> } }> | null, pageInfo: { hasNextPage: boolean, endCursor: string | null } } | null }; export type NovelsQuery = { novels: { edges: Array<{ cursor: string, node: { id: any, url: string, name: string, description: string, rawStatus: NovelStatus, lastUpdatedTime: any, coverImage: { newPath: string | null } | null, volumes: Array<{ id: any, order: number, name: string, chapters: Array<{ order: any, name: string }> }>, tags: Array<{ key: string, displayName: string, tagType: TagType }> } }> | null, pageInfo: { hasNextPage: boolean, endCursor: string | null } } | null };
export type GetReadingListQueryVariables = Exact<{
id: Scalars['Int']['input'];
}>;
export type GetReadingListQuery = { readingList: { id: number, name: string, description: string | null, itemCount: number, createdTime: any, items: Array<{ novelId: any, order: number, addedTime: any }> } | null };
export type GetReadingListsQueryVariables = Exact<{ [key: string]: never; }>;
export type GetReadingListsQuery = { readingLists: Array<{ id: number, name: string, description: string | null, itemCount: number, createdTime: any }> };
export type GetReadingListsWithItemsQueryVariables = Exact<{ [key: string]: never; }>;
export type GetReadingListsWithItemsQuery = { readingLists: Array<{ id: number, name: string, description: string | null, itemCount: number, createdTime: any, items: Array<{ novelId: any, order: number, addedTime: any }> }> };
export type GetSettingsPageDataQueryVariables = Exact<{ [key: string]: never; }>; export type GetSettingsPageDataQueryVariables = Exact<{ [key: string]: never; }>;
export type GetSettingsPageDataQuery = { currentUser: { id: any, username: string, availableInvites: number, invitedUsers: Array<{ username: string, email: string }> | null } | null }; export type GetSettingsPageDataQuery = { currentUser: { id: any, username: string, availableInvites: number, invitedUsers: Array<{ username: string, email: string }> | null } | null };
export const AddToReadingListDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"AddToReadingList"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"AddToReadingListInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"addToReadingList"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"readingListPayload"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"success"}},{"kind":"Field","name":{"kind":"Name","value":"readingList"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"itemCount"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"errors"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Error"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"message"}}]}}]}}]}}]}}]} as unknown as DocumentNode<AddToReadingListMutation, AddToReadingListMutationVariables>;
export const CreateReadingListDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CreateReadingList"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"CreateReadingListInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"createReadingList"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"readingListPayload"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"success"}},{"kind":"Field","name":{"kind":"Name","value":"readingList"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"itemCount"}},{"kind":"Field","name":{"kind":"Name","value":"createdTime"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"errors"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Error"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"message"}}]}}]}}]}}]}}]} as unknown as DocumentNode<CreateReadingListMutation, CreateReadingListMutationVariables>;
export const DeleteNovelDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"DeleteNovel"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"DeleteNovelInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"deleteNovel"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"boolean"}},{"kind":"Field","name":{"kind":"Name","value":"errors"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Error"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"message"}}]}}]}}]}}]}}]} as unknown as DocumentNode<DeleteNovelMutation, DeleteNovelMutationVariables>; export const DeleteNovelDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"DeleteNovel"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"DeleteNovelInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"deleteNovel"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"boolean"}},{"kind":"Field","name":{"kind":"Name","value":"errors"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Error"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"message"}}]}}]}}]}}]}}]} as unknown as DocumentNode<DeleteNovelMutation, DeleteNovelMutationVariables>;
export const DeleteReadingListDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"DeleteReadingList"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"DeleteReadingListInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"deleteReadingList"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"success"}},{"kind":"Field","name":{"kind":"Name","value":"errors"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Error"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"message"}}]}}]}}]}}]}}]} as unknown as DocumentNode<DeleteReadingListMutation, DeleteReadingListMutationVariables>;
export const ImportNovelDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"ImportNovel"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ImportNovelInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"importNovel"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"novelUpdateRequestedEvent"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"novelUrl"}}]}}]}}]}}]} as unknown as DocumentNode<ImportNovelMutation, ImportNovelMutationVariables>; export const ImportNovelDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"ImportNovel"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ImportNovelInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"importNovel"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"novelUpdateRequestedEvent"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"novelUrl"}}]}}]}}]}}]} as unknown as DocumentNode<ImportNovelMutation, ImportNovelMutationVariables>;
export const InviteUserDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"InviteUser"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"InviteUserInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"inviteUser"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"userDto"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"username"}},{"kind":"Field","name":{"kind":"Name","value":"email"}}]}},{"kind":"Field","name":{"kind":"Name","value":"errors"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"InvalidOperationError"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"message"}}]}}]}}]}}]}}]} as unknown as DocumentNode<InviteUserMutation, InviteUserMutationVariables>; export const InviteUserDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"InviteUser"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"InviteUserInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"inviteUser"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"userDto"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"username"}},{"kind":"Field","name":{"kind":"Name","value":"email"}}]}},{"kind":"Field","name":{"kind":"Name","value":"errors"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"InvalidOperationError"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"message"}}]}}]}}]}}]}}]} as unknown as DocumentNode<InviteUserMutation, InviteUserMutationVariables>;
export const RemoveBookmarkDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"RemoveBookmark"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"RemoveBookmarkInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"removeBookmark"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"bookmarkPayload"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"success"}}]}},{"kind":"Field","name":{"kind":"Name","value":"errors"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Error"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"message"}}]}}]}}]}}]}}]} as unknown as DocumentNode<RemoveBookmarkMutation, RemoveBookmarkMutationVariables>; export const RemoveBookmarkDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"RemoveBookmark"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"RemoveBookmarkInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"removeBookmark"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"bookmarkPayload"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"success"}}]}},{"kind":"Field","name":{"kind":"Name","value":"errors"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Error"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"message"}}]}}]}}]}}]}}]} as unknown as DocumentNode<RemoveBookmarkMutation, RemoveBookmarkMutationVariables>;
export const RemoveFromReadingListDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"RemoveFromReadingList"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"RemoveFromReadingListInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"removeFromReadingList"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"readingListPayload"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"success"}},{"kind":"Field","name":{"kind":"Name","value":"readingList"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"itemCount"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"errors"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Error"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"message"}}]}}]}}]}}]}}]} as unknown as DocumentNode<RemoveFromReadingListMutation, RemoveFromReadingListMutationVariables>;
export const ReorderReadingListItemDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"ReorderReadingListItem"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ReorderReadingListItemInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"reorderReadingListItem"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"readingListPayload"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"success"}}]}},{"kind":"Field","name":{"kind":"Name","value":"errors"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Error"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"message"}}]}}]}}]}}]}}]} as unknown as DocumentNode<ReorderReadingListItemMutation, ReorderReadingListItemMutationVariables>;
export const UpdateReadingListDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpdateReadingList"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UpdateReadingListInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"updateReadingList"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"readingListPayload"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"success"}},{"kind":"Field","name":{"kind":"Name","value":"readingList"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"itemCount"}},{"kind":"Field","name":{"kind":"Name","value":"createdTime"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"errors"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Error"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"message"}}]}}]}}]}}]}}]} as unknown as DocumentNode<UpdateReadingListMutation, UpdateReadingListMutationVariables>;
export const UpsertBookmarkDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpsertBookmark"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UpsertBookmarkInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"upsertBookmark"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"bookmarkPayload"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"success"}},{"kind":"Field","name":{"kind":"Name","value":"bookmark"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"chapterId"}},{"kind":"Field","name":{"kind":"Name","value":"novelId"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"createdTime"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"errors"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Error"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"message"}}]}}]}}]}}]}}]} as unknown as DocumentNode<UpsertBookmarkMutation, UpsertBookmarkMutationVariables>; export const UpsertBookmarkDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpsertBookmark"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UpsertBookmarkInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"upsertBookmark"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"bookmarkPayload"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"success"}},{"kind":"Field","name":{"kind":"Name","value":"bookmark"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"chapterId"}},{"kind":"Field","name":{"kind":"Name","value":"novelId"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"createdTime"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"errors"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Error"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"message"}}]}}]}}]}}]}}]} as unknown as DocumentNode<UpsertBookmarkMutation, UpsertBookmarkMutationVariables>;
export const GetBookmarksDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetBookmarks"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"novelId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UnsignedInt"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"bookmarks"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"novelId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"novelId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"chapterId"}},{"kind":"Field","name":{"kind":"Name","value":"novelId"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"createdTime"}}]}}]}}]} as unknown as DocumentNode<GetBookmarksQuery, GetBookmarksQueryVariables>; export const GetBookmarksDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetBookmarks"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"novelId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UnsignedInt"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"bookmarks"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"novelId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"novelId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"chapterId"}},{"kind":"Field","name":{"kind":"Name","value":"novelId"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"createdTime"}}]}}]}}]} as unknown as DocumentNode<GetBookmarksQuery, GetBookmarksQueryVariables>;
export const GetChapterDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetChapter"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"novelId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UnsignedInt"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"volumeOrder"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UnsignedInt"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"chapterOrder"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UnsignedInt"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"chapter"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"novelId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"novelId"}}},{"kind":"Argument","name":{"kind":"Name","value":"volumeOrder"},"value":{"kind":"Variable","name":{"kind":"Name","value":"volumeOrder"}}},{"kind":"Argument","name":{"kind":"Name","value":"chapterOrder"},"value":{"kind":"Variable","name":{"kind":"Name","value":"chapterOrder"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"order"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"body"}},{"kind":"Field","name":{"kind":"Name","value":"url"}},{"kind":"Field","name":{"kind":"Name","value":"revision"}},{"kind":"Field","name":{"kind":"Name","value":"createdTime"}},{"kind":"Field","name":{"kind":"Name","value":"lastUpdatedTime"}},{"kind":"Field","name":{"kind":"Name","value":"images"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"newPath"}}]}},{"kind":"Field","name":{"kind":"Name","value":"novelId"}},{"kind":"Field","name":{"kind":"Name","value":"novelName"}},{"kind":"Field","name":{"kind":"Name","value":"volumeId"}},{"kind":"Field","name":{"kind":"Name","value":"volumeName"}},{"kind":"Field","name":{"kind":"Name","value":"volumeOrder"}},{"kind":"Field","name":{"kind":"Name","value":"totalChaptersInVolume"}},{"kind":"Field","name":{"kind":"Name","value":"prevChapterVolumeOrder"}},{"kind":"Field","name":{"kind":"Name","value":"prevChapterOrder"}},{"kind":"Field","name":{"kind":"Name","value":"nextChapterVolumeOrder"}},{"kind":"Field","name":{"kind":"Name","value":"nextChapterOrder"}}]}}]}}]} as unknown as DocumentNode<GetChapterQuery, GetChapterQueryVariables>; export const GetChapterDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetChapter"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"novelId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UnsignedInt"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"volumeOrder"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UnsignedInt"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"chapterOrder"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UnsignedInt"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"chapter"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"novelId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"novelId"}}},{"kind":"Argument","name":{"kind":"Name","value":"volumeOrder"},"value":{"kind":"Variable","name":{"kind":"Name","value":"volumeOrder"}}},{"kind":"Argument","name":{"kind":"Name","value":"chapterOrder"},"value":{"kind":"Variable","name":{"kind":"Name","value":"chapterOrder"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"order"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"body"}},{"kind":"Field","name":{"kind":"Name","value":"url"}},{"kind":"Field","name":{"kind":"Name","value":"revision"}},{"kind":"Field","name":{"kind":"Name","value":"createdTime"}},{"kind":"Field","name":{"kind":"Name","value":"lastUpdatedTime"}},{"kind":"Field","name":{"kind":"Name","value":"images"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"newPath"}}]}},{"kind":"Field","name":{"kind":"Name","value":"novelId"}},{"kind":"Field","name":{"kind":"Name","value":"novelName"}},{"kind":"Field","name":{"kind":"Name","value":"volumeId"}},{"kind":"Field","name":{"kind":"Name","value":"volumeName"}},{"kind":"Field","name":{"kind":"Name","value":"volumeOrder"}},{"kind":"Field","name":{"kind":"Name","value":"totalChaptersInVolume"}},{"kind":"Field","name":{"kind":"Name","value":"prevChapterVolumeOrder"}},{"kind":"Field","name":{"kind":"Name","value":"prevChapterOrder"}},{"kind":"Field","name":{"kind":"Name","value":"nextChapterVolumeOrder"}},{"kind":"Field","name":{"kind":"Name","value":"nextChapterOrder"}}]}}]}}]} as unknown as DocumentNode<GetChapterQuery, GetChapterQueryVariables>;
export const NovelDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"Novel"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UnsignedInt"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"novels"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"where"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"id"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"eq"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}]}}]}},{"kind":"Argument","name":{"kind":"Name","value":"first"},"value":{"kind":"IntValue","value":"1"}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"nodes"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"url"}},{"kind":"Field","name":{"kind":"Name","value":"rawLanguage"}},{"kind":"Field","name":{"kind":"Name","value":"rawStatus"}},{"kind":"Field","name":{"kind":"Name","value":"statusOverride"}},{"kind":"Field","name":{"kind":"Name","value":"externalId"}},{"kind":"Field","name":{"kind":"Name","value":"createdTime"}},{"kind":"Field","name":{"kind":"Name","value":"lastUpdatedTime"}},{"kind":"Field","name":{"kind":"Name","value":"author"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"externalUrl"}}]}},{"kind":"Field","name":{"kind":"Name","value":"source"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"key"}},{"kind":"Field","name":{"kind":"Name","value":"url"}}]}},{"kind":"Field","name":{"kind":"Name","value":"coverImage"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"newPath"}}]}},{"kind":"Field","name":{"kind":"Name","value":"tags"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"key"}},{"kind":"Field","name":{"kind":"Name","value":"displayName"}},{"kind":"Field","name":{"kind":"Name","value":"tagType"}}]}},{"kind":"Field","name":{"kind":"Name","value":"volumes"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"order"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"createdTime"}},{"kind":"Field","name":{"kind":"Name","value":"lastUpdatedTime"}},{"kind":"Field","name":{"kind":"Name","value":"chapters"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"order"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"lastUpdatedTime"}},{"kind":"Field","name":{"kind":"Name","value":"images"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"newPath"}}]}}]}}]}}]}}]}}]}}]} as unknown as DocumentNode<NovelQuery, NovelQueryVariables>; export const NovelDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"Novel"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UnsignedInt"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"novels"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"where"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"id"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"eq"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}]}}]}},{"kind":"Argument","name":{"kind":"Name","value":"first"},"value":{"kind":"IntValue","value":"1"}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"nodes"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"url"}},{"kind":"Field","name":{"kind":"Name","value":"rawLanguage"}},{"kind":"Field","name":{"kind":"Name","value":"rawStatus"}},{"kind":"Field","name":{"kind":"Name","value":"statusOverride"}},{"kind":"Field","name":{"kind":"Name","value":"externalId"}},{"kind":"Field","name":{"kind":"Name","value":"createdTime"}},{"kind":"Field","name":{"kind":"Name","value":"lastUpdatedTime"}},{"kind":"Field","name":{"kind":"Name","value":"author"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"externalUrl"}}]}},{"kind":"Field","name":{"kind":"Name","value":"source"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"key"}},{"kind":"Field","name":{"kind":"Name","value":"url"}}]}},{"kind":"Field","name":{"kind":"Name","value":"coverImage"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"newPath"}}]}},{"kind":"Field","name":{"kind":"Name","value":"tags"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"key"}},{"kind":"Field","name":{"kind":"Name","value":"displayName"}},{"kind":"Field","name":{"kind":"Name","value":"tagType"}}]}},{"kind":"Field","name":{"kind":"Name","value":"volumes"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"order"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"createdTime"}},{"kind":"Field","name":{"kind":"Name","value":"lastUpdatedTime"}},{"kind":"Field","name":{"kind":"Name","value":"chapters"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"order"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"lastUpdatedTime"}},{"kind":"Field","name":{"kind":"Name","value":"images"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"newPath"}}]}}]}}]}}]}}]}}]}}]} as unknown as DocumentNode<NovelQuery, NovelQueryVariables>;
export const NovelsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"Novels"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"first"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"after"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"where"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"NovelDtoFilterInput"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"order"}},"type":{"kind":"ListType","type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"NovelDtoSortInput"}}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"novels"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"first"},"value":{"kind":"Variable","name":{"kind":"Name","value":"first"}}},{"kind":"Argument","name":{"kind":"Name","value":"after"},"value":{"kind":"Variable","name":{"kind":"Name","value":"after"}}},{"kind":"Argument","name":{"kind":"Name","value":"where"},"value":{"kind":"Variable","name":{"kind":"Name","value":"where"}}},{"kind":"Argument","name":{"kind":"Name","value":"order"},"value":{"kind":"Variable","name":{"kind":"Name","value":"order"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"edges"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"cursor"}},{"kind":"Field","name":{"kind":"Name","value":"node"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"url"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"coverImage"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"newPath"}}]}},{"kind":"Field","name":{"kind":"Name","value":"rawStatus"}},{"kind":"Field","name":{"kind":"Name","value":"lastUpdatedTime"}},{"kind":"Field","name":{"kind":"Name","value":"volumes"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"order"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"chapters"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"order"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"tags"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"key"}},{"kind":"Field","name":{"kind":"Name","value":"displayName"}},{"kind":"Field","name":{"kind":"Name","value":"tagType"}}]}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"pageInfo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"hasNextPage"}},{"kind":"Field","name":{"kind":"Name","value":"endCursor"}}]}}]}}]}}]} as unknown as DocumentNode<NovelsQuery, NovelsQueryVariables>; export const NovelsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"Novels"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"first"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"after"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"where"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"NovelDtoFilterInput"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"order"}},"type":{"kind":"ListType","type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"NovelDtoSortInput"}}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"novels"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"first"},"value":{"kind":"Variable","name":{"kind":"Name","value":"first"}}},{"kind":"Argument","name":{"kind":"Name","value":"after"},"value":{"kind":"Variable","name":{"kind":"Name","value":"after"}}},{"kind":"Argument","name":{"kind":"Name","value":"where"},"value":{"kind":"Variable","name":{"kind":"Name","value":"where"}}},{"kind":"Argument","name":{"kind":"Name","value":"order"},"value":{"kind":"Variable","name":{"kind":"Name","value":"order"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"edges"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"cursor"}},{"kind":"Field","name":{"kind":"Name","value":"node"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"url"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"coverImage"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"newPath"}}]}},{"kind":"Field","name":{"kind":"Name","value":"rawStatus"}},{"kind":"Field","name":{"kind":"Name","value":"lastUpdatedTime"}},{"kind":"Field","name":{"kind":"Name","value":"volumes"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"order"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"chapters"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"order"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"tags"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"key"}},{"kind":"Field","name":{"kind":"Name","value":"displayName"}},{"kind":"Field","name":{"kind":"Name","value":"tagType"}}]}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"pageInfo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"hasNextPage"}},{"kind":"Field","name":{"kind":"Name","value":"endCursor"}}]}}]}}]}}]} as unknown as DocumentNode<NovelsQuery, NovelsQueryVariables>;
export const GetReadingListDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetReadingList"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"readingList"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"itemCount"}},{"kind":"Field","name":{"kind":"Name","value":"createdTime"}},{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"novelId"}},{"kind":"Field","name":{"kind":"Name","value":"order"}},{"kind":"Field","name":{"kind":"Name","value":"addedTime"}}]}}]}}]}}]} as unknown as DocumentNode<GetReadingListQuery, GetReadingListQueryVariables>;
export const GetReadingListsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetReadingLists"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"readingLists"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"itemCount"}},{"kind":"Field","name":{"kind":"Name","value":"createdTime"}}]}}]}}]} as unknown as DocumentNode<GetReadingListsQuery, GetReadingListsQueryVariables>;
export const GetReadingListsWithItemsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetReadingListsWithItems"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"readingLists"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"itemCount"}},{"kind":"Field","name":{"kind":"Name","value":"createdTime"}},{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"novelId"}},{"kind":"Field","name":{"kind":"Name","value":"order"}},{"kind":"Field","name":{"kind":"Name","value":"addedTime"}}]}}]}}]}}]} as unknown as DocumentNode<GetReadingListsWithItemsQuery, GetReadingListsWithItemsQueryVariables>;
export const GetSettingsPageDataDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetSettingsPageData"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"currentUser"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"username"}},{"kind":"Field","name":{"kind":"Name","value":"availableInvites"}},{"kind":"Field","name":{"kind":"Name","value":"invitedUsers"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"username"}},{"kind":"Field","name":{"kind":"Name","value":"email"}}]}}]}}]}}]} as unknown as DocumentNode<GetSettingsPageDataQuery, GetSettingsPageDataQueryVariables>; export const GetSettingsPageDataDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetSettingsPageData"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"currentUser"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"username"}},{"kind":"Field","name":{"kind":"Name","value":"availableInvites"}},{"kind":"Field","name":{"kind":"Name","value":"invitedUsers"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"username"}},{"kind":"Field","name":{"kind":"Name","value":"email"}}]}}]}}]}}]} as unknown as DocumentNode<GetSettingsPageDataQuery, GetSettingsPageDataQueryVariables>;

View File

@@ -0,0 +1,17 @@
mutation AddToReadingList($input: AddToReadingListInput!) {
addToReadingList(input: $input) {
readingListPayload {
success
readingList {
id
name
itemCount
}
}
errors {
... on Error {
message
}
}
}
}

View File

@@ -0,0 +1,19 @@
mutation CreateReadingList($input: CreateReadingListInput!) {
createReadingList(input: $input) {
readingListPayload {
success
readingList {
id
name
description
itemCount
createdTime
}
}
errors {
... on Error {
message
}
}
}
}

View File

@@ -0,0 +1,10 @@
mutation DeleteReadingList($input: DeleteReadingListInput!) {
deleteReadingList(input: $input) {
success
errors {
... on Error {
message
}
}
}
}

View File

@@ -0,0 +1,17 @@
mutation RemoveFromReadingList($input: RemoveFromReadingListInput!) {
removeFromReadingList(input: $input) {
readingListPayload {
success
readingList {
id
name
itemCount
}
}
errors {
... on Error {
message
}
}
}
}

View File

@@ -0,0 +1,12 @@
mutation ReorderReadingListItem($input: ReorderReadingListItemInput!) {
reorderReadingListItem(input: $input) {
readingListPayload {
success
}
errors {
... on Error {
message
}
}
}
}

View File

@@ -0,0 +1,19 @@
mutation UpdateReadingList($input: UpdateReadingListInput!) {
updateReadingList(input: $input) {
readingListPayload {
success
readingList {
id
name
description
itemCount
createdTime
}
}
errors {
... on Error {
message
}
}
}
}

View File

@@ -0,0 +1,14 @@
query GetReadingList($id: Int!) {
readingList(id: $id) {
id
name
description
itemCount
createdTime
items {
novelId
order
addedTime
}
}
}

View File

@@ -0,0 +1,9 @@
query GetReadingLists {
readingLists {
id
name
description
itemCount
createdTime
}
}

View File

@@ -0,0 +1,14 @@
query GetReadingListsWithItems {
readingLists {
id
name
description
itemCount
createdTime
items {
novelId
order
addedTime
}
}
}

View File

@@ -0,0 +1,10 @@
---
import AppLayout from '../../layouts/AppLayout.astro';
import ReadingListDetailPage from '../../lib/components/ReadingListDetailPage.svelte';
const { id } = Astro.params;
---
<AppLayout title="Reading List - FictionArchive">
<ReadingListDetailPage listId={id} client:load />
</AppLayout>

View File

@@ -0,0 +1,8 @@
---
import AppLayout from '../../layouts/AppLayout.astro';
import ReadingListsPage from '../../lib/components/ReadingListsPage.svelte';
---
<AppLayout title="Reading Lists - FictionArchive">
<ReadingListsPage client:load />
</AppLayout>