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
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:
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useState, useEffect } from "react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useRouter, Link } from "@/i18n/routing";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
@@ -14,6 +14,46 @@ import { LabeledField } from "@/components/ui/labeled-field";
|
||||
import { OtpInput } from "@/components/ui/otp-input";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
|
||||
/** Client-side Persian-to-Latin slugifier — mirrors SlugHelper.Slugify on the backend */
|
||||
const PERSIAN_MAP: Record<string, string> = {
|
||||
آ: "a", ا: "a", أ: "a", إ: "a",
|
||||
ب: "b", پ: "p", ت: "t", ث: "s",
|
||||
ج: "j", چ: "ch", ح: "h", خ: "kh",
|
||||
د: "d", ذ: "z", ر: "r", ز: "z", ژ: "zh",
|
||||
س: "s", ش: "sh", ص: "s", ض: "z",
|
||||
ط: "t", ظ: "z", ع: "a", غ: "gh",
|
||||
ف: "f", ق: "gh", ک: "k", ك: "k", گ: "g",
|
||||
ل: "l", م: "m", ن: "n", و: "v",
|
||||
ه: "h", ی: "i", ي: "i",
|
||||
ئ: "y", ء: "", ة: "t", ى: "a", ؤ: "o",
|
||||
"۰": "0", "۱": "1", "۲": "2", "۳": "3", "۴": "4",
|
||||
"۵": "5", "۶": "6", "۷": "7", "۸": "8", "۹": "9",
|
||||
};
|
||||
|
||||
function slugify(input: string): string {
|
||||
let s = "";
|
||||
for (const ch of input) {
|
||||
if (ch in PERSIAN_MAP) {
|
||||
s += PERSIAN_MAP[ch];
|
||||
} else if (/[a-zA-Z0-9]/.test(ch)) {
|
||||
s += ch.toLowerCase();
|
||||
} else if (/[\s\-_]/.test(ch)) {
|
||||
s += "-";
|
||||
}
|
||||
}
|
||||
return s.replace(/-{2,}/g, "-").replace(/^-+|-+$/g, "");
|
||||
}
|
||||
|
||||
function isValidSlug(slug: string): boolean {
|
||||
if (!slug || slug.length < 2 || slug.length > 80) return false;
|
||||
return /^[a-z0-9][a-z0-9\-]*[a-z0-9]$/.test(slug);
|
||||
}
|
||||
|
||||
const KOJA_BASE =
|
||||
typeof window !== "undefined" && window.location.hostname.includes("meezi.ir")
|
||||
? "koja.meezi.ir"
|
||||
: "koja.meezi.ir";
|
||||
|
||||
function RegisterForm() {
|
||||
const t = useTranslations("auth");
|
||||
const router = useRouter();
|
||||
@@ -22,20 +62,31 @@ function RegisterForm() {
|
||||
|
||||
const [phone, setPhone] = useState(searchParams.get("phone") ?? "");
|
||||
const [cafeName, setCafeName] = useState("");
|
||||
const [slug, setSlug] = useState("");
|
||||
const [slugEdited, setSlugEdited] = useState(false);
|
||||
const [code, setCode] = useState("");
|
||||
const [step, setStep] = useState<"info" | "otp">("info");
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
// Auto-derive slug from café name unless the user has manually edited it
|
||||
useEffect(() => {
|
||||
if (!slugEdited) {
|
||||
setSlug(slugify(cafeName));
|
||||
}
|
||||
}, [cafeName, slugEdited]);
|
||||
|
||||
const slugValid = isValidSlug(slug);
|
||||
|
||||
const errorMessage = (err: unknown) => {
|
||||
if (err instanceof ApiClientError) {
|
||||
switch (err.code) {
|
||||
case "RATE_LIMITED": return t("rateLimited");
|
||||
case "ALREADY_REGISTERED": return t("alreadyRegistered");
|
||||
case "SMS_FAILED": return t("smsFailed");
|
||||
case "INVALID_OTP": return t("invalidOtp");
|
||||
case "RATE_LIMITED": return t("rateLimited");
|
||||
case "ALREADY_REGISTERED": return t("alreadyRegistered");
|
||||
case "SMS_FAILED": return t("smsFailed");
|
||||
case "INVALID_OTP": return t("invalidOtp");
|
||||
case "REGISTRATION_EXPIRED": return t("registrationExpired");
|
||||
default: return err.message;
|
||||
default: return err.message;
|
||||
}
|
||||
}
|
||||
return err instanceof Error ? err.message : String(err);
|
||||
@@ -45,7 +96,11 @@ function RegisterForm() {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
await apiPost("/api/auth/register", { phone, cafeName });
|
||||
await apiPost("/api/auth/register", {
|
||||
phone,
|
||||
cafeName,
|
||||
slug: slugValid ? slug : undefined,
|
||||
});
|
||||
setStep("otp");
|
||||
} catch (e) {
|
||||
setError(errorMessage(e));
|
||||
@@ -94,6 +149,31 @@ function RegisterForm() {
|
||||
required
|
||||
/>
|
||||
</LabeledField>
|
||||
|
||||
{/* Koja slug / profile URL */}
|
||||
<LabeledField
|
||||
label={t("kojaSlug")}
|
||||
htmlFor="reg-slug"
|
||||
hint={t("kojaSlugHint")}
|
||||
>
|
||||
<Input
|
||||
id="reg-slug"
|
||||
value={slug}
|
||||
onChange={(e) => {
|
||||
setSlugEdited(true);
|
||||
setSlug(e.target.value.toLowerCase().replace(/[^a-z0-9-]/g, ""));
|
||||
}}
|
||||
placeholder={t("kojaSlugPlaceholder")}
|
||||
dir="ltr"
|
||||
className="text-start font-mono text-sm"
|
||||
/>
|
||||
{slug && (
|
||||
<p className={`mt-1 text-xs font-mono ${slugValid ? "text-muted-foreground" : "text-destructive"}`}>
|
||||
{KOJA_BASE}/{slug}
|
||||
</p>
|
||||
)}
|
||||
</LabeledField>
|
||||
|
||||
<LabeledField label={t("phone")} htmlFor="reg-phone">
|
||||
<Input
|
||||
id="reg-phone"
|
||||
|
||||
Reference in New Issue
Block a user