chore(frontend): commit remaining workspace updates

This commit is contained in:
2026-10-01 12:24:50 +03:30
parent 0f5ec429d0
commit 57c8da1326
8 changed files with 56 additions and 22 deletions

View File

@@ -22,7 +22,10 @@ type Props = {
onOpenChange: (v: boolean) => void;
basePrice: number; // مبلغ اولیه رویداد
onVerifyCouponRaw: (code: string) => Promise<RawVerifyResult>;
onContinue: (coupon?: string, finalPrice?: number) => void; // ادامه‌ی جریان ثبت‌نام/پرداخت
onContinue: (coupon?: string, finalPrice?: number) => void | Promise<void>; // ادامه‌ی جریان ثبت‌نام/پرداخت
continueError?: string | null;
isSubmitting?: boolean;
onContinueErrorClear?: () => void;
};
export default function CouponDialogFa({
@@ -31,6 +34,9 @@ export default function CouponDialogFa({
basePrice,
onVerifyCouponRaw,
onContinue,
continueError,
isSubmitting = false,
onContinueErrorClear,
}: Props) {
const [code, setCode] = React.useState("");
const [verifying, setVerifying] = React.useState(false);
@@ -80,11 +86,15 @@ export default function CouponDialogFa({
<Input
id="coupon"
value={code}
onChange={(e) => setCode(e.target.value)}
onChange={(e) => {
setCode(e.target.value);
setRes(null);
onContinueErrorClear?.();
}}
placeholder="مثلاً OFF20"
className="text-right"
/>
<Button variant="secondary" disabled={!code || verifying} onClick={handleVerify}>
<Button type="button" variant="secondary" disabled={!code.trim() || verifying || isSubmitting} onClick={handleVerify}>
{verifying ? "در حال بررسی..." : "بررسی کد"}
</Button>
</div>
@@ -117,16 +127,22 @@ export default function CouponDialogFa({
<span className="font-semibold">{formatNumberPersian(finalPrice)} تومان</span>
</div>
</div>
{continueError ? (
<p role="alert" className="rounded-md border border-destructive/30 bg-destructive/5 p-3 text-sm leading-6 text-foreground">
{continueError}
</p>
) : null}
</div>
<DialogFooter className="sm:justify-start">
<div className="flex w-full items-center justify-end gap-2">
<Button variant="outline" onClick={() => onOpenChange(false)}>انصراف</Button>
<Button type="button" variant="outline" disabled={isSubmitting} onClick={() => onOpenChange(false)}>انصراف</Button>
<Button
onClick={() => onContinue(code || undefined, finalPrice)}
disabled={verifying /* در حال بررسی که هستیم، ادامه غیرفعال باشد */}
type="button"
onClick={() => void onContinue(code.trim() || undefined, finalPrice)}
disabled={verifying || isSubmitting || Boolean(code.trim() && !res?.valid)}
>
ادامه و پرداخت
{isSubmitting ? "در حال آماده‌سازی..." : finalPrice === 0 ? "ثبت‌نام رایگان با کد تخفیف" : "ادامه و پرداخت"}
</Button>
</div>
</DialogFooter>

View File

@@ -23,6 +23,10 @@ type MarkdownProps = {
size?: MarkdownSize;
};
type MarkdownNodeWithPosition = {
position?: { start?: { line?: number } };
};
function CodeBlock({
className,
children,
@@ -92,7 +96,8 @@ export default function Markdown({
}: MarkdownProps) {
const rehypePlugins: PluggableList | undefined = allowHtml ? [rehypeRaw, rehypeSanitize] : undefined;
const headings = extractMarkdownHeadings(content);
let headingIndex = 0;
const headingIdsByLine = new Map<number, string>();
headings.forEach(({ line, id }) => headingIdsByLine.set(line, id));
const baseSizeClass =
size === "sm" ? "text-sm" : size === "lg" ? "text-lg" : "text-base";
@@ -108,13 +113,9 @@ export default function Markdown({
? { textAlign: "justify", textJustify: "inter-word" }
: undefined;
const nextHeadingId = (level: 1 | 2 | 3) => {
while (headingIndex < headings.length) {
const heading = headings[headingIndex];
headingIndex += 1;
if (heading.level === level) return heading.id;
}
return undefined;
const headingId = (node?: MarkdownNodeWithPosition) => {
const line = node?.position?.start?.line;
return line ? headingIdsByLine.get(line) : undefined;
};
return (
@@ -127,9 +128,9 @@ export default function Markdown({
remarkPlugins={[remarkGfm]}
rehypePlugins={rehypePlugins}
components={{
h1: (p) => <h1 {...p} id={nextHeadingId(1)} className={cn("scroll-mt-28 pt-2 text-right font-bold", hScale.h1)} style={{ ...p.style, textAlign: "right" }} />,
h2: (p) => <h2 {...p} id={nextHeadingId(2)} className={cn("scroll-mt-28 pt-2 text-right font-bold", hScale.h2)} style={{ ...p.style, textAlign: "right" }} />,
h3: (p) => <h3 {...p} id={nextHeadingId(3)} className={cn("scroll-mt-28 pt-2 text-right font-semibold", hScale.h3)} style={{ ...p.style, textAlign: "right" }} />,
h1: (p) => <h1 {...p} id={headingId(p.node)} className={cn("scroll-mt-28 pt-2 text-right font-bold", hScale.h1)} style={{ ...p.style, textAlign: "right" }} />,
h2: (p) => <h2 {...p} id={headingId(p.node)} className={cn("scroll-mt-28 pt-2 text-right font-bold", hScale.h2)} style={{ ...p.style, textAlign: "right" }} />,
h3: (p) => <h3 {...p} id={headingId(p.node)} className={cn("scroll-mt-28 pt-2 text-right font-semibold", hScale.h3)} style={{ ...p.style, textAlign: "right" }} />,
h4: (p) => <h4 {...p} className={cn("mt-4 text-right font-semibold", hScale.h4)} style={{ ...p.style, textAlign: "right" }} />,
p: (p) => <p className="my-4" {...p} />,
a: (p) => <a className="break-all underline decoration-primary hover:opacity-90" target="_blank" rel="noopener noreferrer" {...p} />,

View File

@@ -16,6 +16,7 @@ const Slider = React.forwardRef<
<SliderPrimitive.Range className="absolute h-full bg-primary" />
</SliderPrimitive.Track>
<SliderPrimitive.Thumb className="block h-5 w-5 rounded-full border-2 border-primary bg-background ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50" />
<SliderPrimitive.Thumb className="block h-5 w-5 rounded-full border-2 border-primary bg-background ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50" />
</SliderPrimitive.Root>
));
Slider.displayName = SliderPrimitive.Root.displayName;

View File

@@ -254,7 +254,7 @@ const MOCK_PAGE_CONTENT: Record<ContentPageSlug, CmsContentPage> = {
members_intro: "با اعضایی آشنا شوید که برای ساختن تجربه‌های بهتر در انجمن همکاری می‌کنند.",
gallery_heading: "خاطرات انجمن",
gallery_intro: "تصاویری از گردهمایی‌ها، برنامه‌ها و لحظه‌هایی که کنار هم ساخته‌ایم.",
past_events_heading: "رویدادهای برگزارشده",
past_events_heading: "رویدادهای برگزار شده",
history_heading: "مسیر انجمن",
audience_heading: "برای چه کسانی؟",
year_events_heading: "لحظه‌های هر سال",

View File

@@ -2,6 +2,7 @@ export type MarkdownHeading = {
id: string;
level: 1 | 2 | 3;
text: string;
line: number;
};
function plainHeadingText(value: string) {
@@ -32,7 +33,7 @@ export function extractMarkdownHeadings(content?: string): MarkdownHeading[] {
return (content || "")
.split(/\r?\n/)
.map((line) => {
.map((line, index) => {
const match = /^(#{1,3})\s+(.+?)\s*#*$/.exec(line.trim());
if (!match) return null;
@@ -46,6 +47,7 @@ export function extractMarkdownHeadings(content?: string): MarkdownHeading[] {
id: nextCount === 1 ? base : `${base}-${nextCount}`,
level,
text,
line: index + 1,
};
})
.filter(Boolean) as MarkdownHeading[];