chore(frontend): commit remaining workspace updates
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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} />,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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: "لحظههای هر سال",
|
||||
|
||||
@@ -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[];
|
||||
|
||||
Reference in New Issue
Block a user