fix: sidebar accordion + koja slug + support ticket LINQ crash
CI/CD / CI · API (dotnet build + test) (push) Successful in 5m50s
CI/CD / CI · Admin API (dotnet build) (push) Successful in 32s
CI/CD / CI · Dashboard (tsc) (push) Successful in 1m3s
CI/CD / CI · Admin Web (tsc) (push) Successful in 35s
CI/CD / CI · Website (tsc) (push) Successful in 44s
CI/CD / CI · Koja (tsc) (push) Successful in 48s
CI/CD / Deploy · all services (push) Has been cancelled

Sidebar:
- All groups start collapsed on first load (v4 storage key resets old state)
- Opening one group closes all others (accordion)
- Navigating to a section opens only that section's group

Koja slug:
- SlugHelper: Persian->Latin transliteration, slug validation
- Registration accepts optional custom slug; auto-derives from cafe name
- Slug can be updated from dashboard Settings -> Profile
- Settings PATCH validates uniqueness (SLUG_TAKEN) and format (INVALID_SLUG)
- koja.meezi.ir/{slug} now redirects to /fa/cafe/{slug} (short URL support)

Bug fix:
- SupportTicketService: cafeId/status filters applied before Select() projection
  to fix EF "could not be translated" crash on the support tickets page

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
soroush.asadi
2026-05-31 22:28:25 +03:30
parent 38e3f6a5a2
commit cd1af30bbc
17 changed files with 401 additions and 58 deletions
+87
View File
@@ -0,0 +1,87 @@
using System.Text;
using System.Text.RegularExpressions;
namespace Meezi.Core.Utilities;
/// <summary>
/// Converts Persian/Arabic café names to URL-safe Latin slugs.
/// Used for Koja profile URLs (koja.meezi.ir/fa/cafe/{slug}).
/// </summary>
public static partial class SlugHelper
{
private static readonly Dictionary<char, string> PersianToLatin = new()
{
// Alef variants
{ 'آ', "a" }, { 'ا', "a" }, { 'أ', "a" }, { 'إ', "a" },
// Ba, Pa, Ta, Tha
{ 'ب', "b" }, { 'پ', "p" }, { 'ت', "t" }, { 'ث', "s" },
// Jim, Che, He, Khe
{ 'ج', "j" }, { 'چ', "ch" }, { 'ح', "h" }, { 'خ', "kh" },
// Dal, Zal, Re, Ze, Zhe
{ 'د', "d" }, { 'ذ', "z" }, { 'ر', "r" }, { 'ز', "z" }, { 'ژ', "zh" },
// Sin, Shin, Sad, Zad
{ 'س', "s" }, { 'ش', "sh" }, { 'ص', "s" }, { 'ض', "z" },
// Ta, Za, Ain, Ghain
{ 'ط', "t" }, { 'ظ', "z" }, { 'ع', "a" }, { 'غ', "gh" },
// Fa, Ghaf, Kaf (Arabic+Persian), Gaf
{ 'ف', "f" }, { 'ق', "gh" }, { 'ک', "k" }, { 'ك', "k" }, { 'گ', "g" },
// Lam, Mim, Nun, Vav, He, Ye
{ 'ل', "l" }, { 'م', "m" }, { 'ن', "n" }, { 'و', "v" },
{ 'ه', "h" }, { 'ی', "i" }, { 'ي', "i" },
// Special
{ 'ئ', "y" }, { 'ء', "" }, { 'ة', "t" }, { 'ى', "a" }, { 'ؤ', "o" },
// Persian digits
{ '۰', "0" }, { '۱', "1" }, { '۲', "2" }, { '۳', "3" }, { '۴', "4" },
{ '۵', "5" }, { '۶', "6" }, { '۷', "7" }, { '۸', "8" }, { '۹', "9" },
// Arabic-Indic digits
{ '٠', "0" }, { '١', "1" }, { '٢', "2" }, { '٣', "3" }, { '٤', "4" },
{ '٥', "5" }, { '٦', "6" }, { '٧', "7" }, { '٨', "8" }, { '٩', "9" },
};
/// <summary>
/// Converts a café name (Persian or Latin) to a URL-safe lowercase slug.
/// Returns an empty string if no valid characters can be extracted.
/// </summary>
public static string Slugify(string input)
{
if (string.IsNullOrWhiteSpace(input)) return string.Empty;
var sb = new StringBuilder(input.Length * 2);
foreach (var ch in input)
{
if (PersianToLatin.TryGetValue(ch, out var latin))
{
sb.Append(latin);
}
else if (char.IsAsciiLetterOrDigit(ch))
{
sb.Append(char.ToLowerInvariant(ch));
}
else if (ch is ' ' or '-' or '_' or '\t')
{
sb.Append('-');
}
// else: skip punctuation/unsupported characters
}
// Collapse consecutive hyphens and trim
return MultipleHyphen().Replace(sb.ToString(), "-").Trim('-');
}
/// <summary>
/// Returns true if the slug is a valid Koja URL slug:
/// 280 lowercase letters, digits, or internal hyphens. Must start and end with a letter/digit.
/// </summary>
public static bool IsValidSlug(string? slug)
{
if (string.IsNullOrWhiteSpace(slug)) return false;
if (slug.Length < 2 || slug.Length > 80) return false;
return ValidSlugPattern().IsMatch(slug);
}
[GeneratedRegex(@"-{2,}")]
private static partial Regex MultipleHyphen();
[GeneratedRegex(@"^[a-z0-9][a-z0-9\-]*[a-z0-9]$")]
private static partial Regex ValidSlugPattern();
}