diff --git a/README.md b/README.md index 55dd3bc..61d9ca5 100644 --- a/README.md +++ b/README.md @@ -73,6 +73,10 @@ GET /api/monsters?gameId=4 # every Final Fantasy IV monster | `GET` | `/api/games` | List all games (`page`, `pageSize`) | | `GET` | `/api/games/{id}` | Get a game by ID (includes character + monster counts) | +Both return two pictures: `imageUrl` is the full logo — the wide lockup with the title text — +and `thumbnailUrl` is the square emblem, the artwork alone. They are separate crops rather than +two sizes of one image, so pick by shape, not by resolution. + ### Arena Powers [Battle Square](https://moogleapi.com/battle-square) — one character against eight consecutive waves of their own game's monsters. diff --git a/src/MoogleAPI.Web/Dashboard/index.html b/src/MoogleAPI.Web/Dashboard/index.html index 0d994a6..e4d9fd1 100644 --- a/src/MoogleAPI.Web/Dashboard/index.html +++ b/src/MoogleAPI.Web/Dashboard/index.html @@ -700,8 +700,16 @@ endpoint: '/api/dashboard/games', singular: 'game', paged: false, - hasArt: false, + hasArt: true, + // A square emblem and the wide logo — two crops, not two sizes. No generated slot: + // generation never selects games, so it would upload into a column nothing promotes + // and nothing serves. The API refuses that combination too. + artSlots: [ + { key: 'thumbnailUrl', label: 'Thumbnail', slot: 'thumbnail', preview: true }, + { key: 'imageUrl', label: 'Full logo', slot: 'original' }, + ], columns: [ + { label: '', cls: 'thumb', art: true, px: 64, prio: 1 }, { label: 'ID', cls: 'id', get: r => r.id, px: 54, prio: 2 }, { label: 'Name', cls: 'name', get: r => r.fields.name, flex: 2, prio: 1 }, { label: 'Year', cls: 'num', get: r => r.fields.releaseYear, px: 58, prio: 1 }, @@ -720,12 +728,24 @@ }, }; - // Both artwork columns, and which R2 slot an upload to each one targets. + // Both artwork columns, and which R2 slot an upload to each one targets. A tab can narrow + // this with its own `artSlots` — games carry a logo and nothing else. const ART_SLOTS = [ { key: 'imageUrl', label: 'Original', slot: 'original' }, - { key: 'generatedImageUrl', label: 'Generated', slot: 'generated' }, + // `preview` picks which picture the table cell shows when a row has more than one. + { key: 'generatedImageUrl', label: 'Generated', slot: 'generated', preview: true }, ]; + const artSlotsFor = cfg => cfg.artSlots ?? ART_SLOTS; + + // The marked slot if it has a picture, otherwise whichever slot does. + function previewUrl(row, cfg) { + const slots = artSlotsFor(cfg); + const chosen = slots.find(s => s.preview && row.fields[s.key]) + ?? slots.find(s => row.fields[s.key]); + return chosen ? row.fields[chosen.key] : null; + } + // ── State ─────────────────────────────────────────────────────────────────── const state = { tab: 'characters', search: '', gameId: '', page: 1, pageSize: 50, total: 0 }; const editing = { tab: null, row: null, mode: 'edit', inputs: new Map(), art: new Map() }; @@ -832,7 +852,7 @@ function artCell(row) { const td = document.createElement('td'); td.className = 'thumb'; - const src = row.fields.generatedImageUrl || row.fields.imageUrl; + const src = previewUrl(row, TABS[state.tab]); if (!src) { const none = document.createElement('div'); @@ -934,7 +954,7 @@ // A row can carry two pictures — the original scraped art and its generated replacement — so // the modal offers both rather than picking one: comparing them is usually the reason to look. function openZoom(row) { - const sources = ART_SLOTS + const sources = artSlotsFor(TABS[state.tab]) .filter(s => row.fields[s.key]) .map(s => ({ label: s.label, url: row.fields[s.key] })) .reverse(); @@ -1140,6 +1160,10 @@ } else { fields.releaseYear = new Date().getFullYear(); fields.platform = ''; + // Games carry two pictures and no generated slot. + fields.imageUrl = null; + fields.thumbnailUrl = null; + fields.imageSourceUrl = null; } return { id: null, gameName: '', releaseYear: 0, fields }; } @@ -1168,7 +1192,7 @@ $('editor-art-wrap').hidden = !cfg.hasArt || creating; if (cfg.hasArt && !creating) { - $('editor-art').replaceChildren(...ART_SLOTS.map(slot => buildArtSlot(slot, row))); + $('editor-art').replaceChildren(...artSlotsFor(cfg).map(slot => buildArtSlot(slot, row))); // Provenance is not something to hand-type, but it decides whether the image tool will // overwrite this row's art, so it is shown and editable rather than hidden. @@ -1178,7 +1202,9 @@ } else if (cfg.hasArt && creating) { // The draft may already carry a URL the import found; keep it rather than dropping it on // the floor just because the upload panel is not shown yet. - for (const key of ['imageUrl', 'generatedImageUrl', 'imageSourceUrl']) { + // Only the keys this tab actually has — posting a generatedImageUrl back for a game + // would be a field its edit record does not carry. + for (const key of [...artSlotsFor(cfg).map(s => s.key), 'imageSourceUrl']) { const hidden = document.createElement('input'); hidden.type = 'hidden'; hidden.value = row.fields[key] ?? ''; @@ -1402,7 +1428,7 @@ name.textContent = row.fields.name; body.append(name, ` (#${row.id})? This cannot be undone.`); - if (cfg.hasArt && (row.fields.imageUrl || row.fields.generatedImageUrl)) { + if (cfg.hasArt && artSlotsFor(cfg).some(s => row.fields[s.key])) { body.append(document.createElement('br')); const note = document.createElement('small'); note.style.color = 'var(--muted)'; diff --git a/src/MoogleAPI.Web/Features/Dashboard/Browse/GamesEndpoint.cs b/src/MoogleAPI.Web/Features/Dashboard/Browse/GamesEndpoint.cs index 4d1068f..88cca5f 100644 --- a/src/MoogleAPI.Web/Features/Dashboard/Browse/GamesEndpoint.cs +++ b/src/MoogleAPI.Web/Features/Dashboard/Browse/GamesEndpoint.cs @@ -32,7 +32,8 @@ public override async Task HandleAsync(BrowseRequest req, CancellationToken ct) .OrderBy(g => g.ReleaseYear).ThenBy(g => g.Name) .Select(g => new GameRow( g.Id, g.Characters.Count, g.Monsters.Count, g.Cards.Count, - new GameEdit(g.Name, g.ReleaseYear, g.Platform, g.Description))) + new GameEdit(g.Name, g.ReleaseYear, g.Platform, g.Description, + g.ImageUrl, g.ThumbnailUrl, g.ImageSourceUrl))) .ToListAsync(ct); await Send.OkAsync(new BrowseResponse(items, items.Count, 1, items.Count), ct); diff --git a/src/MoogleAPI.Web/Features/Dashboard/Browse/Models.cs b/src/MoogleAPI.Web/Features/Dashboard/Browse/Models.cs index 0dcb2b5..60f3117 100644 --- a/src/MoogleAPI.Web/Features/Dashboard/Browse/Models.cs +++ b/src/MoogleAPI.Web/Features/Dashboard/Browse/Models.cs @@ -79,11 +79,19 @@ public record MonsterEdit( int GameId ); +/// +/// Two images, and no GeneratedImageUrl. The full logo and the square emblem are separate +/// crops rather than two sizes of one picture, so both are stored. Generation, meanwhile, selects +/// monsters and characters only — a generated slot here would be one nothing can ever fill. +/// public record GameEdit( string Name, int ReleaseYear, string Platform, - string? Description + string? Description, + string? ImageUrl, + string? ThumbnailUrl, + string? ImageSourceUrl ); // ── Rows ────────────────────────────────────────────────────────────────────── diff --git a/src/MoogleAPI.Web/Features/Dashboard/Create/Endpoints.cs b/src/MoogleAPI.Web/Features/Dashboard/Create/Endpoints.cs index 87250e2..220b77a 100644 --- a/src/MoogleAPI.Web/Features/Dashboard/Create/Endpoints.cs +++ b/src/MoogleAPI.Web/Features/Dashboard/Create/Endpoints.cs @@ -208,7 +208,8 @@ public override async Task HandleAsync(CreateGameRequest req, CancellationToken await Send.OkAsync(new CreateResponse( new GameRow(game.Id, 0, 0, 0, - new GameEdit(game.Name, game.ReleaseYear, game.Platform, game.Description)), + new GameEdit(game.Name, game.ReleaseYear, game.Platform, game.Description, + game.ImageUrl, game.ThumbnailUrl, game.ImageSourceUrl)), duplicate ? $"There was already a game called {game.Name}." : null), ct); } } diff --git a/src/MoogleAPI.Web/Features/Dashboard/Update/GameEndpoint.cs b/src/MoogleAPI.Web/Features/Dashboard/Update/GameEndpoint.cs index aeb89bb..4debc7c 100644 --- a/src/MoogleAPI.Web/Features/Dashboard/Update/GameEndpoint.cs +++ b/src/MoogleAPI.Web/Features/Dashboard/Update/GameEndpoint.cs @@ -35,6 +35,9 @@ public override async Task HandleAsync(UpdateGameRequest req, CancellationToken game.ReleaseYear = f.ReleaseYear; game.Platform = f.Platform.Trim(); game.Description = EditText.Clean(f.Description); + game.ImageUrl = EditText.Clean(f.ImageUrl); + game.ThumbnailUrl = EditText.Clean(f.ThumbnailUrl); + game.ImageSourceUrl = EditText.Clean(f.ImageSourceUrl); await db.SaveChangesAsync(ct); await CatalogCache.InvalidateAsync(cache, ct); @@ -46,6 +49,7 @@ public override async Task HandleAsync(UpdateGameRequest req, CancellationToken await Send.OkAsync(new UpdateResponse( new GameRow(game.Id, counts.Characters, counts.Monsters, counts.Cards, - new GameEdit(game.Name, game.ReleaseYear, game.Platform, game.Description))), ct); + new GameEdit(game.Name, game.ReleaseYear, game.Platform, game.Description, + game.ImageUrl, game.ThumbnailUrl, game.ImageSourceUrl))), ct); } } diff --git a/src/MoogleAPI.Web/Features/Dashboard/Update/Models.cs b/src/MoogleAPI.Web/Features/Dashboard/Update/Models.cs index 67251a4..97dcdd6 100644 --- a/src/MoogleAPI.Web/Features/Dashboard/Update/Models.cs +++ b/src/MoogleAPI.Web/Features/Dashboard/Update/Models.cs @@ -130,5 +130,7 @@ public UpdateGameValidator() .WithMessage("Release year looks wrong."); RuleFor(x => x.Fields.Description).MaximumLength(EditRules.MaxLongText); + RuleFor(x => x.Fields.ImageUrl).OptionalUrl(); + RuleFor(x => x.Fields.ThumbnailUrl).OptionalUrl(); } } diff --git a/src/MoogleAPI.Web/Features/Dashboard/UploadImage/Endpoint.cs b/src/MoogleAPI.Web/Features/Dashboard/UploadImage/Endpoint.cs index 79e190e..4b2ec91 100644 --- a/src/MoogleAPI.Web/Features/Dashboard/UploadImage/Endpoint.cs +++ b/src/MoogleAPI.Web/Features/Dashboard/UploadImage/Endpoint.cs @@ -8,7 +8,7 @@ namespace MoogleAPI.Web.Features.Dashboard.UploadImage; public class UploadImageRequest { - /// "characters" or "monsters" — the same words the bucket uses for its folders. + /// "characters", "monsters" or "games" — the same words the bucket uses for its folders. public string Resource { get; set; } = string.Empty; public int Id { get; set; } @@ -53,8 +53,17 @@ public class Endpoint(AppDbContext db, HybridCache cache, ImageUploadStore store /// Comfortably above any source art, far below anything that would stall the server. private const long MaxBytes = 15 * 1024 * 1024; - private static readonly string[] Resources = ["characters", "monsters"]; - private static readonly string[] Slots = ["original", "generated"]; + /// + /// Which slots each resource actually has. A flat list would let a caller upload a game's + /// "generated" art — generation never selects games, so that file would be promoted by + /// nothing and served by nothing. + /// + private static readonly Dictionary SlotsByResource = new() + { + ["characters"] = ["original", "generated"], + ["monsters"] = ["original", "generated"], + ["games"] = ["original", "thumbnail"], + }; public override void Configure() { @@ -78,9 +87,16 @@ public override async Task HandleAsync(UploadImageRequest req, CancellationToken var resource = req.Resource.ToLowerInvariant(); var slot = req.Slot.ToLowerInvariant(); - if (!Resources.Contains(resource) || !Slots.Contains(slot)) + if (!SlotsByResource.TryGetValue(resource, out var allowedSlots)) { - AddError($"Resource must be one of {string.Join(", ", Resources)} and slot one of {string.Join(", ", Slots)}."); + AddError($"Resource must be one of {string.Join(", ", SlotsByResource.Keys)}."); + await Send.ErrorsAsync(cancellation: ct); + return; + } + + if (!allowedSlots.Contains(slot)) + { + AddError($"{resource} take slot {string.Join(" or ", allowedSlots)}, not \"{slot}\"."); await Send.ErrorsAsync(cancellation: ct); return; } @@ -92,9 +108,12 @@ public override async Task HandleAsync(UploadImageRequest req, CancellationToken return; } - var exists = resource == "characters" - ? await db.Characters.AnyAsync(c => c.Id == req.Id, ct) - : await db.Monsters.AnyAsync(m => m.Id == req.Id, ct); + var exists = resource switch + { + "characters" => await db.Characters.AnyAsync(c => c.Id == req.Id, ct), + "monsters" => await db.Monsters.AnyAsync(m => m.Id == req.Id, ct), + _ => await db.Games.AnyAsync(g => g.Id == req.Id, ct), + }; if (!exists) { @@ -102,7 +121,14 @@ public override async Task HandleAsync(UploadImageRequest req, CancellationToken return; } - var key = slot == "generated" ? $"gen/{resource}/{req.Id}.webp" : $"{resource}/{req.Id}.webp"; + // Prefix per slot, mirroring the bucket layout the image tool already writes: + // gen/monsters/12.webp, thumb/games/7.webp, monsters/12.webp. + var key = slot switch + { + "generated" => $"gen/{resource}/{req.Id}.webp", + "thumbnail" => $"thumb/{resource}/{req.Id}.webp", + _ => $"{resource}/{req.Id}.webp", + }; string url; try @@ -125,12 +151,18 @@ public override async Task HandleAsync(UploadImageRequest req, CancellationToken if (slot == "generated") row.GeneratedImageUrl = url; else { row.ImageUrl = url; row.ImageSourceUrl = provenance; } } - else + else if (resource == "monsters") { var row = await db.Monsters.FirstAsync(m => m.Id == req.Id, ct); if (slot == "generated") row.GeneratedImageUrl = url; else { row.ImageUrl = url; row.ImageSourceUrl = provenance; } } + else + { + var row = await db.Games.FirstAsync(g => g.Id == req.Id, ct); + if (slot == "thumbnail") row.ThumbnailUrl = url; + else { row.ImageUrl = url; row.ImageSourceUrl = provenance; } + } await db.SaveChangesAsync(ct); await CatalogCache.InvalidateAsync(cache, ct); diff --git a/src/MoogleAPI.Web/Features/Games/Get/Endpoint.cs b/src/MoogleAPI.Web/Features/Games/Get/Endpoint.cs index 004752c..e6ca061 100644 --- a/src/MoogleAPI.Web/Features/Games/Get/Endpoint.cs +++ b/src/MoogleAPI.Web/Features/Games/Get/Endpoint.cs @@ -24,7 +24,7 @@ public override async Task HandleAsync(GetGameRequest req, CancellationToken ct) async token => await db.Games .Where(g => g.Id == req.Id) .Select(g => new GetGameResponse( - g.Id, g.Name, g.ReleaseYear, g.Platform, g.Description, + g.Id, g.Name, g.ReleaseYear, g.Platform, g.Description, g.ImageUrl, g.ThumbnailUrl, g.Characters.Count, g.Monsters.Count)) .FirstOrDefaultAsync(token), tags: CatalogCache.Tags, diff --git a/src/MoogleAPI.Web/Features/Games/Get/Models.cs b/src/MoogleAPI.Web/Features/Games/Get/Models.cs index c926fbf..73b3eba 100644 --- a/src/MoogleAPI.Web/Features/Games/Get/Models.cs +++ b/src/MoogleAPI.Web/Features/Games/Get/Models.cs @@ -8,6 +8,10 @@ public record GetGameResponse( int ReleaseYear, string Platform, string? Description, + /// The full logo — the wide lockup with the title text. + string? ImageUrl, + /// The square emblem — artwork only, no title text. + string? ThumbnailUrl, int CharacterCount, int MonsterCount ); diff --git a/src/MoogleAPI.Web/Features/Games/GetAll/Endpoint.cs b/src/MoogleAPI.Web/Features/Games/GetAll/Endpoint.cs index 4843b09..0cbaea9 100644 --- a/src/MoogleAPI.Web/Features/Games/GetAll/Endpoint.cs +++ b/src/MoogleAPI.Web/Features/Games/GetAll/Endpoint.cs @@ -30,7 +30,7 @@ public override async Task HandleAsync(GetAllGamesRequest req, CancellationToken .OrderBy(g => g.ReleaseYear) .Skip((req.Page - 1) * req.PageSize) .Take(req.PageSize) - .Select(g => new GameSummary(g.Id, g.Name, g.ReleaseYear, g.Platform)) + .Select(g => new GameSummary(g.Id, g.Name, g.ReleaseYear, g.Platform, g.ImageUrl, g.ThumbnailUrl)) .ToListAsync(token); return new GetAllGamesResponse(items, total, req.Page, req.PageSize); diff --git a/src/MoogleAPI.Web/Features/Games/GetAll/Models.cs b/src/MoogleAPI.Web/Features/Games/GetAll/Models.cs index df8b2b5..327f424 100644 --- a/src/MoogleAPI.Web/Features/Games/GetAll/Models.cs +++ b/src/MoogleAPI.Web/Features/Games/GetAll/Models.cs @@ -2,6 +2,11 @@ namespace MoogleAPI.Web.Features.Games.GetAll; public record GetAllGamesRequest(int Page = 1, int PageSize = 20); -public record GameSummary(int Id, string Name, int ReleaseYear, string Platform); +public record GameSummary( + int Id, string Name, int ReleaseYear, string Platform, + /// The full logo — the wide lockup with the title text. + string? ImageUrl, + /// The square emblem — artwork only, no title text. + string? ThumbnailUrl); public record GetAllGamesResponse(IReadOnlyList Items, int TotalCount, int Page, int PageSize); diff --git a/src/MoogleAPI.Web/Infrastructure/Models/Game.cs b/src/MoogleAPI.Web/Infrastructure/Models/Game.cs index a2974f2..f51ee48 100644 --- a/src/MoogleAPI.Web/Infrastructure/Models/Game.cs +++ b/src/MoogleAPI.Web/Infrastructure/Models/Game.cs @@ -8,6 +8,28 @@ public class Game public string Platform { get; set; } = string.Empty; public string? Description { get; set; } + /// + /// The game's full logo — the wide lockup with the title text. Hand-uploaded through the + /// dashboard rather than scraped: there is no per-game article the image pipeline reads, and + /// a logo is a fixed piece of brand art, not something to search for. + /// + public string? ImageUrl { get; set; } + + /// + /// The square emblem — the artwork alone, without the title text. A separate column rather + /// than a resize of because it is a different crop, not a smaller one: + /// the full logo is wide and mostly text, which is illegible and badly proportioned wherever + /// a square is wanted. + /// + public string? ThumbnailUrl { get; set; } + + /// + /// Where came from, matching Monster.ImageSourceUrl. Games are + /// not in the copy or generation stages, so nothing reads this to decide what to re-fetch; + /// it is kept so a logo's provenance is recorded the same way as every other image. + /// + public string? ImageSourceUrl { get; set; } + public ICollection Characters { get; set; } = []; public ICollection Monsters { get; set; } = []; public ICollection Cards { get; set; } = []; diff --git a/src/MoogleAPI.Web/Migrations/20260805033624_AddGameImages.Designer.cs b/src/MoogleAPI.Web/Migrations/20260805033624_AddGameImages.Designer.cs new file mode 100644 index 0000000..05289fe --- /dev/null +++ b/src/MoogleAPI.Web/Migrations/20260805033624_AddGameImages.Designer.cs @@ -0,0 +1,406 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using MoogleAPI.Web.Infrastructure.Data; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace MoogleAPI.Web.Migrations +{ + [DbContext(typeof(AppDbContext))] + [Migration("20260805033624_AddGameImages")] + partial class AddGameImages + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.7") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("MoogleAPI.Web.Infrastructure.Models.Card", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Bottom") + .HasColumnType("integer"); + + b.Property("CardClass") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Element") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("GameId") + .HasColumnType("integer"); + + b.Property("ImageSourceUrl") + .HasColumnType("text"); + + b.Property("ImageUrl") + .HasColumnType("text"); + + b.Property("Left") + .HasColumnType("integer"); + + b.Property("Level") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("Right") + .HasColumnType("integer"); + + b.Property("Top") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("GameId"); + + b.HasIndex("Name", "GameId") + .IsUnique(); + + b.ToTable("Cards"); + }); + + modelBuilder.Entity("MoogleAPI.Web.Infrastructure.Models.Character", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Abilities") + .HasColumnType("text"); + + b.Property("Affiliation") + .HasColumnType("text"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("GameId") + .HasColumnType("integer"); + + b.Property("GeneratedImageUrl") + .HasColumnType("text"); + + b.Property("Hometown") + .HasColumnType("text"); + + b.Property("ImageKind") + .HasColumnType("text"); + + b.Property("ImageSourceUrl") + .HasColumnType("text"); + + b.Property("ImageUrl") + .HasColumnType("text"); + + b.Property("IsPlayable") + .HasColumnType("boolean"); + + b.Property("Job") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("Popularity") + .HasColumnType("integer"); + + b.Property("Race") + .HasColumnType("text"); + + b.Property("Role") + .HasColumnType("text"); + + b.Property("Weapon") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("WikiBacklinks") + .HasColumnType("integer"); + + b.Property("WikiPageLength") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("GameId"); + + b.HasIndex("Popularity"); + + b.HasIndex("GameId", "IsPlayable") + .HasFilter("\"IsPlayable\" = true"); + + b.HasIndex("Name", "GameId") + .IsUnique(); + + b.ToTable("Characters"); + }); + + modelBuilder.Entity("MoogleAPI.Web.Infrastructure.Models.Game", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("ImageSourceUrl") + .HasColumnType("text"); + + b.Property("ImageUrl") + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("Platform") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("ReleaseYear") + .HasColumnType("integer"); + + b.Property("ThumbnailUrl") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Games"); + }); + + modelBuilder.Entity("MoogleAPI.Web.Infrastructure.Models.Monster", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Abilities") + .HasColumnType("text"); + + b.Property("Absorbs") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("Attack") + .HasColumnType("integer"); + + b.Property("Category") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Defense") + .HasColumnType("integer"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("Drops") + .HasColumnType("text"); + + b.Property("Evasion") + .HasColumnType("integer"); + + b.Property("Experience") + .HasColumnType("integer"); + + b.Property("GameId") + .HasColumnType("integer"); + + b.Property("GeneratedImageUrl") + .HasColumnType("text"); + + b.Property("Gil") + .HasColumnType("integer"); + + b.Property("HitPoints") + .HasColumnType("integer"); + + b.Property("ImageKind") + .HasColumnType("text"); + + b.Property("ImageSourceUrl") + .HasColumnType("text"); + + b.Property("ImageUrl") + .HasColumnType("text"); + + b.Property("Level") + .HasColumnType("integer"); + + b.Property("Location") + .HasColumnType("text"); + + b.Property("MagicAttack") + .HasColumnType("integer"); + + b.Property("MagicDefense") + .HasColumnType("integer"); + + b.Property("MagicPoints") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("Popularity") + .HasColumnType("integer"); + + b.Property("Speed") + .HasColumnType("integer"); + + b.Property("Steals") + .HasColumnType("text"); + + b.Property("Weaknesses") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("WikiBacklinks") + .HasColumnType("integer"); + + b.Property("WikiPageLength") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("Category"); + + b.HasIndex("GameId"); + + b.HasIndex("Popularity"); + + b.HasIndex("Name", "GameId") + .IsUnique(); + + b.ToTable("Monsters"); + }); + + modelBuilder.Entity("MoogleAPI.Web.Infrastructure.Models.RequestLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("DurationMs") + .HasColumnType("integer"); + + b.Property("IpHash") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("IsPremium") + .HasColumnType("boolean"); + + b.Property("Method") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("character varying(10)"); + + b.Property("Path") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ResourceType") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("SearchTerm") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("StatusCode") + .HasColumnType("integer"); + + b.Property("Timestamp") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("Timestamp"); + + b.ToTable("RequestLogs"); + }); + + modelBuilder.Entity("MoogleAPI.Web.Infrastructure.Models.Card", b => + { + b.HasOne("MoogleAPI.Web.Infrastructure.Models.Game", "Game") + .WithMany("Cards") + .HasForeignKey("GameId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Game"); + }); + + modelBuilder.Entity("MoogleAPI.Web.Infrastructure.Models.Character", b => + { + b.HasOne("MoogleAPI.Web.Infrastructure.Models.Game", "Game") + .WithMany("Characters") + .HasForeignKey("GameId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Game"); + }); + + modelBuilder.Entity("MoogleAPI.Web.Infrastructure.Models.Monster", b => + { + b.HasOne("MoogleAPI.Web.Infrastructure.Models.Game", "Game") + .WithMany("Monsters") + .HasForeignKey("GameId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Game"); + }); + + modelBuilder.Entity("MoogleAPI.Web.Infrastructure.Models.Game", b => + { + b.Navigation("Cards"); + + b.Navigation("Characters"); + + b.Navigation("Monsters"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/MoogleAPI.Web/Migrations/20260805033624_AddGameImages.cs b/src/MoogleAPI.Web/Migrations/20260805033624_AddGameImages.cs new file mode 100644 index 0000000..eae2fce --- /dev/null +++ b/src/MoogleAPI.Web/Migrations/20260805033624_AddGameImages.cs @@ -0,0 +1,48 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace MoogleAPI.Web.Migrations +{ + /// + public partial class AddGameImages : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "ImageSourceUrl", + table: "Games", + type: "text", + nullable: true); + + migrationBuilder.AddColumn( + name: "ImageUrl", + table: "Games", + type: "text", + nullable: true); + + migrationBuilder.AddColumn( + name: "ThumbnailUrl", + table: "Games", + type: "text", + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "ImageSourceUrl", + table: "Games"); + + migrationBuilder.DropColumn( + name: "ImageUrl", + table: "Games"); + + migrationBuilder.DropColumn( + name: "ThumbnailUrl", + table: "Games"); + } + } +} diff --git a/src/MoogleAPI.Web/Migrations/AppDbContextModelSnapshot.cs b/src/MoogleAPI.Web/Migrations/AppDbContextModelSnapshot.cs index ca9e30f..a032cdd 100644 --- a/src/MoogleAPI.Web/Migrations/AppDbContextModelSnapshot.cs +++ b/src/MoogleAPI.Web/Migrations/AppDbContextModelSnapshot.cs @@ -169,6 +169,12 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("Description") .HasColumnType("text"); + b.Property("ImageSourceUrl") + .HasColumnType("text"); + + b.Property("ImageUrl") + .HasColumnType("text"); + b.Property("Name") .IsRequired() .HasMaxLength(200) @@ -182,6 +188,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("ReleaseYear") .HasColumnType("integer"); + b.Property("ThumbnailUrl") + .HasColumnType("text"); + b.HasKey("Id"); b.ToTable("Games");