Compare commits
1 Commits
feature/ge
...
feature/we
| Author | SHA1 | Date | |
|---|---|---|---|
| 97acaf6b10 |
@@ -11,17 +11,21 @@ public class CmsTests : IClassFixture<ApiFactory>
|
|||||||
public CmsTests(ApiFactory factory) => _client = factory.CreateClient();
|
public CmsTests(ApiFactory factory) => _client = factory.CreateClient();
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Snapshot_has_site_nav_and_six_seeded_pages()
|
public async Task Snapshot_has_site_nav_and_seeded_pages()
|
||||||
{
|
{
|
||||||
var doc = JsonDocument.Parse(await _client.GetStringAsync("/api/site-snapshot"));
|
var doc = JsonDocument.Parse(await _client.GetStringAsync("/api/site-snapshot"));
|
||||||
var root = doc.RootElement;
|
var root = doc.RootElement;
|
||||||
|
|
||||||
Assert.Equal("de", root.GetProperty("site").GetProperty("defaultLocale").GetString());
|
Assert.Equal("de", root.GetProperty("site").GetProperty("defaultLocale").GetString());
|
||||||
var nav = root.GetProperty("site").GetProperty("navOrder").EnumerateArray().Select(x => x.GetString()).ToList();
|
var nav = root.GetProperty("site").GetProperty("navOrder").EnumerateArray().Select(x => x.GetString()).ToList();
|
||||||
|
// main nav = 6 core pages (Impressum/Datenschutz are footer-only, not in navOrder)
|
||||||
Assert.Equal(new[] { "start", "ueber-die-zucht", "abgabetiere", "abgabebedingungen", "farben-genetik", "kontakt" }, nav);
|
Assert.Equal(new[] { "start", "ueber-die-zucht", "abgabetiere", "abgabebedingungen", "farben-genetik", "kontakt" }, nav);
|
||||||
|
|
||||||
var pages = root.GetProperty("pages").EnumerateArray().ToList();
|
var pages = root.GetProperty("pages").EnumerateArray().ToList();
|
||||||
Assert.Equal(6, pages.Count);
|
// 8 pages: 6 core + impressum + datenschutz (footer-only legal pages)
|
||||||
|
Assert.Equal(8, pages.Count);
|
||||||
|
Assert.Contains(pages, p => p.GetProperty("slug").GetString() == "impressum");
|
||||||
|
Assert.Contains(pages, p => p.GetProperty("slug").GetString() == "datenschutz");
|
||||||
// abgabetiere page carries an AbgabetiereList block with a resolved (possibly empty) animals array
|
// abgabetiere page carries an AbgabetiereList block with a resolved (possibly empty) animals array
|
||||||
var abg = pages.Single(p => p.GetProperty("slug").GetString() == "abgabetiere");
|
var abg = pages.Single(p => p.GetProperty("slug").GetString() == "abgabetiere");
|
||||||
var listBlock = abg.GetProperty("blocks").EnumerateArray()
|
var listBlock = abg.GetProperty("blocks").EnumerateArray()
|
||||||
|
|||||||
@@ -69,10 +69,11 @@ public class SiteRendererTests
|
|||||||
["data"] = new JsonObject { ["name"] = name, ["email"] = email, ["phone"] = phone },
|
["data"] = new JsonObject { ["name"] = name, ["email"] = email, ["phone"] = phone },
|
||||||
};
|
};
|
||||||
|
|
||||||
private static JsonObject AbgabetiereBlock(string intro, params (string name, string farbe, string? group, string? photo)[] animals)
|
private static JsonObject AbgabetiereBlock(string intro,
|
||||||
|
params (string name, string farbe, string? group, string? photo, string? gender, string? dob, string? note)[] animals)
|
||||||
{
|
{
|
||||||
var animalArr = new JsonArray();
|
var animalArr = new JsonArray();
|
||||||
foreach (var (name, farbe, group, photo) in animals)
|
foreach (var (name, farbe, group, photo, gender, dob, note) in animals)
|
||||||
{
|
{
|
||||||
var photos = new JsonArray();
|
var photos = new JsonArray();
|
||||||
if (photo is not null) photos.Add(photo);
|
if (photo is not null) photos.Add(photo);
|
||||||
@@ -81,6 +82,9 @@ public class SiteRendererTests
|
|||||||
["name"] = name,
|
["name"] = name,
|
||||||
["farbschlag"] = farbe,
|
["farbschlag"] = farbe,
|
||||||
["group"] = group,
|
["group"] = group,
|
||||||
|
["gender"] = gender,
|
||||||
|
["dateOfBirth"] = dob,
|
||||||
|
["characterNote"] = note,
|
||||||
["photos"] = photos,
|
["photos"] = photos,
|
||||||
["aiSaleText"] = (JsonNode?)null,
|
["aiSaleText"] = (JsonNode?)null,
|
||||||
});
|
});
|
||||||
@@ -92,6 +96,7 @@ public class SiteRendererTests
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// ── Tests ────────────────────────────────────────────────────────────────
|
// ── Tests ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -211,16 +216,52 @@ public class SiteRendererTests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void AbgabetiereList_renders_animal_cards_with_name_and_farbschlag()
|
public void AbgabetiereList_renders_group_card_with_name_farbschlag_and_photo()
|
||||||
{
|
{
|
||||||
var snap = MakeSnapshot(("abgabetiere", "Abgabetiere",
|
var snap = MakeSnapshot(("abgabetiere", "Abgabetiere",
|
||||||
Blocks(AbgabetiereBlock("Aktuelle Tiere:", ("Krümel", "CP-Agouti", "Großbecken", "/photos/files/abc.jpg")))));
|
Blocks(AbgabetiereBlock("Aktuelle Tiere:", ("Krümel", "CP-Agouti", "Großbecken", "/photos/files/abc.jpg", null, null, null)))));
|
||||||
var html = SiteRenderer.Render(snap)["abgabetiere/index.html"];
|
var html = SiteRenderer.Render(snap)["abgabetiere/index.html"];
|
||||||
Assert.Contains("Krümel", html);
|
Assert.Contains("Krümel", html);
|
||||||
Assert.Contains("CP-Agouti", html);
|
Assert.Contains("CP-Agouti", html);
|
||||||
Assert.Contains("Großbecken", html);
|
Assert.Contains("Großbecken", html);
|
||||||
Assert.Contains("/photos/files/abc.jpg", html);
|
Assert.Contains("/photos/files/abc.jpg", html);
|
||||||
Assert.Contains("cms-animal-card", html);
|
Assert.Contains("cms-group-card", html);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AbgabetiereList_group_card_shows_gender_label_and_available_badge()
|
||||||
|
{
|
||||||
|
var snap = MakeSnapshot(("abgabetiere", "Abgabetiere",
|
||||||
|
Blocks(AbgabetiereBlock("",
|
||||||
|
("Frieda", "Agouti", "Becken 1", null, "female", null, null),
|
||||||
|
("Rosa", "PEW", "Becken 1", null, "female", null, null)))));
|
||||||
|
var html = SiteRenderer.Render(snap)["abgabetiere/index.html"];
|
||||||
|
Assert.Contains("Weibchen", html);
|
||||||
|
Assert.Contains("Verfügbar", html);
|
||||||
|
Assert.Contains("cms-badge--free", html);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AbgabetiereList_shows_dateOfBirth_and_characterNote()
|
||||||
|
{
|
||||||
|
var snap = MakeSnapshot(("abgabetiere", "Abgabetiere",
|
||||||
|
Blocks(AbgabetiereBlock("",
|
||||||
|
("Pünktchen", "Siamesisch", null, null, "female", "2024-03-15", "Sehr neugierig und verspielt.")))));
|
||||||
|
var html = SiteRenderer.Render(snap)["abgabetiere/index.html"];
|
||||||
|
Assert.Contains("März 2024", html);
|
||||||
|
Assert.Contains("Sehr neugierig und verspielt.", html);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AbgabetiereList_solo_animal_without_group_gets_its_own_card()
|
||||||
|
{
|
||||||
|
var snap = MakeSnapshot(("abgabetiere", "Abgabetiere",
|
||||||
|
Blocks(AbgabetiereBlock("",
|
||||||
|
("Einzelkind", "Zobel", null, null, "male", null, null)))));
|
||||||
|
var html = SiteRenderer.Render(snap)["abgabetiere/index.html"];
|
||||||
|
Assert.Contains("Einzelkind", html);
|
||||||
|
Assert.Contains("Männchen", html);
|
||||||
|
Assert.Contains("cms-group-card", html);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -283,6 +324,44 @@ public class SiteRendererTests
|
|||||||
Assert.Equal(7, files.Count);
|
Assert.Equal(7, files.Count);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Render_impressum_and_datenschutz_pages_are_rendered()
|
||||||
|
{
|
||||||
|
var snap = MakeSnapshot(
|
||||||
|
("start", "Startseite", Blocks()),
|
||||||
|
("impressum", "Impressum", Blocks(RichTextBlock("§ 5 TMG Platzhalter"))),
|
||||||
|
("datenschutz", "Datenschutz", Blocks(RichTextBlock("Datenschutz Platzhalter"))));
|
||||||
|
var files = SiteRenderer.Render(snap);
|
||||||
|
Assert.True(files.ContainsKey("impressum/index.html"));
|
||||||
|
Assert.True(files.ContainsKey("datenschutz/index.html"));
|
||||||
|
Assert.Contains("§ 5 TMG Platzhalter", files["impressum/index.html"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Footer_contains_impressum_and_datenschutz_links()
|
||||||
|
{
|
||||||
|
var snap = MakeSnapshot(
|
||||||
|
("start", "Startseite", Blocks()),
|
||||||
|
("kontakt", "Kontakt", Blocks()));
|
||||||
|
var startHtml = SiteRenderer.Render(snap)["index.html"];
|
||||||
|
Assert.Contains("impressum/index.html", startHtml);
|
||||||
|
Assert.Contains("datenschutz/index.html", startHtml);
|
||||||
|
// links in footer, not nav
|
||||||
|
Assert.Contains("site-footer-link", startHtml);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Footer_links_use_correct_relative_path_from_subpage()
|
||||||
|
{
|
||||||
|
var snap = MakeSnapshot(
|
||||||
|
("start", "Startseite", Blocks()),
|
||||||
|
("kontakt", "Kontakt", Blocks()));
|
||||||
|
var kontaktHtml = SiteRenderer.Render(snap)["kontakt/index.html"];
|
||||||
|
// subpage needs ../ prefix for footer links
|
||||||
|
Assert.Contains("../impressum/index.html", kontaktHtml);
|
||||||
|
Assert.Contains("../datenschutz/index.html", kontaktHtml);
|
||||||
|
}
|
||||||
|
|
||||||
// ── Markdown tests ───────────────────────────────────────────────────────
|
// ── Markdown tests ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
|
|||||||
@@ -193,6 +193,15 @@ public class ApplicationContext : DbContext
|
|||||||
(6, "kontakt", "Kontakt"),
|
(6, "kontakt", "Kontakt"),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// WEB-6a: footer-only legal pages (not in main nav)
|
||||||
|
(int n, string Slug, string Title, string BodyMd)[] legalPages =
|
||||||
|
{
|
||||||
|
(7, "impressum", "Impressum",
|
||||||
|
"**Angaben gemäß § 5 TMG**\\n\\nSeitenbetreiber: [Name und vollständige Adresse eintragen]\\n\\nE-Mail: [E-Mail-Adresse eintragen]\\n\\n---\\n\\n*Diese Seite wird vom Seitenbetreiber noch vervollständigt.*"),
|
||||||
|
(8, "datenschutz", "Datenschutz",
|
||||||
|
"**Datenschutzerklärung**\\n\\nDiese Webseite dient der Vorstellung unserer Rennmauszucht. Es werden keine personenbezogenen Daten gespeichert oder weitergegeben.\\n\\nBei datenschutzbezogenen Fragen: [E-Mail-Adresse eintragen]\\n\\n---\\n\\n*Diese Seite wird vom Seitenbetreiber noch vervollständigt.*"),
|
||||||
|
};
|
||||||
|
|
||||||
var pageRows = new List<Page>();
|
var pageRows = new List<Page>();
|
||||||
var blockRows = new List<Block>();
|
var blockRows = new List<Block>();
|
||||||
foreach (var p in pages)
|
foreach (var p in pages)
|
||||||
@@ -211,6 +220,12 @@ public class ApplicationContext : DbContext
|
|||||||
Id = Bid(10), PageId = Pid(3), Order = 1, Type = BlockType.AbgabetiereList,
|
Id = Bid(10), PageId = Pid(3), Order = 1, Type = BlockType.AbgabetiereList,
|
||||||
Data = "{\"mode\":\"auto\",\"intro\":\"\"}",
|
Data = "{\"mode\":\"auto\",\"intro\":\"\"}",
|
||||||
});
|
});
|
||||||
|
foreach (var lp in legalPages)
|
||||||
|
{
|
||||||
|
pageRows.Add(new Page { Id = Pid(lp.n), Slug = lp.Slug, Title = lp.Title, Status = PageStatus.Published });
|
||||||
|
blockRows.Add(new Block { Id = Bid(lp.n), PageId = Pid(lp.n), Order = 0, Type = BlockType.Heading, Data = $"{{\"text\":\"{lp.Title}\",\"level\":1}}" });
|
||||||
|
blockRows.Add(new Block { Id = Bid(lp.n * 10), PageId = Pid(lp.n), Order = 1, Type = BlockType.RichText, Data = $"{{\"markdown\":\"{lp.BodyMd}\"}}" });
|
||||||
|
}
|
||||||
|
|
||||||
modelBuilder.Entity<Page>().HasData(pageRows);
|
modelBuilder.Entity<Page>().HasData(pageRows);
|
||||||
modelBuilder.Entity<Block>().HasData(blockRows);
|
modelBuilder.Entity<Block>().HasData(blockRows);
|
||||||
|
|||||||
@@ -131,7 +131,8 @@ namespace GerbilManagerWebAPI.Cms
|
|||||||
private static string RenderAbgabetiereList(JsonObject d)
|
private static string RenderAbgabetiereList(JsonObject d)
|
||||||
{
|
{
|
||||||
var intro = Str(d["intro"]);
|
var intro = Str(d["intro"]);
|
||||||
var animals = d["animals"] as JsonArray ?? [];
|
var animals = (d["animals"] as JsonArray ?? [])
|
||||||
|
.OfType<JsonObject>().ToList();
|
||||||
var sb = new StringBuilder("\n<section class=\"cms-abgabe\">\n");
|
var sb = new StringBuilder("\n<section class=\"cms-abgabe\">\n");
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(intro))
|
if (!string.IsNullOrWhiteSpace(intro))
|
||||||
@@ -140,39 +141,80 @@ namespace GerbilManagerWebAPI.Cms
|
|||||||
if (animals.Count == 0)
|
if (animals.Count == 0)
|
||||||
{
|
{
|
||||||
sb.Append(" <p class=\"cms-abgabe-empty\">Zurzeit stehen keine Tiere zur Abgabe bereit.</p>\n");
|
sb.Append(" <p class=\"cms-abgabe-empty\">Zurzeit stehen keine Tiere zur Abgabe bereit.</p>\n");
|
||||||
|
sb.Append("</section>\n");
|
||||||
|
return sb.ToString();
|
||||||
}
|
}
|
||||||
else
|
|
||||||
|
// Group by Becken name; null group → solo card keyed by animal name
|
||||||
|
var groups = animals
|
||||||
|
.GroupBy(a => Str(a["group"]) ?? ("__solo__" + (Str(a["name"]) ?? "")))
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
sb.Append(" <div class=\"cms-group-list\">\n");
|
||||||
|
foreach (var grp in groups)
|
||||||
{
|
{
|
||||||
sb.Append(" <div class=\"cms-animal-grid\">\n");
|
var isSolo = grp.Key.StartsWith("__solo__", StringComparison.Ordinal);
|
||||||
foreach (var node in animals)
|
var groupName = isSolo ? null : grp.Key;
|
||||||
|
var memberList = grp.ToList();
|
||||||
|
|
||||||
|
// Determine group gender label
|
||||||
|
var genders = memberList.Select(a => Str(a["gender"]) ?? "unknown").Distinct().ToList();
|
||||||
|
string genderLabel = genders.Count == 1
|
||||||
|
? genders[0] switch { "female" => "Weibchen", "male" => "Männchen", _ => "" }
|
||||||
|
: genders.Any(g => g != "unknown") ? "Gemischt" : "";
|
||||||
|
|
||||||
|
sb.Append(" <article class=\"cms-group-card\">\n");
|
||||||
|
sb.Append(" <div class=\"cms-group-header\">\n");
|
||||||
|
if (!string.IsNullOrEmpty(groupName))
|
||||||
|
sb.Append($" <h3 class=\"cms-group-name\">{H(groupName)}</h3>\n");
|
||||||
|
var metaParts = new List<string>();
|
||||||
|
if (!string.IsNullOrEmpty(genderLabel)) metaParts.Add(H(genderLabel));
|
||||||
|
metaParts.Add("<span class=\"cms-badge cms-badge--free\">Verfügbar</span>");
|
||||||
|
sb.Append($" <span class=\"cms-group-meta\">{string.Join(" · ", metaParts)}</span>\n");
|
||||||
|
sb.Append(" </div>\n");
|
||||||
|
|
||||||
|
// Gallery: first photo of each animal in the group
|
||||||
|
var gallery = memberList
|
||||||
|
.Select(a => (
|
||||||
|
url: (a["photos"] as JsonArray ?? []).FirstOrDefault()?.GetValue<string>(),
|
||||||
|
name: Str(a["name"]) ?? ""))
|
||||||
|
.Where(x => !string.IsNullOrEmpty(x.url))
|
||||||
|
.ToList();
|
||||||
|
if (gallery.Count > 0)
|
||||||
{
|
{
|
||||||
if (node is not JsonObject a) continue;
|
sb.Append(" <div class=\"cms-group-gallery\">\n");
|
||||||
var name = H(Str(a["name"]) ?? "");
|
foreach (var (url, name) in gallery)
|
||||||
var farbe = H(Str(a["farbschlag"]) ?? "");
|
sb.Append($" <img src=\"{H(url)}\" alt=\"{H(name)}\" loading=\"lazy\" class=\"cms-group-photo\">\n");
|
||||||
var group = Str(a["group"]);
|
|
||||||
var saleText = Str(a["aiSaleText"]);
|
|
||||||
var photos = a["photos"] as JsonArray ?? [];
|
|
||||||
|
|
||||||
sb.Append(" <article class=\"cms-animal-card\">\n");
|
|
||||||
|
|
||||||
// Profile photo (first photo)
|
|
||||||
var firstPhoto = photos.FirstOrDefault()?.GetValue<string>();
|
|
||||||
if (!string.IsNullOrEmpty(firstPhoto))
|
|
||||||
sb.Append($" <img class=\"cms-animal-photo\" src=\"{H(firstPhoto)}\" alt=\"{name}\" loading=\"lazy\">\n");
|
|
||||||
|
|
||||||
sb.Append(" <div class=\"cms-animal-info\">\n");
|
|
||||||
sb.Append($" <h3 class=\"cms-animal-name\">{name}</h3>\n");
|
|
||||||
if (!string.IsNullOrEmpty(farbe))
|
|
||||||
sb.Append($" <p class=\"cms-animal-farbe\">{farbe}</p>\n");
|
|
||||||
if (!string.IsNullOrWhiteSpace(group))
|
|
||||||
sb.Append($" <p class=\"cms-animal-group\">Gruppe: {H(group)}</p>\n");
|
|
||||||
if (!string.IsNullOrWhiteSpace(saleText))
|
|
||||||
sb.Append($" <p class=\"cms-animal-text\">{H(saleText)}</p>\n");
|
|
||||||
sb.Append(" </div>\n");
|
sb.Append(" </div>\n");
|
||||||
sb.Append(" </article>\n");
|
|
||||||
}
|
}
|
||||||
sb.Append(" </div>\n");
|
|
||||||
|
// Per-animal detail rows
|
||||||
|
sb.Append(" <ul class=\"cms-animal-list\">\n");
|
||||||
|
foreach (var a in memberList)
|
||||||
|
{
|
||||||
|
var name = H(Str(a["name"]) ?? "");
|
||||||
|
var farbe = Str(a["farbschlag"]);
|
||||||
|
var dob = Str(a["dateOfBirth"]);
|
||||||
|
var note = Str(a["characterNote"]);
|
||||||
|
var saleText = Str(a["aiSaleText"]);
|
||||||
|
|
||||||
|
sb.Append(" <li class=\"cms-animal-row\">\n");
|
||||||
|
sb.Append($" <span class=\"cms-animal-name\">{name}</span>\n");
|
||||||
|
var details = new List<string>();
|
||||||
|
if (!string.IsNullOrEmpty(farbe)) details.Add(H(farbe));
|
||||||
|
if (!string.IsNullOrEmpty(dob) && DateOnly.TryParse(dob, out var dob2))
|
||||||
|
details.Add($"* {dob2.ToString("MMMM yyyy", System.Globalization.CultureInfo.GetCultureInfo("de-DE"))}");
|
||||||
|
if (details.Count > 0)
|
||||||
|
sb.Append($" <span class=\"cms-animal-detail\">{string.Join(" · ", details)}</span>\n");
|
||||||
|
var personalityText = note ?? saleText;
|
||||||
|
if (!string.IsNullOrWhiteSpace(personalityText))
|
||||||
|
sb.Append($" <span class=\"cms-animal-note\">{H(personalityText)}</span>\n");
|
||||||
|
sb.Append(" </li>\n");
|
||||||
|
}
|
||||||
|
sb.Append(" </ul>\n");
|
||||||
|
sb.Append(" </article>\n");
|
||||||
}
|
}
|
||||||
|
sb.Append(" </div>\n");
|
||||||
|
|
||||||
sb.Append("</section>\n");
|
sb.Append("</section>\n");
|
||||||
return sb.ToString();
|
return sb.ToString();
|
||||||
@@ -212,6 +254,10 @@ namespace GerbilManagerWebAPI.Cms
|
|||||||
</main>
|
</main>
|
||||||
<footer class="site-footer">
|
<footer class="site-footer">
|
||||||
<p>© {H(SiteName)} — Mongolische Rennmäuse</p>
|
<p>© {H(SiteName)} — Mongolische Rennmäuse</p>
|
||||||
|
<nav class="site-footer-nav" aria-label="Rechtliches">
|
||||||
|
<a href="{navBase}impressum/index.html" class="site-footer-link">Impressum</a>
|
||||||
|
<a href="{navBase}datenschutz/index.html" class="site-footer-link">Datenschutz</a>
|
||||||
|
</nav>
|
||||||
</footer>
|
</footer>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -228,6 +274,8 @@ namespace GerbilManagerWebAPI.Cms
|
|||||||
["abgabebedingungen"] = "Abgabebedingungen",
|
["abgabebedingungen"] = "Abgabebedingungen",
|
||||||
["farben-genetik"] = "Farben & Genetik",
|
["farben-genetik"] = "Farben & Genetik",
|
||||||
["kontakt"] = "Kontakt",
|
["kontakt"] = "Kontakt",
|
||||||
|
["impressum"] = "Impressum",
|
||||||
|
["datenschutz"] = "Datenschutz",
|
||||||
};
|
};
|
||||||
|
|
||||||
var sb = new StringBuilder();
|
var sb = new StringBuilder();
|
||||||
@@ -336,121 +384,256 @@ namespace GerbilManagerWebAPI.Cms
|
|||||||
internal static string SiteCss() => """
|
internal static string SiteCss() => """
|
||||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||||
:root {
|
:root {
|
||||||
--color-bg: #fdf8f3;
|
--color-bg: #fdf7f0;
|
||||||
--color-surface: #fff;
|
--color-surface: #fff;
|
||||||
--color-text: #2c2c2c;
|
--color-surface-warm: #fef9f4;
|
||||||
--color-muted: #6b6b6b;
|
--color-text: #2a2118;
|
||||||
--color-accent: #c0392b;
|
--color-muted: #7a6d62;
|
||||||
--color-border: #e0d8d0;
|
--color-accent: #b5331a;
|
||||||
--font-body: system-ui, sans-serif;
|
--color-accent-hover: #8f2815;
|
||||||
--max-w: 860px;
|
--color-accent-soft: #fdf0ed;
|
||||||
|
--color-border: #e8ddd4;
|
||||||
|
--color-badge-free: #2d7a4a;
|
||||||
|
--color-badge-free-bg: #e6f4ec;
|
||||||
|
--font-body: Georgia, "Times New Roman", serif;
|
||||||
|
--font-ui: system-ui, -apple-system, sans-serif;
|
||||||
|
--max-w: 900px;
|
||||||
|
--radius: 10px;
|
||||||
}
|
}
|
||||||
body {
|
body {
|
||||||
font-family: var(--font-body);
|
font-family: var(--font-body);
|
||||||
background: var(--color-bg);
|
background: var(--color-bg);
|
||||||
color: var(--color-text);
|
color: var(--color-text);
|
||||||
line-height: 1.65;
|
line-height: 1.7;
|
||||||
|
font-size: 1rem;
|
||||||
}
|
}
|
||||||
a { color: var(--color-accent); text-decoration: none; }
|
a { color: var(--color-accent); text-decoration: none; }
|
||||||
a:hover { text-decoration: underline; }
|
a:hover { text-decoration: underline; color: var(--color-accent-hover); }
|
||||||
img { max-width: 100%; height: auto; display: block; }
|
img { max-width: 100%; height: auto; display: block; }
|
||||||
|
p { margin-bottom: .75rem; }
|
||||||
|
|
||||||
/* ── Header ── */
|
/* ── Header ── */
|
||||||
.site-header {
|
.site-header {
|
||||||
background: var(--color-surface);
|
background: var(--color-surface);
|
||||||
border-bottom: 1px solid var(--color-border);
|
border-bottom: 2px solid var(--color-border);
|
||||||
padding: .75rem 1rem;
|
padding: 0 1rem;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
align-items: stretch;
|
||||||
align-items: center;
|
gap: 0;
|
||||||
gap: .5rem 1.5rem;
|
min-height: 56px;
|
||||||
}
|
}
|
||||||
.site-logo {
|
.site-logo {
|
||||||
|
font-family: var(--font-body);
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
font-size: 1.1rem;
|
font-size: 1.15rem;
|
||||||
color: var(--color-text);
|
color: var(--color-text);
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
display: flex;
|
||||||
.site-nav { display: flex; flex-wrap: wrap; gap: .25rem .75rem; }
|
align-items: center;
|
||||||
.site-nav-link {
|
padding-right: 1.5rem;
|
||||||
font-size: .9rem;
|
border-right: 1px solid var(--color-border);
|
||||||
color: var(--color-muted);
|
|
||||||
padding: .2rem .4rem;
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
.site-nav-link:hover, .site-nav-link[aria-current="page"] {
|
|
||||||
color: var(--color-accent);
|
|
||||||
background: #fef0ee;
|
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.site-logo:hover { color: var(--color-accent); text-decoration: none; }
|
||||||
|
.site-nav {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0;
|
||||||
|
overflow-x: auto;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
scrollbar-width: none;
|
||||||
|
padding-left: .5rem;
|
||||||
|
}
|
||||||
|
.site-nav::-webkit-scrollbar { display: none; }
|
||||||
|
.site-nav-link {
|
||||||
|
font-family: var(--font-ui);
|
||||||
|
font-size: .875rem;
|
||||||
|
color: var(--color-muted);
|
||||||
|
padding: .5rem .65rem;
|
||||||
|
border-radius: 6px;
|
||||||
|
white-space: nowrap;
|
||||||
|
transition: color .15s, background .15s;
|
||||||
|
}
|
||||||
|
.site-nav-link:hover { color: var(--color-accent); background: var(--color-accent-soft); text-decoration: none; }
|
||||||
|
.site-nav-link[aria-current="page"] {
|
||||||
|
color: var(--color-accent);
|
||||||
|
background: var(--color-accent-soft);
|
||||||
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Main ── */
|
/* ── Main ── */
|
||||||
.site-main {
|
.site-main {
|
||||||
max-width: var(--max-w);
|
max-width: var(--max-w);
|
||||||
margin: 2rem auto;
|
margin: 2.5rem auto;
|
||||||
padding: 0 1rem 3rem;
|
padding: 0 1rem 4rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Footer ── */
|
/* ── Footer ── */
|
||||||
.site-footer {
|
.site-footer {
|
||||||
border-top: 1px solid var(--color-border);
|
background: var(--color-surface);
|
||||||
padding: 1.5rem 1rem;
|
border-top: 2px solid var(--color-border);
|
||||||
|
padding: 1.75rem 1rem;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
font-family: var(--font-ui);
|
||||||
font-size: .85rem;
|
font-size: .85rem;
|
||||||
color: var(--color-muted);
|
color: var(--color-muted);
|
||||||
}
|
}
|
||||||
|
.site-footer-nav { margin-top: .6rem; display: flex; justify-content: center; gap: 1.25rem; }
|
||||||
|
.site-footer-link { color: var(--color-muted); font-size: .8rem; }
|
||||||
|
.site-footer-link:hover { color: var(--color-accent); }
|
||||||
|
|
||||||
/* ── CMS blocks ── */
|
/* ── CMS blocks ── */
|
||||||
.cms-heading { margin: 1.5rem 0 .5rem; }
|
.cms-heading { margin: 2rem 0 .6rem; line-height: 1.3; }
|
||||||
h1.cms-heading { font-size: 1.8rem; }
|
h1.cms-heading { font-size: 2rem; margin-top: 0; }
|
||||||
h2.cms-heading { font-size: 1.4rem; }
|
h2.cms-heading { font-size: 1.5rem; }
|
||||||
|
h3.cms-heading { font-size: 1.2rem; }
|
||||||
.cms-richtext { margin: 1rem 0; }
|
.cms-richtext { margin: 1rem 0; }
|
||||||
.cms-richtext p { margin-bottom: .75rem; }
|
.cms-richtext p { margin-bottom: .75rem; }
|
||||||
.cms-richtext ul, .cms-richtext ol { margin: .5rem 0 .75rem 1.5rem; }
|
.cms-richtext ul, .cms-richtext ol { margin: .5rem 0 .75rem 1.5rem; }
|
||||||
.cms-richtext li { margin-bottom: .3rem; }
|
.cms-richtext li { margin-bottom: .35rem; }
|
||||||
.cms-image { margin: 1.5rem 0; }
|
.cms-richtext hr { border: none; border-top: 1px solid var(--color-border); margin: 1.5rem 0; }
|
||||||
.cms-image img { border-radius: 6px; }
|
.cms-image { margin: 1.75rem 0; }
|
||||||
|
.cms-image img { border-radius: var(--radius); box-shadow: 0 2px 8px rgba(0,0,0,.08); }
|
||||||
.cms-gallery {
|
.cms-gallery {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
|
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
|
||||||
gap: .75rem;
|
gap: .75rem;
|
||||||
margin: 1.5rem 0;
|
margin: 1.75rem 0;
|
||||||
}
|
}
|
||||||
.cms-gallery-item img { border-radius: 4px; aspect-ratio: 1; object-fit: cover; }
|
.cms-gallery-item img { border-radius: 6px; aspect-ratio: 1; object-fit: cover; }
|
||||||
.cms-contact {
|
.cms-contact {
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
|
font-family: var(--font-ui);
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: .4rem;
|
gap: .5rem;
|
||||||
margin: 1rem 0;
|
margin: 1.25rem 0;
|
||||||
|
background: var(--color-surface-warm);
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
padding: 1.25rem;
|
||||||
}
|
}
|
||||||
.cms-contact-row { display: flex; gap: .5rem; align-items: flex-start; }
|
.cms-contact-row { display: flex; gap: .6rem; align-items: flex-start; }
|
||||||
|
|
||||||
/* ── Abgabetiere ── */
|
/* ── Abgabetiere — group cards ── */
|
||||||
.cms-abgabe { margin: 1rem 0; }
|
.cms-abgabe { margin: 1.25rem 0; }
|
||||||
.cms-abgabe-intro { margin-bottom: 1.25rem; font-size: 1.05rem; }
|
.cms-abgabe-intro { margin-bottom: 1.5rem; font-size: 1.05rem; }
|
||||||
.cms-abgabe-empty { color: var(--color-muted); font-style: italic; }
|
.cms-abgabe-empty { color: var(--color-muted); font-style: italic; }
|
||||||
.cms-animal-grid {
|
|
||||||
display: grid;
|
.cms-group-list { display: flex; flex-direction: column; gap: 2rem; }
|
||||||
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
|
|
||||||
gap: 1.25rem;
|
.cms-group-card {
|
||||||
}
|
|
||||||
.cms-animal-card {
|
|
||||||
background: var(--color-surface);
|
background: var(--color-surface);
|
||||||
border: 1px solid var(--color-border);
|
border: 1px solid var(--color-border);
|
||||||
border-radius: 8px;
|
border-radius: var(--radius);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
box-shadow: 0 2px 6px rgba(0,0,0,.06);
|
||||||
|
}
|
||||||
|
.cms-group-header {
|
||||||
|
padding: 1rem 1.25rem .75rem;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
gap: .5rem .75rem;
|
||||||
|
border-bottom: 1px solid var(--color-border);
|
||||||
|
background: var(--color-surface-warm);
|
||||||
|
}
|
||||||
|
.cms-group-name {
|
||||||
|
font-family: var(--font-body);
|
||||||
|
font-size: 1.2rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--color-text);
|
||||||
|
}
|
||||||
|
.cms-group-meta {
|
||||||
|
font-family: var(--font-ui);
|
||||||
|
font-size: .875rem;
|
||||||
|
color: var(--color-muted);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: .5rem;
|
||||||
}
|
}
|
||||||
.cms-animal-photo { width: 100%; aspect-ratio: 4/3; object-fit: cover; }
|
|
||||||
.cms-animal-info { padding: .875rem; }
|
|
||||||
.cms-animal-name { font-size: 1.1rem; font-weight: 600; margin-bottom: .25rem; }
|
|
||||||
.cms-animal-farbe { font-size: .9rem; color: var(--color-muted); margin-bottom: .25rem; }
|
|
||||||
.cms-animal-group { font-size: .85rem; color: var(--color-muted); }
|
|
||||||
.cms-animal-text { font-size: .9rem; margin-top: .5rem; }
|
|
||||||
|
|
||||||
@media (max-width: 480px) {
|
/* availability badge */
|
||||||
h1.cms-heading { font-size: 1.4rem; }
|
.cms-badge {
|
||||||
.cms-animal-grid { grid-template-columns: 1fr; }
|
display: inline-block;
|
||||||
|
font-size: .75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
font-family: var(--font-ui);
|
||||||
|
padding: .2rem .55rem;
|
||||||
|
border-radius: 20px;
|
||||||
|
letter-spacing: .02em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
.cms-badge--free {
|
||||||
|
background: var(--color-badge-free-bg);
|
||||||
|
color: var(--color-badge-free);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* group photo strip */
|
||||||
|
.cms-group-gallery {
|
||||||
|
display: flex;
|
||||||
|
gap: .375rem;
|
||||||
|
overflow-x: auto;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
scrollbar-width: none;
|
||||||
|
padding: .75rem 1.25rem;
|
||||||
|
background: #f5ede4;
|
||||||
|
}
|
||||||
|
.cms-group-gallery::-webkit-scrollbar { display: none; }
|
||||||
|
.cms-group-photo {
|
||||||
|
width: 140px;
|
||||||
|
height: 140px;
|
||||||
|
object-fit: cover;
|
||||||
|
border-radius: 6px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* animal detail list */
|
||||||
|
.cms-animal-list {
|
||||||
|
list-style: none;
|
||||||
|
padding: .75rem 1.25rem 1.25rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: .875rem;
|
||||||
|
}
|
||||||
|
.cms-animal-row {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: .15rem;
|
||||||
|
padding-bottom: .875rem;
|
||||||
|
border-bottom: 1px solid var(--color-border);
|
||||||
|
}
|
||||||
|
.cms-animal-row:last-child { border-bottom: none; padding-bottom: 0; }
|
||||||
|
.cms-animal-name {
|
||||||
|
font-family: var(--font-body);
|
||||||
|
font-size: 1.05rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--color-text);
|
||||||
|
}
|
||||||
|
.cms-animal-detail {
|
||||||
|
font-family: var(--font-ui);
|
||||||
|
font-size: .875rem;
|
||||||
|
color: var(--color-muted);
|
||||||
|
}
|
||||||
|
.cms-animal-note {
|
||||||
|
font-style: italic;
|
||||||
|
font-size: .925rem;
|
||||||
|
color: var(--color-text);
|
||||||
|
margin-top: .1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Responsive ── */
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
.site-header { padding: 0 .75rem; min-height: 50px; }
|
||||||
|
.site-logo { font-size: 1rem; padding-right: 1rem; }
|
||||||
|
h1.cms-heading { font-size: 1.5rem; }
|
||||||
|
.site-main { margin-top: 1.5rem; }
|
||||||
|
.cms-group-photo { width: 110px; height: 110px; }
|
||||||
|
}
|
||||||
|
@media (min-width: 700px) {
|
||||||
|
.cms-animal-list { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 1rem; }
|
||||||
|
.cms-animal-row { border-bottom: none; padding-bottom: 0; border: 1px solid var(--color-border); border-radius: 8px; padding: .75rem; }
|
||||||
}
|
}
|
||||||
""";
|
""";
|
||||||
|
|
||||||
|
|||||||
@@ -82,6 +82,9 @@ namespace GerbilManagerWebAPI.Cms
|
|||||||
{
|
{
|
||||||
g.Id,
|
g.Id,
|
||||||
g.Name,
|
g.Name,
|
||||||
|
g.Gender,
|
||||||
|
g.DateOfBirth,
|
||||||
|
g.CharacterNote,
|
||||||
Farbschlag = g.ColorVariety != null ? g.ColorVariety.Name : null,
|
Farbschlag = g.ColorVariety != null ? g.ColorVariety.Name : null,
|
||||||
// group = Becken (enclosure) name — matches how the Abgabe composer
|
// group = Becken (enclosure) name — matches how the Abgabe composer
|
||||||
// groups ForSale animals by enclosure-mates (god ruling). null = single.
|
// groups ForSale animals by enclosure-mates (god ruling). null = single.
|
||||||
@@ -109,6 +112,9 @@ namespace GerbilManagerWebAPI.Cms
|
|||||||
arr.Add(new JsonObject
|
arr.Add(new JsonObject
|
||||||
{
|
{
|
||||||
["name"] = a.Name,
|
["name"] = a.Name,
|
||||||
|
["gender"] = a.Gender.ToString(),
|
||||||
|
["dateOfBirth"] = a.DateOfBirth?.ToString("yyyy-MM-dd"),
|
||||||
|
["characterNote"] = a.CharacterNote,
|
||||||
["farbschlag"] = a.Farbschlag,
|
["farbschlag"] = a.Farbschlag,
|
||||||
["group"] = a.Group,
|
["group"] = a.Group,
|
||||||
["photos"] = photoArr,
|
["photos"] = photoArr,
|
||||||
|
|||||||
1348
GerbilManagerWebAPI/Migrations/20260606131032_AddImpressumDatenschutzPages.Designer.cs
generated
Normal file
1348
GerbilManagerWebAPI/Migrations/20260606131032_AddImpressumDatenschutzPages.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,71 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
#pragma warning disable CA1814 // Prefer jagged arrays over multidimensional
|
||||||
|
|
||||||
|
namespace GerbilManagerWebAPI.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddImpressumDatenschutzPages : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.InsertData(
|
||||||
|
table: "Pages",
|
||||||
|
columns: new[] { "Id", "SeoDescription", "Slug", "Status", "Title" },
|
||||||
|
values: new object[,]
|
||||||
|
{
|
||||||
|
{ new Guid("51720001-0000-0000-0000-000000000007"), null, "impressum", "Published", "Impressum" },
|
||||||
|
{ new Guid("51720001-0000-0000-0000-000000000008"), null, "datenschutz", "Published", "Datenschutz" }
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.InsertData(
|
||||||
|
table: "Blocks",
|
||||||
|
columns: new[] { "Id", "Data", "Order", "PageId", "Type" },
|
||||||
|
values: new object[,]
|
||||||
|
{
|
||||||
|
{ new Guid("51720002-0000-0000-0000-000000000007"), "{\"text\":\"Impressum\",\"level\":1}", 0, new Guid("51720001-0000-0000-0000-000000000007"), "Heading" },
|
||||||
|
{ new Guid("51720002-0000-0000-0000-000000000008"), "{\"text\":\"Datenschutz\",\"level\":1}", 0, new Guid("51720001-0000-0000-0000-000000000008"), "Heading" },
|
||||||
|
{ new Guid("51720002-0000-0000-0000-000000000070"), "{\"markdown\":\"**Angaben gemäß § 5 TMG**\\n\\nSeitenbetreiber: [Name und vollständige Adresse eintragen]\\n\\nE-Mail: [E-Mail-Adresse eintragen]\\n\\n---\\n\\n*Diese Seite wird vom Seitenbetreiber noch vervollständigt.*\"}", 1, new Guid("51720001-0000-0000-0000-000000000007"), "RichText" },
|
||||||
|
{ new Guid("51720002-0000-0000-0000-000000000080"), "{\"markdown\":\"**Datenschutzerklärung**\\n\\nDiese Webseite dient der Vorstellung unserer Rennmauszucht. Es werden keine personenbezogenen Daten gespeichert oder weitergegeben.\\n\\nBei datenschutzbezogenen Fragen: [E-Mail-Adresse eintragen]\\n\\n---\\n\\n*Diese Seite wird vom Seitenbetreiber noch vervollständigt.*\"}", 1, new Guid("51720001-0000-0000-0000-000000000008"), "RichText" }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DeleteData(
|
||||||
|
table: "Blocks",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: new Guid("51720002-0000-0000-0000-000000000007"));
|
||||||
|
|
||||||
|
migrationBuilder.DeleteData(
|
||||||
|
table: "Blocks",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: new Guid("51720002-0000-0000-0000-000000000008"));
|
||||||
|
|
||||||
|
migrationBuilder.DeleteData(
|
||||||
|
table: "Blocks",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: new Guid("51720002-0000-0000-0000-000000000070"));
|
||||||
|
|
||||||
|
migrationBuilder.DeleteData(
|
||||||
|
table: "Blocks",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: new Guid("51720002-0000-0000-0000-000000000080"));
|
||||||
|
|
||||||
|
migrationBuilder.DeleteData(
|
||||||
|
table: "Pages",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: new Guid("51720001-0000-0000-0000-000000000007"));
|
||||||
|
|
||||||
|
migrationBuilder.DeleteData(
|
||||||
|
table: "Pages",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: new Guid("51720001-0000-0000-0000-000000000008"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -103,6 +103,38 @@ namespace GerbilManagerWebAPI.Migrations
|
|||||||
Order = 1,
|
Order = 1,
|
||||||
PageId = new Guid("51720001-0000-0000-0000-000000000003"),
|
PageId = new Guid("51720001-0000-0000-0000-000000000003"),
|
||||||
Type = "AbgabetiereList"
|
Type = "AbgabetiereList"
|
||||||
|
},
|
||||||
|
new
|
||||||
|
{
|
||||||
|
Id = new Guid("51720002-0000-0000-0000-000000000007"),
|
||||||
|
Data = "{\"text\":\"Impressum\",\"level\":1}",
|
||||||
|
Order = 0,
|
||||||
|
PageId = new Guid("51720001-0000-0000-0000-000000000007"),
|
||||||
|
Type = "Heading"
|
||||||
|
},
|
||||||
|
new
|
||||||
|
{
|
||||||
|
Id = new Guid("51720002-0000-0000-0000-000000000070"),
|
||||||
|
Data = "{\"markdown\":\"**Angaben gemäß § 5 TMG**\\n\\nSeitenbetreiber: [Name und vollständige Adresse eintragen]\\n\\nE-Mail: [E-Mail-Adresse eintragen]\\n\\n---\\n\\n*Diese Seite wird vom Seitenbetreiber noch vervollständigt.*\"}",
|
||||||
|
Order = 1,
|
||||||
|
PageId = new Guid("51720001-0000-0000-0000-000000000007"),
|
||||||
|
Type = "RichText"
|
||||||
|
},
|
||||||
|
new
|
||||||
|
{
|
||||||
|
Id = new Guid("51720002-0000-0000-0000-000000000008"),
|
||||||
|
Data = "{\"text\":\"Datenschutz\",\"level\":1}",
|
||||||
|
Order = 0,
|
||||||
|
PageId = new Guid("51720001-0000-0000-0000-000000000008"),
|
||||||
|
Type = "Heading"
|
||||||
|
},
|
||||||
|
new
|
||||||
|
{
|
||||||
|
Id = new Guid("51720002-0000-0000-0000-000000000080"),
|
||||||
|
Data = "{\"markdown\":\"**Datenschutzerklärung**\\n\\nDiese Webseite dient der Vorstellung unserer Rennmauszucht. Es werden keine personenbezogenen Daten gespeichert oder weitergegeben.\\n\\nBei datenschutzbezogenen Fragen: [E-Mail-Adresse eintragen]\\n\\n---\\n\\n*Diese Seite wird vom Seitenbetreiber noch vervollständigt.*\"}",
|
||||||
|
Order = 1,
|
||||||
|
PageId = new Guid("51720001-0000-0000-0000-000000000008"),
|
||||||
|
Type = "RichText"
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -989,6 +1021,20 @@ namespace GerbilManagerWebAPI.Migrations
|
|||||||
Slug = "kontakt",
|
Slug = "kontakt",
|
||||||
Status = "Published",
|
Status = "Published",
|
||||||
Title = "Kontakt"
|
Title = "Kontakt"
|
||||||
|
},
|
||||||
|
new
|
||||||
|
{
|
||||||
|
Id = new Guid("51720001-0000-0000-0000-000000000007"),
|
||||||
|
Slug = "impressum",
|
||||||
|
Status = "Published",
|
||||||
|
Title = "Impressum"
|
||||||
|
},
|
||||||
|
new
|
||||||
|
{
|
||||||
|
Id = new Guid("51720001-0000-0000-0000-000000000008"),
|
||||||
|
Slug = "datenschutz",
|
||||||
|
Status = "Published",
|
||||||
|
Title = "Datenschutz"
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ gelisteten Konflikt-Tiere + Tiere mit Sonder-Kürzeln warten in Quarantäne —
|
|||||||
| ~~C3~~ | ✅ **BEANTWORTET** (2026-06-06): Ja, automatisch zusammenführen — **aber nur wenn auch der Zuchtname gleich ist** (Name + Geburtsdatum + Zuchtname = dasselbe Tier). Wird in die Dedup-Regel eingebaut. (Hinweis: der Extraktor fand bisher 0 Fälle mit gleichem Name+Datum aber verschiedenem Zuchtnamen, also ändert sich an den bestehenden Zusammenführungen nichts — die Regel ist die Absicherung.) | — erledigt |
|
| ~~C3~~ | ✅ **BEANTWORTET** (2026-06-06): Ja, automatisch zusammenführen — **aber nur wenn auch der Zuchtname gleich ist** (Name + Geburtsdatum + Zuchtname = dasselbe Tier). Wird in die Dedup-Regel eingebaut. (Hinweis: der Extraktor fand bisher 0 Fälle mit gleichem Name+Datum aber verschiedenem Zuchtnamen, also ändert sich an den bestehenden Zusammenführungen nichts — die Regel ist die Absicherung.) | — erledigt |
|
||||||
| ~~C4~~ | ✅ **BEANTWORTET** (2026-06-06): Ja, **Wurfchronik Teil 2 existiert** — wird gerade überarbeitet, kommt später. Der Importer ist pro Datei wiederholbar (idempotent), also einfach die Datei schicken, sobald fertig → Michael importiert sie nach (keine Doppelungen). | ⏳ Datei folgt, wenn überarbeitet |
|
| ~~C4~~ | ✅ **BEANTWORTET** (2026-06-06): Ja, **Wurfchronik Teil 2 existiert** — wird gerade überarbeitet, kommt später. Der Importer ist pro Datei wiederholbar (idempotent), also einfach die Datei schicken, sobald fertig → Michael importiert sie nach (keine Doppelungen). | ⏳ Datei folgt, wenn überarbeitet |
|
||||||
| ~~C5~~ | ✅ **BEANTWORTET** (2026-06-06): **Es gibt KEIN „Schwarzschimmel"** — das war ein Fehler in unserem Katalog. Die korrekten Schimmelarten: `efef` → **Orangeschimmel** · `efef pp` → **Rotaugenschimmel** · `efef gg` → **Silberschimmel** · Kombis z. B. `c[chm]c[chm] efef` → **CP-Orangeschimmel**. Michael korrigiert den Katalog (Schwarzschimmel raus, efef = Orangeschimmel). | — erledigt |
|
| ~~C5~~ | ✅ **BEANTWORTET** (2026-06-06): **Es gibt KEIN „Schwarzschimmel"** — das war ein Fehler in unserem Katalog. Die korrekten Schimmelarten: `efef` → **Orangeschimmel** · `efef pp` → **Rotaugenschimmel** · `efef gg` → **Silberschimmel** · Kombis z. B. `c[chm]c[chm] efef` → **CP-Orangeschimmel**. Michael korrigiert den Katalog (Schwarzschimmel raus, efef = Orangeschimmel). | — erledigt |
|
||||||
| C6 | **FAST ERLEDIGT** (Stand 06.06. nachmittags): von den ursprünglich 32 Konflikt-Tieren sind **27 geklärt + geladen** (deine D1–D5-Antworten + Beibehalten-Regel + „genauer gewinnt"-Regel). **Offen sind nur noch die 5 Tiere in D6**: Hanami (Sterbedatum), Big Ben (PP↔Pp), Vance Jr. (Spsp↔spsp), Kazu (3 Loci), Skarlett (Sterbedatum). | nur diese 5 warten noch auf den Import |
|
| C6 | **Die 32 Konflikt-Tiere prüfen** → siehe Abschnitt **D**. | diese 32 Tiere werden erst danach geladen |
|
||||||
| C7 | *(optional)* Was hat dir bei **Renner Pro** gefehlt? Lieblings-Auswertungen? | mögliche neue Funktionen |
|
| C7 | *(optional)* Was hat dir bei **Renner Pro** gefehlt? Lieblings-Auswertungen? | mögliche neue Funktionen |
|
||||||
| ~~C8~~ | ✅ **BEANTWORTET** (2026-06-06): **Himalaya gibt es** — Himalaya = **`A- c[h]c[h]`** (agouti), Hermelin = **`aa c[h]c[h]`** (nicht-agouti). Beide bleiben im Katalog; die Engine unterscheidet bereits korrekt nach A-/aa. — erledigt |
|
| ~~C8~~ | ✅ **BEANTWORTET** (2026-06-06): **Himalaya gibt es** — Himalaya = **`A- c[h]c[h]`** (agouti), Hermelin = **`aa c[h]c[h]`** (nicht-agouti). Beide bleiben im Katalog; die Engine unterscheidet bereits korrekt nach A-/aa. — erledigt |
|
||||||
| ~~C9~~ | ✅ **BEANTWORTET** (2026-06-06): **„CP-Fuchs" ist ein Sammelbegriff** — bei diesen Tieren ist unklar, ob es CP-Polarfuchs, CP-Algierfuchs, CP-Kohlfuchs oder CP-Blaufuchs ist (Tiere sind schneeweiß mit schwarzen Augen; Verpaarungen haben die Gene nicht verraten). Bekannt ist nur: **„CP-Fuchs" = `c[chm]c[chm]`**, **„CP-Fuchs hell" = `c[chm]c[h]`**. **Generelle Regel: das Wort „hell" im Farbschlag-Namen bedeutet immer, dass ein `c[h]` im Gencode steckt** (also `c[chm]c[h]`); ohne „hell" = `c[chm]c[chm]`. Die „-Hell"-Vermutung war richtig ✓; Engine-Update beauftragt (GEN-3g): bei unbekannten Unterscheidungs-Loci bleibt der Sammelbegriff „CP-Fuchs" korrekt. — erledigt |
|
| ~~C9~~ | ✅ **BEANTWORTET** (2026-06-06): **„CP-Fuchs" ist ein Sammelbegriff** — bei diesen Tieren ist unklar, ob es CP-Polarfuchs, CP-Algierfuchs, CP-Kohlfuchs oder CP-Blaufuchs ist (Tiere sind schneeweiß mit schwarzen Augen; Verpaarungen haben die Gene nicht verraten). Bekannt ist nur: **„CP-Fuchs" = `c[chm]c[chm]`**, **„CP-Fuchs hell" = `c[chm]c[h]`**. **Generelle Regel: das Wort „hell" im Farbschlag-Namen bedeutet immer, dass ein `c[h]` im Gencode steckt** (also `c[chm]c[h]`); ohne „hell" = `c[chm]c[chm]`. Die „-Hell"-Vermutung war richtig ✓; Engine-Update beauftragt (GEN-3g): bei unbekannten Unterscheidungs-Loci bleibt der Sammelbegriff „CP-Fuchs" korrekt. — erledigt |
|
||||||
|
|||||||
@@ -179,11 +179,10 @@ describe('Farbschlag catalog', () => {
|
|||||||
expect(match.name).toBe('Unbekannter Farbschlag')
|
expect(match.name).toBe('Unbekannter Farbschlag')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('has the expected catalogue coverage (GEN-3g: 66 after adding CP-*-Hell het variants)', () => {
|
it('has the expected catalogue coverage (GEN-3f: 61 after cchm CP reconciliation)', () => {
|
||||||
// GEN-3f: 73 -> 61 (cchm CP reconciliation).
|
// GEN-3f collapsed the 24 portal cchm colourpoint rows to 12 breeder-named
|
||||||
// GEN-3g: +5 het variants (CP-Agouti/Silberagouti/Algierfuchs/Polarfuchs/Orangeschimmel -Hell),
|
// varieties (Marder/Siam/Zobel/Zobel-Hell + CP-<base>), so 73 -> 61.
|
||||||
// giving 61 + 5 = 66. CP-Fuchs-Hell was already counted.
|
expect(CATALOG_SIZE).toBe(61)
|
||||||
expect(CATALOG_SIZE).toBe(66)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('frozen contract names round-trip to themselves (DB-key guard)', () => {
|
it('frozen contract names round-trip to themselves (DB-key guard)', () => {
|
||||||
@@ -415,12 +414,8 @@ describe('GEN-3e: C-locus colourpoint naming', () => {
|
|||||||
expect(name('AA cchmcchm DD EE GG PP spsp rere')).toBe('CP-Agouti')
|
expect(name('AA cchmcchm DD EE GG PP spsp rere')).toBe('CP-Agouti')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('A- + cchm/ch -> CP-<base colour>-Hell (GEN-3g: het gets -Hell suffix)', () => {
|
it('A- + cchm/ch -> CP-<base colour>', () => {
|
||||||
expect(name('AA cchmch DD EE GG PP spsp rere')).toBe('CP-Agouti-Hell')
|
expect(name('AA cchmch DD EE GG PP spsp rere')).toBe('CP-Agouti')
|
||||||
expect(name('AA cchmch DD EE gg PP spsp rere')).toBe('CP-Silberagouti-Hell')
|
|
||||||
expect(name('AA cchmch DD ee GG PP spsp rere')).toBe('CP-Algierfuchs-Hell')
|
|
||||||
expect(name('AA cchmch DD ee gg PP spsp rere')).toBe('CP-Polarfuchs-Hell')
|
|
||||||
expect(name('AA cchmch dd ee GG PP spsp rere')).toBe('CP-Fuchs-Hell')
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('aa fixed colourpoint names (Marder/Siam/Zobel/Zobel-Hell)', () => {
|
it('aa fixed colourpoint names (Marder/Siam/Zobel/Zobel-Hell)', () => {
|
||||||
@@ -476,50 +471,10 @@ describe('GEN-3f: CP catalog reconciled to the breeder CP- naming (matches her l
|
|||||||
expect(name('AA cchmcchm DD efef GG PP spsp rere')).toBe('CP-Orangeschimmel')
|
expect(name('AA cchmcchm DD efef GG PP spsp rere')).toBe('CP-Orangeschimmel')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('het cchm/ch colourpoints (Siam, Zobel-Hell, CP-*-Hell) resolve to their own names', () => {
|
it('het cchm/ch colourpoints (Siam, Zobel-Hell) resolve to their own names', () => {
|
||||||
const siam = BASE_COLORS.find((e) => e.name === 'Siam')!
|
const siam = BASE_COLORS.find((e) => e.name === 'Siam')!
|
||||||
const zh = BASE_COLORS.find((e) => e.name === 'Zobel-Hell')!
|
const zh = BASE_COLORS.find((e) => e.name === 'Zobel-Hell')!
|
||||||
const cpah = BASE_COLORS.find((e) => e.name === 'CP-Agouti-Hell')!
|
|
||||||
const cpfh = BASE_COLORS.find((e) => e.name === 'CP-Fuchs-Hell')!
|
|
||||||
expect(genotypeToFarbschlag(representativeGenotype(siam))).toBe('Siam')
|
expect(genotypeToFarbschlag(representativeGenotype(siam))).toBe('Siam')
|
||||||
expect(genotypeToFarbschlag(representativeGenotype(zh))).toBe('Zobel-Hell')
|
expect(genotypeToFarbschlag(representativeGenotype(zh))).toBe('Zobel-Hell')
|
||||||
expect(genotypeToFarbschlag(representativeGenotype(cpah))).toBe('CP-Agouti-Hell')
|
|
||||||
expect(genotypeToFarbschlag(representativeGenotype(cpfh))).toBe('CP-Fuchs-Hell')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('GEN-3g: "-Hell" in variety name == cchm/ch het; hom == cchm/cchm', () => {
|
|
||||||
const name = (s: string) => genotypeToFarbschlag(fromDisplayString(s))
|
|
||||||
const has = (n: string) => BASE_COLORS.some((e) => e.name === n)
|
|
||||||
|
|
||||||
it('all new -Hell het entries exist in catalog', () => {
|
|
||||||
for (const n of [
|
|
||||||
'CP-Agouti-Hell', 'CP-Silberagouti-Hell', 'CP-Algierfuchs-Hell',
|
|
||||||
'CP-Polarfuchs-Hell', 'CP-Orangeschimmel-Hell',
|
|
||||||
]) {
|
|
||||||
expect(has(n)).toBe(true)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
it('engine correctly maps het (cchm/ch) to -Hell suffix for all A- bases', () => {
|
|
||||||
expect(name('AA cchmch DD EE GG PP spsp rere')).toBe('CP-Agouti-Hell')
|
|
||||||
expect(name('AA cchmch DD EE gg PP spsp rere')).toBe('CP-Silberagouti-Hell')
|
|
||||||
expect(name('AA cchmch DD ee GG PP spsp rere')).toBe('CP-Algierfuchs-Hell')
|
|
||||||
expect(name('AA cchmch DD ee gg PP spsp rere')).toBe('CP-Polarfuchs-Hell')
|
|
||||||
expect(name('AA cchmch dd ee GG PP spsp rere')).toBe('CP-Fuchs-Hell')
|
|
||||||
expect(name('AA cchmch DD efef GG PP spsp rere')).toBe('CP-Orangeschimmel-Hell')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('hom (cchm/cchm) still maps without -Hell suffix', () => {
|
|
||||||
expect(name('AA cchmcchm DD EE GG PP spsp rere')).toBe('CP-Agouti')
|
|
||||||
expect(name('AA cchmcchm DD EE gg PP spsp rere')).toBe('CP-Silberagouti')
|
|
||||||
expect(name('AA cchmcchm DD efef GG PP spsp rere')).toBe('CP-Orangeschimmel')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('aa non-agouti branch is unchanged (Marder/Siam/Zobel/Zobel-Hell)', () => {
|
|
||||||
expect(name('aa cchmcchm DD EE GG PP spsp rere')).toBe('Marder')
|
|
||||||
expect(name('aa cchmch DD EE GG PP spsp rere')).toBe('Siam')
|
|
||||||
expect(name('aa cchmcchm DD EE gg PP spsp rere')).toBe('Zobel')
|
|
||||||
expect(name('aa cchmch DD EE gg PP spsp rere')).toBe('Zobel-Hell')
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -107,29 +107,27 @@ export const BASE_COLORS: readonly FarbschlagEntry[] = [
|
|||||||
{ name: 'Topas dd', tokens: { A: 'A', C: 'C', D: 'd', E: 'E', G: 'G', P: 'p' }, image: 'topas-dd.jpg' },
|
{ name: 'Topas dd', tokens: { A: 'A', C: 'C', D: 'd', E: 'E', G: 'G', P: 'p' }, image: 'topas-dd.jpg' },
|
||||||
{ name: 'Blaufuchs dd', tokens: { A: 'a', C: 'C', D: 'd', E: 'e', G: 'g', P: 'p' }, image: 'blaufuchs-dd.jpg' },
|
{ name: 'Blaufuchs dd', tokens: { A: 'a', C: 'C', D: 'd', E: 'e', G: 'g', P: 'p' }, image: 'blaufuchs-dd.jpg' },
|
||||||
|
|
||||||
// ── GEN-3f/3g: c^chm colourpoint varieties ──
|
// ── GEN-3f: c^chm colourpoint varieties, reconciled to the breeder's CP- naming ──
|
||||||
// GEN-3f: aa points = marten/sable group (Marder/Siam, +gg Zobel/Zobel-Hell).
|
// The merged GEN-3e colourpointName() rule is authoritative: aa points are the
|
||||||
// GEN-3g (breeder rule): '-Hell' == cchm/ch het; no '-Hell' == cchm/cchm hom.
|
// marten/sable group (Marder/Siam, +gg Zobel/Zobel-Hell — E and D irrelevant);
|
||||||
// A- points: hom -> 'CP-<base>', het -> 'CP-<base>-Hell' (colourpointName()).
|
// A- points take the 'CP-<base colour>' prefix and the '-Hell' shade variants
|
||||||
// CP-Fuchs is a Sammelbegriff (unknown loci); its -Hell het = CP-Fuchs-Hell.
|
// collapse (other loci irrelevant for the CP prefix). These names == the strings
|
||||||
// CP-Blaufuchs (D:d, G:g) still resolves engine-side to 'CP-Fuchs' (dd/gg
|
// in her live data (god: extract animals.json) so the re-import name-matches and
|
||||||
// fox CP has no dedicated base entry); kept for import name-match + hand-pick.
|
// the Farbschlag mismatch hint stops. The het cchm/ch points (Siam, Zobel-Hell,
|
||||||
|
// CP-Fuchs-Hell) use the 'cchm/ch' pair token. The agouti fox/dilute points
|
||||||
|
// (CP-Fuchs/CP-Blaufuchs) resolve through the engine's E-family fallback to
|
||||||
|
// 'CP-Fuchs'; their distinct dropdown names remain for hand-pick + import match.
|
||||||
{ name: 'Marder', tokens: { A: 'a', C: 'cchm', D: 'D', E: 'E', G: 'G', P: 'P' }, image: 'marder.JPG' },
|
{ name: 'Marder', tokens: { A: 'a', C: 'cchm', D: 'D', E: 'E', G: 'G', P: 'P' }, image: 'marder.JPG' },
|
||||||
{ name: 'Siam', tokens: { A: 'a', C: 'cchm/ch', D: 'D', E: 'E', G: 'G', P: 'P' }, image: 'siam-marder-hell.JPG' },
|
{ name: 'Siam', tokens: { A: 'a', C: 'cchm/ch', D: 'D', E: 'E', G: 'G', P: 'P' }, image: 'siam-marder-hell.JPG' },
|
||||||
{ name: 'Zobel-Hell', tokens: { A: 'a', C: 'cchm/ch', D: 'D', E: 'E', G: 'g', P: 'P' }, image: 'zobel-hell.jpg' },
|
{ name: 'Zobel-Hell', tokens: { A: 'a', C: 'cchm/ch', D: 'D', E: 'E', G: 'g', P: 'P' }, image: 'zobel-hell.jpg' },
|
||||||
{ name: 'CP-Agouti', tokens: { A: 'A', C: 'cchm', D: 'D', E: 'E', G: 'G', P: 'P' }, image: 'agouti-cp.jpg' },
|
{ name: 'CP-Agouti', tokens: { A: 'A', C: 'cchm', D: 'D', E: 'E', G: 'G', P: 'P' }, image: 'agouti-cp.jpg' },
|
||||||
{ name: 'CP-Agouti-Hell', tokens: { A: 'A', C: 'cchm/ch', D: 'D', E: 'E', G: 'G', P: 'P' } },
|
|
||||||
{ name: 'CP-Silberagouti', tokens: { A: 'A', C: 'cchm', D: 'D', E: 'E', G: 'g', P: 'P' }, image: 'silberagouti-cp.JPG' },
|
{ name: 'CP-Silberagouti', tokens: { A: 'A', C: 'cchm', D: 'D', E: 'E', G: 'g', P: 'P' }, image: 'silberagouti-cp.JPG' },
|
||||||
{ name: 'CP-Silberagouti-Hell', tokens: { A: 'A', C: 'cchm/ch', D: 'D', E: 'E', G: 'g', P: 'P' } },
|
|
||||||
{ name: 'CP-Algierfuchs', tokens: { A: 'A', C: 'cchm', D: 'D', E: 'e', G: 'G', P: 'P' }, image: 'algierfuchs-cp.jpg' },
|
{ name: 'CP-Algierfuchs', tokens: { A: 'A', C: 'cchm', D: 'D', E: 'e', G: 'G', P: 'P' }, image: 'algierfuchs-cp.jpg' },
|
||||||
{ name: 'CP-Algierfuchs-Hell', tokens: { A: 'A', C: 'cchm/ch', D: 'D', E: 'e', G: 'G', P: 'P' } },
|
|
||||||
{ name: 'CP-Polarfuchs', tokens: { A: 'A', C: 'cchm', D: 'D', E: 'e', G: 'g', P: 'P' }, image: 'polarfuchs-cp.jpg' },
|
{ name: 'CP-Polarfuchs', tokens: { A: 'A', C: 'cchm', D: 'D', E: 'e', G: 'g', P: 'P' }, image: 'polarfuchs-cp.jpg' },
|
||||||
{ name: 'CP-Polarfuchs-Hell', tokens: { A: 'A', C: 'cchm/ch', D: 'D', E: 'e', G: 'g', P: 'P' } },
|
|
||||||
{ name: 'CP-Fuchs', tokens: { A: 'A', C: 'cchm', D: 'd', E: 'e', G: 'G', P: 'P' } },
|
{ name: 'CP-Fuchs', tokens: { A: 'A', C: 'cchm', D: 'd', E: 'e', G: 'G', P: 'P' } },
|
||||||
{ name: 'CP-Fuchs-Hell', tokens: { A: 'A', C: 'cchm/ch', D: 'd', E: 'e', G: 'G', P: 'P' } },
|
{ name: 'CP-Fuchs-Hell', tokens: { A: 'A', C: 'cchm/ch', D: 'd', E: 'e', G: 'G', P: 'P' } },
|
||||||
{ name: 'CP-Blaufuchs', tokens: { A: 'A', C: 'cchm', D: 'd', E: 'e', G: 'g', P: 'P' } },
|
{ name: 'CP-Blaufuchs', tokens: { A: 'A', C: 'cchm', D: 'd', E: 'e', G: 'g', P: 'P' } },
|
||||||
{ name: 'CP-Orangeschimmel', tokens: { C: 'cchm', D: 'D', E: 'ef', G: 'G', P: 'P' } },
|
{ name: 'CP-Orangeschimmel', tokens: { C: 'cchm', D: 'D', E: 'ef', G: 'G', P: 'P' } },
|
||||||
{ name: 'CP-Orangeschimmel-Hell', tokens: { C: 'cchm/ch', D: 'D', E: 'ef', G: 'G', P: 'P' } },
|
|
||||||
]
|
]
|
||||||
|
|
||||||
export const UNKNOWN_FARBSCHLAG = 'Unbekannter Farbschlag'
|
export const UNKNOWN_FARBSCHLAG = 'Unbekannter Farbschlag'
|
||||||
@@ -209,14 +207,12 @@ function baseColourFor(g: Genotype): string | null {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* GEN-3e/3g: the C-locus colourpoint NAMING transform (breeder-authoritative).
|
* GEN-3e: the C-locus colourpoint NAMING transform (breeder-authoritative).
|
||||||
* Returns the colourpoint name, or null when it doesn't apply (full C present,
|
* Returns the colourpoint name, or null when it doesn't apply (full C present,
|
||||||
* or chch — which the base matcher names Hermelin/Himalaya, preserving both).
|
* or chch — which the base matcher names Hermelin/Himalaya, preserving both).
|
||||||
* aa cchm/cchm -> Marder | aa cchm/ch -> Siam
|
* aa cchm/cchm -> Marder | aa cchm/ch -> Siam
|
||||||
* aa cchm/cchm gg -> Zobel | aa cchm/ch gg -> Zobel-Hell
|
* aa cchm/cchm gg -> Zobel | aa cchm/ch gg -> Zobel-Hell
|
||||||
* A- cchm/cchm -> CP-<base> | A- cchm/ch -> CP-<base>-Hell
|
* A- cchm/cchm | cchm/ch -> CP-<base colour> (base computed as if C were full)
|
||||||
* GEN-3g (breeder rule): "-Hell" in variety name == c[h]-Allel (cchm/ch het);
|
|
||||||
* no "-Hell" == cchm/cchm hom. CP-Fuchs is a Sammelbegriff (unknown loci).
|
|
||||||
*/
|
*/
|
||||||
function colourpointName(g: Genotype): string | null {
|
function colourpointName(g: Genotype): string | null {
|
||||||
const c = resolvedPair(g, 'C')
|
const c = resolvedPair(g, 'C')
|
||||||
@@ -231,9 +227,9 @@ function colourpointName(g: Genotype): string | null {
|
|||||||
if (grey) return bothCchm ? 'Zobel' : 'Zobel-Hell'
|
if (grey) return bothCchm ? 'Zobel' : 'Zobel-Hell'
|
||||||
return bothCchm ? 'Marder' : 'Siam'
|
return bothCchm ? 'Marder' : 'Siam'
|
||||||
}
|
}
|
||||||
// A- colourpoint: base as if C were full; het (cchm/ch) -> '-Hell' suffix.
|
// A- colourpoint -> CP-<base colour>, base as if C were full.
|
||||||
const base = baseColourFor(makeGenotype({ ...g, C: ['C', 'C'] }))
|
const base = baseColourFor(makeGenotype({ ...g, C: ['C', 'C'] }))
|
||||||
return base ? `CP-${base}${bothCchm ? '' : '-Hell'}` : null
|
return base ? `CP-${base}` : null
|
||||||
}
|
}
|
||||||
|
|
||||||
export function farbschlagFor(g: Genotype): FarbschlagMatch {
|
export function farbschlagFor(g: Genotype): FarbschlagMatch {
|
||||||
|
|||||||
@@ -340,67 +340,42 @@
|
|||||||
"sortOrder": 53,
|
"sortOrder": 53,
|
||||||
"image": "agouti-cp.jpg"
|
"image": "agouti-cp.jpg"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"name": "CP-Agouti-Hell",
|
|
||||||
"canonicalGenotype": "AA cchmch DD EE GG PP spsp rere",
|
|
||||||
"sortOrder": 54
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "CP-Silberagouti",
|
"name": "CP-Silberagouti",
|
||||||
"canonicalGenotype": "AA cchmcchm DD EE gg PP spsp rere",
|
"canonicalGenotype": "AA cchmcchm DD EE gg PP spsp rere",
|
||||||
"sortOrder": 55,
|
"sortOrder": 54,
|
||||||
"image": "silberagouti-cp.JPG"
|
"image": "silberagouti-cp.JPG"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"name": "CP-Silberagouti-Hell",
|
|
||||||
"canonicalGenotype": "AA cchmch DD EE gg PP spsp rere",
|
|
||||||
"sortOrder": 56
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "CP-Algierfuchs",
|
"name": "CP-Algierfuchs",
|
||||||
"canonicalGenotype": "AA cchmcchm DD ee GG PP spsp rere",
|
"canonicalGenotype": "AA cchmcchm DD ee GG PP spsp rere",
|
||||||
"sortOrder": 57,
|
"sortOrder": 55,
|
||||||
"image": "algierfuchs-cp.jpg"
|
"image": "algierfuchs-cp.jpg"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"name": "CP-Algierfuchs-Hell",
|
|
||||||
"canonicalGenotype": "AA cchmch DD ee GG PP spsp rere",
|
|
||||||
"sortOrder": 58
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "CP-Polarfuchs",
|
"name": "CP-Polarfuchs",
|
||||||
"canonicalGenotype": "AA cchmcchm DD ee gg PP spsp rere",
|
"canonicalGenotype": "AA cchmcchm DD ee gg PP spsp rere",
|
||||||
"sortOrder": 59,
|
"sortOrder": 56,
|
||||||
"image": "polarfuchs-cp.jpg"
|
"image": "polarfuchs-cp.jpg"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"name": "CP-Polarfuchs-Hell",
|
|
||||||
"canonicalGenotype": "AA cchmch DD ee gg PP spsp rere",
|
|
||||||
"sortOrder": 60
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "CP-Fuchs",
|
"name": "CP-Fuchs",
|
||||||
"canonicalGenotype": "AA cchmcchm dd ee GG PP spsp rere",
|
"canonicalGenotype": "AA cchmcchm dd ee GG PP spsp rere",
|
||||||
"sortOrder": 61
|
"sortOrder": 57
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "CP-Fuchs-Hell",
|
"name": "CP-Fuchs-Hell",
|
||||||
"canonicalGenotype": "AA cchmch dd ee GG PP spsp rere",
|
"canonicalGenotype": "AA cchmch dd ee GG PP spsp rere",
|
||||||
"sortOrder": 62
|
"sortOrder": 58
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "CP-Blaufuchs",
|
"name": "CP-Blaufuchs",
|
||||||
"canonicalGenotype": "AA cchmcchm dd ee gg PP spsp rere",
|
"canonicalGenotype": "AA cchmcchm dd ee gg PP spsp rere",
|
||||||
"sortOrder": 63
|
"sortOrder": 59
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "CP-Orangeschimmel",
|
"name": "CP-Orangeschimmel",
|
||||||
"canonicalGenotype": "AA cchmcchm DD efef GG PP spsp rere",
|
"canonicalGenotype": "AA cchmcchm DD efef GG PP spsp rere",
|
||||||
"sortOrder": 64
|
"sortOrder": 60
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "CP-Orangeschimmel-Hell",
|
|
||||||
"canonicalGenotype": "AA cchmch DD efef GG PP spsp rere",
|
|
||||||
"sortOrder": 65
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ _Automatisch erzeugt von `tools/import/extract.py` — **noch nichts in die Date
|
|||||||
- Nach Zusammenführung (eindeutige Tiere): **621**
|
- Nach Zusammenführung (eindeutige Tiere): **621**
|
||||||
- davon mit Geburtsdatum: 326
|
- davon mit Geburtsdatum: 326
|
||||||
- in mehreren Dateien gefunden (Dubletten zusammengeführt): 158
|
- in mehreren Dateien gefunden (Dubletten zusammengeführt): 158
|
||||||
- Konflikte zur Klärung: **5**
|
- Konflikte zur Klärung: **8**
|
||||||
- Mehrdeutige / unvollständige Einträge (ohne Name+Datum): **310**
|
- Mehrdeutige / unvollständige Einträge (ohne Name+Datum): **310**
|
||||||
- Fotos zugeordnet: **137**
|
- Fotos zugeordnet: **137**
|
||||||
- Würfe aus der Wurfchronik: **752**
|
- Würfe aus der Wurfchronik: **752**
|
||||||
@@ -25,9 +25,12 @@ Gleiches Tier (Name+Datum), aber widersprüchliche Angaben in verschiedenen Date
|
|||||||
|
|
||||||
| Tier | Geburtsdatum | abweichende Genotypen | abweichende Farbschläge | Sterbedaten | Dateien |
|
| Tier | Geburtsdatum | abweichende Genotypen | abweichende Farbschläge | Sterbedaten | Dateien |
|
||||||
|---|---|---|---|---|---|
|
|---|---|---|---|---|---|
|
||||||
|
| Ella | 10.06.2019 | Aa C D- ee[f] GG P- spsp // Aa Cc[chm] D- ee[f] UwUw P- spsp | Algierfuchsschimmel, hell | 03.02.2023 | Stammbaum von Akio Kids, Stammbaum von CP-Fuchs, CP-Sa Sp von Unity, Stammbaum von Valentino Firehearts Kids |
|
||||||
| Kazu von den Kleinen Chaoten | 23.04.2013 | Aa Cc[chm] DD e[f]e[f] Gg P Spsp // Aa Cc[chm] DD ee[f] UwUw PP Spsp | — | 03.09.2017 | Stammbaum von Akio Kids, Stammbaum von Vance |
|
| Kazu von den Kleinen Chaoten | 23.04.2013 | Aa Cc[chm] DD e[f]e[f] Gg P Spsp // Aa Cc[chm] DD ee[f] UwUw PP Spsp | — | 03.09.2017 | Stammbaum von Akio Kids, Stammbaum von Vance |
|
||||||
|
| Enya von den Kleinen Chaoten | 01.11.2017 | Aa c[chm]c[chm] D- ee[-] G- P- spsp // Aa c[chm]c[chm] D- ee[-] Uwuw[d] P- spsp | — | — | Stammbaum von Alberto Kids, Stammbaum von Fire Kids, Stammbaum von Kohlief, Goldfuchsef Sp von Chrissi, Stammbaum von Stella Kids |
|
||||||
| Little Runner's Big Ben | 03.02.2020 | Aa Cc[chm] DD Ee Gg PP Spsp // Aa Cc[chm] DD Ee Gg Pp Spsp | — | 14.10.2023 | Stammbaum von CP-Fuchs, CP-Sa Sp von Unity, Stammbaum von Fire Kids, Stammbaum von Goldfuchs Sp (Pikachu) Kids, Stammbaum von Kohlief, Goldfuchsef Sp von Chrissi, Stammbaum von Valentino Firehearts Kids, Stammbaum von Watarus Kids |
|
| Little Runner's Big Ben | 03.02.2020 | Aa Cc[chm] DD Ee Gg PP Spsp // Aa Cc[chm] DD Ee Gg Pp Spsp | — | 14.10.2023 | Stammbaum von CP-Fuchs, CP-Sa Sp von Unity, Stammbaum von Fire Kids, Stammbaum von Goldfuchs Sp (Pikachu) Kids, Stammbaum von Kohlief, Goldfuchsef Sp von Chrissi, Stammbaum von Valentino Firehearts Kids, Stammbaum von Watarus Kids |
|
||||||
| Vance Jr. von den Kleinen Chaoten | 10.04.2022 | aa Cc[hm] Dd Ee gg P- Spsp // aa Cc[hm] Dd Ee gg P- spsp | Kohlfuchs, hell | — | Stammbaum von Fire Kids, Stammbaum von Stella Kids |
|
| Vance Jr. von den Kleinen Chaoten | 10.04.2022 | aa Cc[hm] Dd Ee gg P- Spsp // aa Cc[hm] Dd Ee gg P- spsp | Kohlfuchs, hell | — | Stammbaum von Fire Kids, Stammbaum von Stella Kids |
|
||||||
|
| Zac gen. Action von den Kleinen Chaoten | 25.12.2020 | aa C- D- Ee G- Pp Spsp [DP] // aa CC D- Ee G- Pp Spsp [DP] | — | 31.01.2025 | Stammbaum von Goldfuchs Sp (Pikachu) Kids, Stammbaum von Kohlief, Goldfuchsef Sp von Chrissi, Stammbaum von Watarus Kids |
|
||||||
| Hanami von den Kleinen Chaoten | 10.09.2015 | aa Cc[chm] D- Ee gg P- spsp | — | 12.12.2019 // 14.01.2020 | Stammbaum von Kentucky, Stammbaum von Stella Kids |
|
| Hanami von den Kleinen Chaoten | 10.09.2015 | aa Cc[chm] D- Ee gg P- spsp | — | 12.12.2019 // 14.01.2020 | Stammbaum von Kentucky, Stammbaum von Stella Kids |
|
||||||
| Skarlett v.d. Kleinen Chaoten | 14.07.2013 | / +2018, Aa Cc[chm] DD ee uw[d]uw[d] PP spsp // Aa Cc[chm] DD ee uw[d]uw[d] PP spsp | — | 17.04.2016 // 2018 | Stammbaum von Vance |
|
| Skarlett v.d. Kleinen Chaoten | 14.07.2013 | / +2018, Aa Cc[chm] DD ee uw[d]uw[d] PP spsp // Aa Cc[chm] DD ee uw[d]uw[d] PP spsp | — | 17.04.2016 // 2018 | Stammbaum von Vance |
|
||||||
|
|
||||||
@@ -126,7 +129,7 @@ Diese Tokens stehen weiter in `rawGenotype`/`unmappedTokens` — Entscheidung (M
|
|||||||
|
|
||||||
| Token | Vorkommen | Bedeutung (Vermutung) |
|
| Token | Vorkommen | Bedeutung (Vermutung) |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `/+` | 6 | ? |
|
| `/+` | 7 | ? |
|
||||||
| `-g` | 2 | ? |
|
| `-g` | 2 | ? |
|
||||||
| `C(C)` | 2 | Schreibweise (C trägt c) |
|
| `C(C)` | 2 | Schreibweise (C trägt c) |
|
||||||
| `Cc[]` | 1 | ? |
|
| `Cc[]` | 1 | ? |
|
||||||
|
|||||||
Reference in New Issue
Block a user