diff --git a/frontend/src/components/DetailsDialog.tsx b/frontend/src/components/DetailsDialog.tsx index 709da46..2436f79 100644 --- a/frontend/src/components/DetailsDialog.tsx +++ b/frontend/src/components/DetailsDialog.tsx @@ -1,108 +1,117 @@ import { useMemo, useState } from "react"; import { - Dialog, - DialogContent, - DialogHeader, - DialogTitle, - DialogFooter, - DialogClose, + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogFooter, + DialogClose, } from "@/components/ui/dialog"; import { Button } from "@/components/ui/button"; import { MoreHorizontal } from "lucide-react"; type DetailsCellProps = { - text?: string | null; - limit?: number; - title?: string; - triggerVariant?: "text" | "icon"; - justify?: "between" | "start" | "center" + text?: string | null; + limit?: number; + title?: string; + triggerVariant?: "text" | "icon"; + justify?: "between" | "start" | "center"; }; export function DetailsDialog({ - text, - limit = 25, - title = "Details", - triggerVariant = "text", - justify = "between" + text, + limit = 25, + title = "Details", + triggerVariant = "text", + justify = "between", }: DetailsCellProps) { - const [open, setOpen] = useState(false); - const value = text ?? ""; - - const isTruncated = value.length > limit; - const wrapperJustifyClass = justify === "start" ? "justify-start" : justify === "center" ? "justify-center" : "justify-between" - - const display = useMemo(() => { - if (!value) return ""; - - if (!isTruncated) return value; - - if (triggerVariant === "text") { - return value.slice(0, limit) + "..."; - } - - return value.slice(0, limit); - }, [value, limit, isTruncated, triggerVariant]); - - const handleOpen = () => setOpen(true); - - return ( - <> -
-
- {display} -
- - - {isTruncated && ( - <> - {triggerVariant === "text" && ( - - )} - - {triggerVariant === "icon" && ( - + )} + + {triggerVariant === "icon" && ( + - )} - - )} + title="See More" + > + + + )} + + )} +
+ + + +
+ + {title} +
+ + +
+
+ {value || "No details available."} +
- - - -
- - {title} -
- - -
-
- {value || "No details available."} -
-
- - - - - - -
- -
- - ); + + + + + +
+ +
+ + ); } diff --git a/frontend/src/components/OBSViewDialog.tsx b/frontend/src/components/OBSViewDialog.tsx index e755946..9a81b0e 100644 --- a/frontend/src/components/OBSViewDialog.tsx +++ b/frontend/src/components/OBSViewDialog.tsx @@ -613,7 +613,7 @@ export const OBSViewDialog = ({ className="flex-1 overflow-auto border rounded" > {!editMode && ( -
+
{previewMode ? ( + header + .replace(/^\uFEFF/, "") + .trim() + .toLowerCase(); + export const isISLOBSCSV = (headers: string[]) => - ISL_HEADERS.every((h) => - headers.map((x) => x.toLowerCase()).includes(h.toLowerCase()), - ); + ISL_HEADERS.every((h) => headers.map(cleanHeader).includes(h.toLowerCase())); + +function extractHeadingData(headingLine: string): { + story_no: number; + title: string; +} { + const normalized = headingLine.normalize("NFC"); + + const match = normalized.match(OBS_HEADING_REGEX); + + if (!match) { + throw new Error( + "Invalid OBS heading. Expected '# 1. Title' or '# 1۔ عنوان'", + ); + } + + return { + story_no: Number(match[1]), + title: match[2].trim(), + }; +} export function parseISLOBSCSV(file: File): Promise { return new Promise((resolve, reject) => { @@ -40,23 +69,16 @@ export function parseISLOBSCSV(file: File): Promise { export async function parseOBSMarkdown(file: File): Promise { const content = await file.text(); - const lines = content.split("\n"); - // markdown heading - const headingLine = lines.find((l) => l.startsWith("#")); + const lines = normalizeContent(content).split("\n"); + + const headingLine = lines.find((l) => l.trim().startsWith("#")); if (!headingLine) { throw new Error("Invalid OBS markdown: missing heading"); } - const match = headingLine.match(/^#+\s*(\d+)[.\s]+(.+)/); - - if (!match) { - throw new Error("Invalid OBS heading. Expected format: '# 1. Title'"); - } - - const story_no = Number(match[1]); - const title = match[2].trim(); + const { story_no, title } = extractHeadingData(headingLine); return { story_no, @@ -77,21 +99,19 @@ export function extractOBSMetadataFromMarkdown(markdown: string): { story_no: number; title: string; } { - const lines = markdown.split("\n"); - const headingLine = lines.find((l) => l.startsWith("#")); + const lines = normalizeContent(markdown).split("\n"); + + const headingLine = lines.find((l) => l.trim().startsWith("#")); if (!headingLine) { throw new Error("Invalid OBS markdown: missing heading"); } - const match = headingLine.match(/^#+\s*(\d+)[.\s]+(.+)/); - - if (!match) { - throw new Error("Invalid OBS heading. Expected format: '# 1. Title'"); - } - - return { - story_no: Number(match[1]), - title: match[2].trim(), - }; + return extractHeadingData(headingLine); } + +const normalizeContent = (content: string) => + content + .replace(/^\uFEFF/, "") + .replace(/\r\n/g, "\n") + .replace(/\r/g, "\n"); \ No newline at end of file